bemusic/bemuse

View on GitHub
bemuse/config/webpack.js

Summary

Maintainability
D
2 days
Test Coverage
import Gauge from 'gauge'
import TerserPlugin from 'terser-webpack-plugin'
import express from 'express'
import webpack from 'webpack'
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
import { ServiceWorkerPlugin } from 'service-worker-webpack'
import { flowRight } from 'lodash'

import * as Env from './env'
import path from './path'
import routes from './routes'
import webpackResolve from './webpackResolve'

function generateBaseConfig() {
  const config = {
    mode: Env.production() ? 'production' : 'development',
    context: path('src'),
    resolve: webpackResolve,
    resolveLoader: {
      alias: {
        bemuse: path('src'),
      },
    },
    devServer: {
      allowedHosts: 'all',
      static: false,
      devMiddleware: {
        publicPath: '/',
        stats: { colors: true, chunkModules: false },
      },
      setupMiddlewares: (middlewares, devServer) => {
        devServer.app.use('/', express.static(path('..', 'public')))
        for (const route of routes) {
          devServer.app.use(
            '/' + route.dest.join('/'),
            express.static(route.src)
          )
        }
        const cacheSettings = {
          etag: true,
          setHeaders(res) {
            res.setHeader('Cache-Control', 'public, max-age=31536000, no-cache')
          },
        }
        devServer.app.use(
          '/music',
          express.static(path('..', 'music'), cacheSettings)
        )
        devServer.app.use(
          '/coverage',
          express.static(path('coverage', 'lcov-report'))
        )
        return middlewares
      },
    },
    module: {
      strictExportPresence: true,
      exprContextCritical: false,
      rules: generateLoadersConfig(),
      noParse: [/sinon\.js/],
    },
    plugins: [
      new CompileProgressPlugin(),
      new webpack.ProvidePlugin({
        BemuseLogger: 'bemuse/logger',
        process: 'process/browser',
        Buffer: ['buffer', 'Buffer'],
      }),
    ],
  }

  if (Env.production()) {
    config.optimization = {
      minimize: true,
      minimizer: [
        new TerserPlugin({
          terserOptions: {
            format: { semicolons: false },
          },
        }),
      ],
    }
  }

  if (Env.sourceMapsEnabled() && Env.development()) {
    config.devtool = 'eval-source-map'
  } else if (Env.sourceMapsEnabled() || Env.production()) {
    config.devtool = 'source-map'
  } else if (Env.development()) {
    config.devtool = 'eval'
  }

  return config
}

function generateLoadersConfig() {
  return [
    ...(Env.coverageEnabled()
      ? [
          {
            test: /\.[jt]sx?$/,
            resourceQuery: { not: [/raw/] },
            include: [path('src')],
            use: {
              loader: '@ephesoft/webpack.istanbul.loader',
              options: { esModules: true },
            },
            enforce: 'post',
          },
        ]
      : []),
    {
      test: /\.spec\.js$/,
      use: [
        {
          loader: 'webpack-espower-loader',
        },
      ],
    },
    {
      test: /\.[jt]sx?$/,
      include: [path('src'), path('spec')],
      use: {
        loader: 'ts-loader',
        options: {
          transpileOnly: true,
          compilerOptions: {
            module: 'esnext',
            moduleResolution: 'node',
          },
        },
      },
    },
    {
      test: /\.js$/,
      type: 'javascript/auto',
      include: [path('node_modules', 'pixi.js')],
      use: {
        loader: 'transform-loader/cacheable',
        options: {
          brfs: true,
        },
      },
    },
    {
      test: /\.json$/,
      type: 'javascript/auto',
      loader: 'json-loader',
    },
    {
      test: /\.pegjs$/,
      loader: 'pegjs-loader',
    },
    {
      test: /\.scss$/,
      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            importLoaders: 1,
          },
        },
        {
          loader: 'postcss-loader',
          options: {
            postcssOptions: {
              ident: 'postcss',
              plugins: () => [
                require('postcss-flexbugs-fixes'),
                require('autoprefixer')({
                  flexbox: 'no-2009',
                }),
              ],
            },
          },
        },
        {
          loader: 'sass-loader',
          options: {
            sassOptions: {
              outputStyle: 'expanded',
            },
          },
        },
      ],
    },
    {
      test: /\.css$/,
      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            importLoaders: 1,
          },
        },
        {
          loader: 'postcss-loader',
          options: {
            postcssOptions: {
              ident: 'postcss',
              plugins: () => [
                require('postcss-flexbugs-fixes'),
                require('autoprefixer')({
                  flexbox: 'no-2009',
                }),
              ],
            },
          },
        },
      ],
    },
    {
      test: /\.jade$/,
      loader: 'pug-loader',
    },
    {
      test: /\.png$/,
      type: 'asset',
    },
    {
      test: /\.jpg$/,
      type: 'asset/resource',
    },
    {
      test: /\.(?:mp3|mp4|ogg|m4a)$/,
      type: 'asset/resource',
    },
    {
      test: /\.(otf|eot|svg|ttf|woff|woff2)(?:$|\?)/,
      type: 'asset',
    },
    {
      test: /\.(?:md)$/,
      type: 'asset/source',
    },
    {
      resourceQuery: /raw/,
      type: 'asset/source',
    },
  ]
}

