function getWebpackConfig()

in fusion-cli/build/get-webpack-config.js [119:639]


function getWebpackConfig(opts /*: WebpackConfigOpts */) {
  const {
    id,
    dev,
    dir,
    hmr,
    watch,
    state,
    fusionConfig,
    zopfli, // TODO: Remove redundant zopfli option
    gzip,
    brotli,
    minify,
    skipSourceMaps,
    legacyPkgConfig = {},
    worker,
  } = opts;
  const main = 'src/main.js';

  if (!fs.existsSync(path.join(dir, main))) {
    throw new Error(`Project directory must contain a ${main} file`);
  }

  const runtime = COMPILATIONS[id];
  const env = dev ? 'development' : 'production';
  const shouldMinify = !dev && minify;

  // Both options default to true, but if `--zopfli=false`
  // it should be respected for backwards compatibility
  const shouldGzip = zopfli && gzip;

  const babelConfigData = {
    target: runtime === 'server' ? 'node-bundled' : 'browser-modern',
    specOnly: true,
    plugins:
      fusionConfig.babel && fusionConfig.babel.plugins
        ? fusionConfig.babel.plugins
        : [],
    presets:
      fusionConfig.babel && fusionConfig.babel.presets
        ? fusionConfig.babel.presets
        : [],
  };

  const babelOverridesData = {
    dev: dev,
    fusionTransforms: true,
    assumeNoImportSideEffects: fusionConfig.assumeNoImportSideEffects,
    target: runtime === 'server' ? 'node-bundled' : 'browser-modern',
    specOnly: false,
  };

  const legacyBabelOverridesData = {
    dev: dev,
    fusionTransforms: true,
    assumeNoImportSideEffects: fusionConfig.assumeNoImportSideEffects,
    target: runtime === 'server' ? 'node-bundled' : 'browser-legacy',
    specOnly: false,
  };

  const {experimentalBundleTest, experimentalTransformTest} = fusionConfig;
  const babelTester = experimentalTransformTest
    ? modulePath => {
        if (!JS_EXT_PATTERN.test(modulePath)) {
          return false;
        }
        const transform = experimentalTransformTest(
          modulePath,
          getTransformDefault(modulePath, dir)
        );
        if (transform === 'none') {
          return false;
        } else if (transform === 'all' || transform === 'spec') {
          return true;
        } else {
          throw new Error(
            `Unexpected value from experimentalTransformTest ${transform}. Expected 'spec' | 'all' | 'none'`
          );
        }
      }
    : JS_EXT_PATTERN;

  return {
    name: runtime,
    target: {server: 'node', client: 'web', sw: 'webworker'}[runtime],
    entry: {
      main: [
        runtime === 'client' &&
          path.join(__dirname, '../entries/client-public-path.js'),
        runtime === 'server' &&
          path.join(__dirname, '../entries/server-public-path.js'),
        dev &&
          hmr &&
          watch &&
          runtime !== 'server' &&
          `${require.resolve('webpack-hot-middleware/client')}?name=client`,
        // TODO(#46): use 'webpack/hot/signal' instead
        dev &&
          hmr &&
          watch &&
          runtime === 'server' &&
          `${require.resolve('webpack/hot/poll')}?1000`,
        runtime === 'server' &&
          path.join(__dirname, `../entries/${id}-entry.js`), // server-entry or serverless-entry
        runtime === 'client' &&
          path.join(__dirname, '../entries/client-entry.js'),
      ].filter(Boolean),
    },
    mode: dev ? 'development' : 'production',
    // TODO(#47): Do we need to do something different here for production?
    stats: 'minimal',
    /**
     * `cheap-module-source-map` is best supported by Chrome DevTools
     * See: https://github.com/webpack/webpack/issues/2145#issuecomment-294361203
     *
     * We use `source-map` in production but effectively create a
     * `hidden-source-map` using SourceMapPlugin to strip the comment.
     *
     * Chrome DevTools support doesn't matter in these case.
     * We only use it for generating nice stack traces
     */
    // TODO(#6): what about node v8 inspector?
    devtool: skipSourceMaps
      ? false
      : runtime === 'client' && !dev
      ? 'source-map'
      : runtime === 'sw'
      ? 'hidden-source-map'
      : 'cheap-module-source-map',
    output: {
      path: path.join(dir, `.fusion/dist/${env}/${runtime}`),
      filename:
        runtime === 'server'
          ? 'server-main.js'
          : dev
          ? 'client-[name].js'
          : 'client-[name]-[chunkhash].js',
      libraryTarget: runtime === 'server' ? 'commonjs2' : 'var',
      // This is the recommended default.
      // See https://webpack.js.org/configuration/output/#output-sourcemapfilename
      sourceMapFilename: `[file].map`,
      // We will set __webpack_public_path__ at runtime, so this should be set to undefined
      publicPath: void 0,
      crossOriginLoading: 'anonymous',
      devtoolModuleFilenameTemplate: (info /*: Object */) => {
        // always return absolute paths in order to get sensible source map explorer visualization
        return path.isAbsolute(info.absoluteResourcePath)
          ? info.absoluteResourcePath
          : path.join(dir, info.absoluteResourcePath);
      },
    },
    performance: {
      hints: false,
    },
    context: dir,
    node: Object.assign(
      getNodeConfig(runtime),
      legacyPkgConfig.node,
      fusionConfig.nodeBuiltins
    ),
    module: {
      /**
       * Compile-time error for importing a non-existent export
       * https://github.com/facebookincubator/create-react-app/issues/1559
       */
      strictExportPresence: true,
      rules: [
        /**
         * Global transforms (including ES2017+ transpilations)
         */
        runtime === 'server' && {
          compiler: id => id === 'server',
          test: babelTester,
          exclude: EXCLUDE_TRANSPILATION_PATTERNS,
          use: [
            {
              loader: babelLoader.path,
              options: {
                dir,
                configCacheKey: 'server-config',
                overrideCacheKey: 'server-override',
                babelConfigData: {...babelConfigData},
                /**
                 * Fusion-specific transforms (not applied to node_modules)
                 */
                overrides: [
                  {
                    ...babelOverridesData,
                  },
                ],
              },
            },
          ],
        },
        /**
         * Global transforms (including ES2017+ transpilations)
         */
        (runtime === 'client' || runtime === 'sw') && {
          compiler: id => id === 'client' || id === 'sw',
          test: babelTester,
          exclude: EXCLUDE_TRANSPILATION_PATTERNS,
          use: [
            {
              loader: babelLoader.path,
              options: {
                dir,
                configCacheKey: 'client-config',
                overrideCacheKey: 'client-override',
                babelConfigData: {...babelConfigData},
                /**
                 * Fusion-specific transforms (not applied to node_modules)
                 */
                overrides: [
                  {
                    ...babelOverridesData,
                  },
                ],
              },
            },
          ],
        },
        /**
         * Global transforms (including ES2017+ transpilations)
         */
        runtime === 'client' && {
          compiler: id => id === 'client-legacy',
          test: babelTester,
          exclude: EXCLUDE_TRANSPILATION_PATTERNS,
          use: [
            {
              loader: babelLoader.path,
              options: {
                dir,
                configCacheKey: 'legacy-config',
                overrideCacheKey: 'legacy-override',
                babelConfigData: {
                  target:
                    runtime === 'server' ? 'node-bundled' : 'browser-legacy',
                  specOnly: true,
                  plugins:
                    fusionConfig.babel && fusionConfig.babel.plugins
                      ? fusionConfig.babel.plugins
                      : [],
                  presets:
                    fusionConfig.babel && fusionConfig.babel.presets
                      ? fusionConfig.babel.presets
                      : [],
                },
                /**
                 * Fusion-specific transforms (not applied to node_modules)
                 */
                overrides: [
                  {
                    ...legacyBabelOverridesData,
                  },
                ],
              },
            },
          ],
        },
        {
          test: /\.json$/,
          type: 'javascript/auto',
          loader: require.resolve('./loaders/json-loader.js'),
        },
        {
          test: /\.ya?ml$/,
          type: 'json',
          loader: require.resolve('yaml-loader'),
        },
        {
          test: /\.graphql$|.gql$/,
          loader: require.resolve('graphql-tag/loader'),
        },
        fusionConfig.assumeNoImportSideEffects && {
          sideEffects: false,
          test: modulePath => {
            if (
              modulePath.includes('core-js/modules') ||
              modulePath.includes('regenerator-runtime/runtime')
            ) {
              return false;
            }

            return true;
          },
        },
      ].filter(Boolean),
    },
    externals: [
      runtime === 'server' &&
        ((context, request, callback) => {
          if (/^[@a-z\-0-9]+/.test(request)) {
            const absolutePath = resolveFrom.silent(context, request);
            // do not bundle external packages and those not whitelisted
            if (typeof absolutePath !== 'string') {
              // if module is missing, skip rewriting to absolute path
              return callback(null, request);
            }
            if (experimentalBundleTest) {
              const bundle = experimentalBundleTest(
                absolutePath,
                'browser-only'
              );
              if (bundle === 'browser-only') {
                // don't bundle on the server
                return callback(null, 'commonjs ' + absolutePath);
              } else if (bundle === 'universal') {
                // bundle on the server
                return callback();
              } else {
                throw new Error(
                  `Unexpected value: ${bundle} from experimentalBundleTest. Expected 'browser-only' | 'universal'.`
                );
              }
            }
            return callback(null, 'commonjs ' + absolutePath);
          }
          // bundle everything else (local files, __*)
          return callback();
        }),
    ].filter(Boolean),
    resolve: {
      symlinks: process.env.NODE_PRESERVE_SYMLINKS ? false : true,
      aliasFields: [
        (runtime === 'client' || runtime === 'sw') && 'browser',
        'es2015',
        'es2017',
      ].filter(Boolean),
      alias: {
        // we replace need to set the path to user application at build-time
        __FUSION_ENTRY_PATH__: path.join(dir, main),
        __ENV__: env,
        ...(process.env.ENABLE_REACT_PROFILER === 'true'
          ? {
              'react-dom$': 'react-dom/profiling',
              'scheduler/tracing': 'scheduler/tracing-profiling',
            }
          : {}),
      },
      plugins: [PnpWebpackPlugin],
    },
    resolveLoader: {
      symlinks: process.env.NODE_PRESERVE_SYMLINKS ? false : true,
      alias: {
        [fileLoader.alias]: fileLoader.path,
        [chunkIdsLoader.alias]: chunkIdsLoader.path,
        [syncChunkIdsLoader.alias]: syncChunkIdsLoader.path,
        [syncChunkPathsLoader.alias]: syncChunkPathsLoader.path,
        [chunkUrlMapLoader.alias]: chunkUrlMapLoader.path,
        [i18nManifestLoader.alias]: i18nManifestLoader.path,
        [swLoader.alias]: swLoader.path,
        [workerLoader.alias]: workerLoader.path,
      },
      plugins: [PnpWebpackPlugin.moduleLoader(module)],
    },

    plugins: [
      runtime === 'client' && !dev && new SourceMapPlugin(),
      runtime === 'client' &&
        new webpack.optimize.RuntimeChunkPlugin({
          name: 'runtime',
        }),
      (fusionConfig.defaultImportSideEffects === false ||
        Array.isArray(fusionConfig.defaultImportSideEffects)) &&
        new DefaultNoImportSideEffectsPlugin(
          Array.isArray(fusionConfig.defaultImportSideEffects)
            ? {ignoredPackages: fusionConfig.defaultImportSideEffects}
            : {}
        ),
      new webpack.optimize.SideEffectsFlagPlugin(),
      runtime === 'server' &&
        new webpack.optimize.LimitChunkCountPlugin({maxChunks: 1}),
      new ProgressBarPlugin(),
      runtime === 'server' &&
        new LoaderContextProviderPlugin('optsContext', opts),
      new LoaderContextProviderPlugin(devContextKey, dev),
      runtime === 'server' &&
        new LoaderContextProviderPlugin(
          clientChunkMetadataContextKey,
          state.mergedClientChunkMetadata
        ),
      runtime === 'client'
        ? new I18nDiscoveryPlugin(
            state.i18nDeferredManifest,
            state.i18nManifest
          )
        : new LoaderContextProviderPlugin(
            translationsManifestContextKey,
            state.i18nDeferredManifest
          ),
      new LoaderContextProviderPlugin(workerKey, worker),
      !dev && shouldGzip && gzipWebpackPlugin,
      !dev && brotli && brotliWebpackPlugin,
      !dev && svgoWebpackPlugin,
      // In development, skip the emitting phase on errors to ensure there are
      // no assets emitted that include errors. This fixes an issue with hot reloading
      // server side code and recovering from errors correctly. We only want to do this
      // in dev because the CLI will not exit with an error code if the option is enabled,
      // so failed builds would look like successful ones.
      watch && new webpack.NoEmitOnErrorsPlugin(),
      runtime === 'server'
        ? // Server
          new InstrumentedImportDependencyTemplatePlugin({
            compilation: 'server',
            clientChunkMetadata: state.mergedClientChunkMetadata,
          })
        : /**
           * Client
           * Don't wait for the client manifest on the client.
           * The underlying plugin is able determine client chunk metadata on its own.
           */
          new InstrumentedImportDependencyTemplatePlugin({
            compilation: 'client',
            i18nManifest: state.i18nManifest,
          }),
      dev && hmr && watch && new webpack.HotModuleReplacementPlugin(),
      !dev && runtime === 'client' && new webpack.HashedModuleIdsPlugin(),
      runtime === 'client' &&
        // case-insensitive paths can cause problems
        new CaseSensitivePathsPlugin(),
      runtime === 'server' &&
        new webpack.BannerPlugin({
          raw: true,
          entryOnly: true,
          // Enforce NODE_ENV at runtime
          banner: getEnvBanner(env),
        }),
      new webpack.EnvironmentPlugin({NODE_ENV: env}),
      id === 'client-modern' &&
        new ClientChunkMetadataStateHydratorPlugin(state.clientChunkMetadata),
      id === 'client-modern' &&
        new ChildCompilationPlugin({
          name: 'client-legacy',
          entry: [
            path.resolve(__dirname, '../entries/client-public-path.js'),
            path.resolve(__dirname, '../entries/client-entry.js'),
            // EVENTUALLY HAVE HMR
          ],
          enabledState: opts.state.legacyBuildEnabled,
          outputOptions: {
            filename: opts.dev
              ? 'client-legacy-[name].js'
              : 'client-legacy-[name]-[chunkhash].js',
            chunkFilename: opts.dev
              ? 'client-legacy-[name].js'
              : 'client-legacy-[name]-[chunkhash].js',
          },
          plugins: options => [
            new webpack.optimize.RuntimeChunkPlugin(
              options.optimization.runtimeChunk
            ),
            new webpack.optimize.SplitChunksPlugin(
              options.optimization.splitChunks
            ),
            // need to re-apply template
            new InstrumentedImportDependencyTemplatePlugin({
              compilation: 'client',
              i18nManifest: state.i18nManifest,
            }),
            new ClientChunkMetadataStateHydratorPlugin(
              state.legacyClientChunkMetadata
            ),
            new ChunkIdPrefixPlugin(),
          ],
        }),
    ].filter(Boolean),
    optimization: {
      runtimeChunk: runtime === 'client' && {name: 'runtime'},
      splitChunks:
        runtime !== 'client'
          ? void 0
          : fusionConfig.splitChunks
          ? // Tilde character in filenames is not well supported
            // https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html
            {...fusionConfig.splitChunks, automaticNameDelimiter: '-'}
          : {
              chunks: 'async',
              automaticNameDelimiter: '-',
              cacheGroups: {
                default: {
                  minChunks: 2,
                  reuseExistingChunk: true,
                },
                vendor: {
                  test: /[\\/]node_modules[\\/]/,
                  name: 'vendor',
                  chunks: 'initial',
                  enforce: true,
                },
              },
            },
      minimize: shouldMinify,
      minimizer: shouldMinify
        ? [
            new TerserPlugin({
              sourceMap: skipSourceMaps ? false : true, // default from webpack (see https://github.com/webpack/webpack/blob/aab3554cad2ebc5d5e9645e74fb61842e266da34/lib/WebpackOptionsDefaulter.js#L290-L297)
              cache: true, // default from webpack
              parallel: true, // default from webpack
              extractComments: false,
              terserOptions: {
                compress: {
                  // typeofs: true (default) transforms typeof foo == "undefined" into foo === void 0.
                  // This mangles mapbox-gl creating an error when used alongside with window global mangling:
                  // https://github.com/webpack-contrib/uglifyjs-webpack-plugin/issues/189
                  typeofs: false,

                  // inline=2 can cause const reassignment
                  // https://github.com/mishoo/UglifyJS2/issues/2842
                  inline: 1,
                },

                keep_fnames: opts.preserveNames,
                keep_classnames: opts.preserveNames,
              },
            }),
          ]
        : undefined,
    },
  };
}