function applyWebConfig(config) {
  Object.assign(config, {
    entry: {
      boot: ['./boot'],
    },
    output: {
      publicPath: '/',
      globalObject: 'this',
      filename: 'build/[name].js',
      assetModuleFilename: 'build/assets/[name]-[hash][ext][query]',
      chunkFilename: 'build/[name]-[chunkhash].js',
      devtoolModuleFilenameTemplate: 'file://[absolute-resource-path]',
      devtoolFallbackModuleFilenameTemplate:
        'file://[absolute-resource-path]?[contenthash]',
    },
  })

  config.plugins.push(
    new ServiceWorkerPlugin({
      enableWorkboxLogging: true,
      registration: {
        entry: 'boot',
      },
      workbox: {
        manifestTransforms: [
          (manifest) => {
            // manifest.push({ url: '/', revision: null })
            return { manifest }
          },
        ],
        // navigateFallback: '/',
        // navigateFallbackAllowlist: [/^\/(?:\?.*)?$/],
        runtimeCaching: [
          // Cache all .bemuse files.
          // These files are hashed, so the CacheFirst strategy is safe.
          {
            urlPattern: /^.*\.bemuse$/,
            handler: 'CacheFirst',
            options: {
              cacheName: 'bemuse-song-assets',
              cacheableResponse: { statuses: [200] },
            },
          },
          // Cache chart files.
          // These files may be updated, so we use the NetworkFirst strategy.
          {
            urlPattern: /^.*\.(bms|bme|bml|bmson)$/,
            handler: 'NetworkFirst',
            options: {
              cacheName: 'bemuse-song-charts',
              cacheableResponse: { statuses: [200] },
            },
          },
          // Cache music server files.
          // These files may be updated, so we use the NetworkFirst strategy.
          {
            urlPattern: /^.*\/index\.json$/,
            handler: 'NetworkFirst',
            options: {
              cacheName: 'bemuse-servers',
              cacheableResponse: { statuses: [200] },
            },
          },
          // Cache asset metadata files.
          // These files may be updated, so we use the NetworkFirst strategy.
          {
            urlPattern: /^.*\/metadata\.json$/,
            handler: 'NetworkFirst',
            options: {
              cacheName: 'bemuse-song-assets',
              cacheableResponse: { statuses: [200] },
            },
          },
          // Cache skin files.
          // These files do not change frequently, so we use the StaleWhileRevalidate strategy.
          {
            urlPattern: /^\/skins\//,
            handler: 'StaleWhileRevalidate',
            options: {
              cacheName: 'bemuse-skin',
              cacheableResponse: { statuses: [200] },
            },
          },
          // Cache resource files.
          // These files do not change frequently, so we use the StaleWhileRevalidate strategy.
          {
            urlPattern: /^\/res\//,
            handler: 'StaleWhileRevalidate',
            options: {
              cacheName: 'bemuse-res',
              cacheableResponse: { statuses: [200] },
            },
          },
        ],
        skipWaiting: true,
        clientsClaim: true,
      },
    })
  )

  if (!Env.hotModeEnabled()) {
    config.plugins.push(
      new BundleAnalyzerPlugin({
        analyzerMode: 'static',
        reportFilename: 'build/report/index.html',
        generateStatsFile: true,
        statsFilename: 'build/report/stats.json',
        openAnalyzer: false,
      })
    )
  }

  if (Env.hotModeEnabled()) {
    config.entry.boot.unshift(
      'webpack-dev-server/client?http://' +
        Env.serverHost() +
        ':' +
        Env.serverPort(),
      'webpack/hot/only-dev-server'
    )
  }

  return config
}

function applyKarmaConfig(config) {
  config.devtool = 'inline-cheap-source-map'
  return config
}

export const generateWebConfig = flowRight(applyWebConfig, generateBaseConfig)

export const generateKarmaConfig = flowRight(
  applyKarmaConfig,
  generateBaseConfig
)

export default generateWebConfig()

function CompileProgressPlugin() {
  const gauge = new Gauge()
  return new webpack.ProgressPlugin(function (percentage, message) {
    if (percentage === 1) gauge.hide()
    else gauge.show(message, percentage)
  })
}