fusion-cli
Version:
1,049 lines (1,004 loc) • 36.8 kB
JavaScript
/** Copyright (c) 2018 Uber Technologies, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
/* eslint-env node */
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');
const ChunkIdPrefixPlugin = require('./plugins/chunk-id-prefix-plugin.js');
const resolveFrom = require('../lib/resolve-from.js');
const isEsModule = require('../lib/is-es-module.js');
const LoaderContextProviderPlugin = require('./plugins/loader-context-provider-plugin.js');
const BuildTimingPlugin = require('./plugins/build-timing-plugin.js');
const ChildCompilationPlugin = require('./plugins/child-compilation-plugin.js');
const NodeSourcePlugin = require('./plugins/node-source-plugin.js');
const MergeChunksPlugin = require('./plugins/merge-chunks-plugin.js');
const {
chunkIdsLoader,
fileLoader,
svgoLoader,
babelLoader,
i18nManifestLoader,
chunkManifestLoader,
chunkUrlMapLoader,
syncChunkIdsLoader,
syncChunkPathsLoader,
swLoader,
workerLoader,
} = require('./loaders/index.js');
const {
translationsManifestContextKey,
clientChunkMetadataContextKey,
devContextKey,
workerKey,
} = require('./loaders/loader-context.js');
const ClientChunkMetadataStateHydratorPlugin = require('./plugins/client-chunk-metadata-state-hydrator-plugin.js');
const InstrumentedImportDependencyTemplatePlugin = require('./plugins/instrumented-import-dependency-template-plugin');
const I18nDiscoveryPlugin = require('./plugins/i18n-discovery-plugin.js');
const {version: fusionCLIVersion} = require('../package.json');
const {JS_EXT_PATTERN} = require('./constants/paths.js');
/*::
type Runtime = "server" | "client" | "sw";
*/
const COMPILATIONS /*: {[string]: Runtime} */ = {
server: 'server',
serverless: 'server',
'client-modern': 'client',
sw: 'sw',
};
const EXCLUDE_TRANSPILATION_PATTERNS = [
/node_modules\/mapbox-gl\//,
/node_modules\/react-dom\//,
/node_modules\/react\//,
/node_modules\/core-js\//,
];
/*::
import type {
ClientChunkMetadataState,
TranslationsManifest,
TranslationsManifestState,
LegacyBuildEnabledState,
} from "./types.js";
import type {
BuildStats,
FusionRC
} from "./load-fusionrc.js";
export type WebpackConfigOpts = {|
analyze?: 'client' | 'server',
id: $Keys<typeof COMPILATIONS>,
dir: string,
dev: boolean,
hmr: boolean,
serverHmr: boolean,
watch: boolean,
preserveNames: boolean,
zopfli: boolean,
gzip: boolean,
brotli: boolean,
minify: boolean,
skipSourceMaps: boolean,
state: {
clientChunkMetadata: ClientChunkMetadataState,
legacyClientChunkMetadata: ClientChunkMetadataState,
mergedClientChunkMetadata: ClientChunkMetadataState,
i18nManifest: TranslationsManifest,
i18nDeferredManifest: TranslationsManifestState,
legacyBuildEnabled: LegacyBuildEnabledState,
},
fusionConfig: FusionRC,
legacyPkgConfig?: {
node?: Object
},
worker: Object,
onBuildEnd?: $PropertyType<FusionRC, 'onBuildEnd'>,
command?: 'dev' | 'build',
isBuildCacheEnabled: boolean,
isEsbuildMinifierEnabled: boolean,
unsafeCache?: boolean,
|};
type JsonValue = boolean | number | string | null | void | $Shape<{ [string]: JsonValue }> | $ReadOnlyArray<JsonValue>;
type SerializableConfigOpts = {
[string]: JsonValue
};
*/
const isProjectCode = (modulePath /*:string*/, dir /*:string*/) =>
modulePath.startsWith(getSrcPath(dir)) ||
/fusion-cli(\/|\\)(entries|plugins)/.test(modulePath);
const getTransformDefault = (modulePath /*:string*/, dir /*:string*/) =>
isProjectCode(modulePath, dir) ? 'all' : 'spec';
module.exports = {
getWebpackConfig,
getTransformDefault,
};
const WEBPACK_NODE_OPTIONS = new Set(['__filename', '__dirname', 'global']);
function getWebpackConfig(opts /*: WebpackConfigOpts */) {
const {
analyze,
id,
dev,
dir,
hmr,
serverHmr,
watch,
state,
fusionConfig,
zopfli, // TODO: Remove redundant zopfli option
gzip,
brotli,
minify,
skipSourceMaps,
legacyPkgConfig = {},
worker,
onBuildEnd,
command,
preserveNames,
isBuildCacheEnabled,
isEsbuildMinifierEnabled,
unsafeCache,
// ACHTUNG:
// Adding new config option? Please do not forget to add it to `cacheVersionVars`
} = opts;
const mainJs = 'src/main.js';
const mainTs = 'src/main.ts';
const mainTsx = 'src/main.tsx';
let main;
let isTypeScriptProject = false;
if (fs.existsSync(path.join(dir, mainJs))) {
main = mainJs;
} else if (fs.existsSync(path.join(dir, mainTs))) {
main = mainTs;
isTypeScriptProject = true;
} else if (fs.existsSync(path.join(dir, mainTsx))) {
main = mainTsx;
isTypeScriptProject = true;
} else {
throw new Error(
`Project directory must contain either one of these files: ${[
mainJs,
mainTs,
mainTsx,
].join(', ')}`
);
}
const runtime = COMPILATIONS[id];
const isAnalyzerEnabled = analyze === runtime;
const mode = dev ? 'development' : 'production';
const env = dev ? 'development' : 'production';
const shouldMinify = !dev && minify;
const isHmrEnabled =
dev &&
hmr &&
watch &&
// Disable client HMR when running in analyze mode,
// so hot-update chunks do not get in a way.
!isAnalyzerEnabled;
const isServerHmrEnabled = dev && serverHmr && watch;
const target = {server: 'node', client: 'web', sw: 'webworker'}[runtime];
const fusionBuildFolder = path.resolve(dir, '.fusion');
// 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,
};
const isAssumeNoImportSideEffectsEnabled =
fusionConfig.assumeNoImportSideEffects === true ||
Array.isArray(fusionConfig.assumeNoImportSideEffects);
const isDefaultImportSideEffectsEnabled = !(
fusionConfig.defaultImportSideEffects === false ||
Array.isArray(fusionConfig.defaultImportSideEffects)
);
const babelOverridesData = {
dev: dev,
fusionTransforms: true,
assumeNoImportSideEffects: isAssumeNoImportSideEffectsEnabled,
target: runtime === 'server' ? 'node-bundled' : 'browser-modern',
specOnly: false,
};
const legacyBabelOverridesData = {
...babelOverridesData,
target: runtime === 'server' ? 'node-bundled' : 'browser-legacy',
};
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;
const nodeBuiltins = Object.assign(
{
// Polyfilling process involves lots of cruft. Better to explicitly inline env value statically
process: false,
// We definitely don't want automatic Buffer polyfills. This should be explicit and in userland code
Buffer: false,
// This is required until we have better tree shaking. See https://github.com/fusionjs/fusion-cli/issues/254
child_process: 'empty',
cluster: 'empty',
crypto: 'empty',
dgram: 'empty',
dns: 'empty',
fs: 'empty',
http2: 'empty',
module: 'empty',
net: 'empty',
readline: 'empty',
repl: 'empty',
tls: 'empty',
},
legacyPkgConfig.node,
fusionConfig.nodeBuiltins
);
// Invalidate cache when any of these values change
const cacheVersionVars /*: SerializableConfigOpts */ = {
id,
dev,
dir,
hmr,
serverHmr,
watch,
zopfli,
gzip,
brotli,
minify,
skipSourceMaps,
preserveNames,
nodeBuiltins,
fusionCLIVersion,
main,
isTypeScriptProject,
};
const cacheDirectory = path.join(fusionBuildFolder, '.build-cache');
const isBuildCachePersistent =
isBuildCacheEnabled && !isEmptyDir(cacheDirectory);
const withHmr =
(isHmrEnabled && runtime === 'client') ||
(isServerHmrEnabled && runtime === 'server');
const externalsCache = new Map();
return {
...(isBuildCacheEnabled
? {
cache: {
type: 'filesystem',
cacheDirectory,
name: `${runtime}-${mode}${withHmr ? '-hmr' : ''}`,
version: JSON.stringify(cacheVersionVars),
buildDependencies: {
// Invalidate cache when any of these files, or any of their dependencies change
config: [
__filename,
// .fusionrc contains some non-serializable options (e.g. functions), so we need to
// tell webpack to track any changes to this file to invalidate the build cache
fusionConfig && fusionConfig.configPath,
].filter(Boolean),
},
},
}
: null),
experiments: {
cacheUnaffected: true,
},
snapshot: {
// It's common that developers modify code inside node_modules to debug
// some problem with their application or third party package. Hence we
// we are letting webpack check the file timestamps to invalidate cache,
// instead of relying on package's version defined in package.json file.
managedPaths: [],
},
watchOptions: {
// Note: Webpack recently changed the defaults to 20ms (was 200ms), which
// seems to be the reason why we see double compilation on every file save.
aggregateTimeout: 100,
// Ignore Yarn PnP immutable paths, as well as invalid virtual paths
// @see: https://github.com/yarnpkg/berry/blob/5869e5934dcd7491422c2045675bcea2944708cc/packages/yarnpkg-fslib/sources/VirtualFS.ts#L8-L14
ignored:
/(\/\.yarn(\/[^/]+)*\/cache\/[^/]+\.zip|\/\.yarn\/(?:\$\$virtual|__virtual__)(?!(\/[^/]+){2}\/.+$))/,
},
name: runtime,
target: target === 'node' ? 'node12.17' : target,
entry: {
main: [
runtime === 'client' &&
path.join(__dirname, '../entries/client-public-path.js'),
runtime === 'server' &&
path.join(__dirname, '../entries/server-public-path.js'),
isHmrEnabled &&
runtime === 'client' &&
`${require.resolve('webpack-hot-middleware/client')}?name=client`,
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,
/**
* `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-*` devtool can't be used with minimizers, as it doesn't include column mappings
// @see: https://github.com/webpack/webpack/issues/4176#issuecomment-762347256
shouldMinify
? 'source-map'
: 'cheap-module-source-map',
output: {
uniqueName: 'Fusion',
path: path.join(fusionBuildFolder, 'dist', env, runtime),
pathinfo: false,
filename:
runtime === 'server'
? 'server-main.js'
: dev
? 'client-[name].js'
: 'client-[name]-[chunkhash].js',
...(runtime === 'server'
? {
libraryTarget: 'commonjs2',
}
: null),
hashDigestLength: 16,
hashFunction: 'xxhash64',
// This is the recommended default.
// See https://webpack.js.org/configuration/output/#output-sourcemapfilename
sourceMapFilename: `[file].map`,
// NOTE: Breaking change in webpack v5
// Webpack 5 has new default automatic `publicPath`, which is not well supported in
// legacy browsers. Setting it to a static value makes it bypass this new behavior.
// Fusion.js will set __webpack_public_path__ at runtime based on ENV variables.
publicPath: '',
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: false,
context: dir,
node: Object.assign(
{
// We want these to resolve to the original file source location, not the compiled location
// in the future, we may want to consider using `import.meta`
__filename: true,
__dirname: true,
},
Object.entries(nodeBuiltins)
.filter(([key]) => WEBPACK_NODE_OPTIONS.has(key))
.reduce((acc, [key, val]) => {
acc[key] = val;
return acc;
}, {})
),
module: {
// NOTE: Breaking change in webpack v5
// Webpack v5 does not allow named imports from JSON-modules, that makes it
// hard to resolve for some apps dependent on packages with such imports.
// Hence need to relax this rule to produce a warning instead of an error
// @see: https://webpack.js.org/blog/2020-10-10-webpack-5-release/#json-modules
strictExportPresence: false,
rules: [
// NOTE: Breaking change in webpack v5
// Webpack aligns with the spec for ECMAScript module,
// where import paths need to be fully specified
// @see: https://github.com/webpack/webpack/issues/11467#issuecomment-691702706
{
test: /\.m?js/,
resolve: {
fullySpecified: false,
},
},
/**
* Global transforms (including ES2017+ transpilations)
*/
runtime === 'server' && {
compiler: (id) => id === 'server' || id === 'worker-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' || id === 'worker-client',
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' || id === 'worker-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,
},
/**
* Fusion-specific transforms (not applied to node_modules)
*/
overrides: [
{
...legacyBabelOverridesData,
},
],
},
},
],
},
// Need to disable auto-parsing JSON loaded via `assetUrl()` construct
{
test: /\.json$/,
resourceQuery: /assetUrl=true/,
type: 'javascript/auto',
},
{
test: /\.svg$/,
resourceQuery: /assetUrl=true/,
loader: svgoLoader.path,
},
{
test: /\.ya?ml$/,
type: 'json',
loader: require.resolve('yaml-loader'),
},
{
test: /\.graphql$|.gql$/,
loader: require.resolve('graphql-tag/loader'),
},
(isAssumeNoImportSideEffectsEnabled ||
!isDefaultImportSideEffectsEnabled) && {
sideEffects: false,
...(isAssumeNoImportSideEffectsEnabled
? null
: {
// `defaultImportSideEffects: false` only applies to imports to other packages from within
// application. Imports within other packages to other packages will not be affected.
issuer: dir,
}),
test: (modulePath) =>
// NOTE: Breaking change in webpack v5
// Need to skip modules generated via custom webpack loaders, for which there's no module resource set
// @see: https://github.com/webpack/webpack/blob/v4.46.0/lib/RuleSet.js#L487
Boolean(modulePath) &&
// `defaultImportSideEffects: false` does not apply to application code,
// in which case we defer to the `sideEffects` in app-root package.json
(isAssumeNoImportSideEffectsEnabled || modulePath.startsWith(dir)),
descriptionData: {
// We need to respect the value set in package.json whenever possible, hence
// only set module as sideEffects free if sideEffects field was not defined.
sideEffects: (val) => !val,
...(function () {
const ignoredPackages = new Set([
'core-js',
'regenerator-runtime',
...(isAssumeNoImportSideEffectsEnabled &&
Array.isArray(fusionConfig.assumeNoImportSideEffects)
? fusionConfig.assumeNoImportSideEffects
: []),
...(!isDefaultImportSideEffectsEnabled &&
Array.isArray(fusionConfig.defaultImportSideEffects)
? fusionConfig.defaultImportSideEffects
: []),
]);
return {
name: (packageName) => !ignoredPackages.has(packageName),
};
})(),
},
},
].filter(Boolean),
...(unsafeCache
? {
unsafeCache: true,
}
: null),
},
externals:
runtime === 'server'
? (
{context, request} /*: { context: string, request: string }*/,
callback /*: (error: ?Error, result?: string) => void */
) => {
const cacheKey = `${context}|${request}`;
if (externalsCache.has(cacheKey)) {
return callback(null, externalsCache.get(cacheKey));
}
function handleExternalResult(result) {
externalsCache.set(cacheKey, result);
callback(null, result);
}
function handleExternalModule(modulePath) {
const isEsm = isEsModule(modulePath);
if (isEsm) {
if (typeof process.versions.pnp !== 'undefined') {
// Yarn PnP does not support es modules yet,
// have to bundle this dependency on the server
// @see: https://github.com/yarnpkg/berry/issues/638
return handleExternalResult();
}
}
return handleExternalResult(
`${isEsm ? 'module' : 'commonjs'} ${modulePath}`
);
}
if (/^[@a-z\-0-9]+/.test(request)) {
const absolutePath = getModuleAbsolutePath(context, request);
if (!absolutePath) {
// bundle the dependency if absolute path could not be determined
// using node.js resolver at build time. This will ensure a build
// failure in case the dependency is missing, as opposed to making
// an assumption that the dependency will be available at runtime.
// And will also fallback to webpack for resolving, and building a
// module imported from a local workspace (monorepo support).
return handleExternalResult();
}
if (experimentalBundleTest) {
const bundle = experimentalBundleTest(
absolutePath,
'browser-only'
);
if (bundle === 'browser-only') {
// don't bundle on the server
return handleExternalModule(absolutePath);
} else if (bundle === 'universal') {
// bundle on the server
return handleExternalResult();
} else {
throw new Error(
`Unexpected value: ${bundle} from experimentalBundleTest. Expected 'browser-only' | 'universal'.`
);
}
}
// do not bundle external packages
return handleExternalModule(absolutePath);
}
// bundle everything else (local files, __*)
return handleExternalResult();
}
: undefined,
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',
}
: {}),
},
// NOTE: Breaking change in webpack v5
// Need to prioritize .mjs extension to keep similar behavior to
// webpack v4, also some packages lack fully specified path in esm
// @see: https://github.com/webpack/webpack/issues/11467#issuecomment-691702706
extensions: ['.mjs', '.ts', '.tsx', '...'],
},
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,
[chunkManifestLoader.alias]: chunkManifestLoader.path,
[chunkUrlMapLoader.alias]: chunkUrlMapLoader.path,
[i18nManifestLoader.alias]: i18nManifestLoader.path,
[swLoader.alias]: swLoader.path,
[workerLoader.alias]: workerLoader.path,
},
},
plugins: [
isAnalyzerEnabled &&
new BundleAnalyzerPlugin({
analyzerHost: 'localhost',
analyzerPort: 'auto',
}),
runtime === 'client' &&
!dev &&
!skipSourceMaps &&
((compiler) => {
const SourceMapPlugin = require('./plugins/source-map-plugin.js');
new SourceMapPlugin().apply(compiler);
}),
// NOTE: Breaking change in webpack v5
// Need to provide same API to source node modules in client bundles
// @see: https://github.com/webpack/webpack/blob/v4.46.0/lib/node/NodeSourcePlugin.js#L83-L94
target !== 'node' &&
new webpack.ProvidePlugin({
// $FlowFixMe
...(nodeBuiltins.Buffer
? {
Buffer: ['buffer', 'Buffer'],
}
: null),
...(nodeBuiltins.process
? {
process: 'process',
}
: null),
}),
// NOTE: Breaking change in webpack v5
// Need to provide same API to source node modules in client bundles
// @see: https://github.com/webpack/webpack/blob/v4.46.0/lib/node/NodeSourcePlugin.js#L21-L36
target !== 'node' && new NodeSourcePlugin(nodeBuiltins),
runtime === 'server' && new MergeChunksPlugin(),
new ProgressBarPlugin(),
onBuildEnd &&
new BuildTimingPlugin(({buildTime, isIncrementalBuild}) => {
onBuildEnd({
command,
target: id,
mode,
path: dir,
watch,
minify: shouldMinify,
skipSourceMaps,
buildTime,
isIncrementalBuild,
isBuildCacheEnabled,
isBuildCachePersistent,
isLegacyBuildEnabled: state.legacyBuildEnabled.value,
version: fusionCLIVersion,
buildToolVersion: 'webpack v5',
});
}),
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 &&
((compiler) => {
const CompressionPlugin = require('compression-webpack-plugin');
new CompressionPlugin({
filename: '[file].gz',
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
// There's no need to compress server bundle
exclude: 'server-main.js',
threshold: 0,
minRatio: 1,
}).apply(compiler);
}),
!dev &&
brotli &&
((compiler) => {
const CompressionPlugin = require('compression-webpack-plugin');
new CompressionPlugin({
filename: '[file].br',
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
// There's no need to compress server bundle
exclude: 'server-main.js',
threshold: 0,
minRatio: 1,
}).apply(compiler);
}),
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,
}),
withHmr && new webpack.HotModuleReplacementPlugin(),
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: state.legacyBuildEnabled,
outputOptions: {
// Can not change target properties for child compiler
// Need to downgrade the environment options manually
// @see: https://webpack.js.org/configuration/output/#outputenvironment
environment: {
arrowFunction: false,
bigIntLiteral: false,
const: false,
destructuring: false,
dynamicImport: false,
forOf: false,
module: false,
},
filename: dev
? 'client-legacy-[name].js'
: 'client-legacy-[name]-[chunkhash].js',
chunkFilename: dev
? 'client-legacy-[name].js'
: 'client-legacy-[name]-[chunkhash].js',
},
plugins: (options) =>
[
options.optimization.runtimeChunk &&
new webpack.optimize.RuntimeChunkPlugin(
options.optimization.runtimeChunk
),
options.optimization.splitChunks &&
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('legacy'),
shouldMinify &&
isEsbuildMinifierEnabled && {
apply: (compiler) => {
const EsbuildMinifyPlugin = require('./plugins/esbuild-minify-plugin.js');
new EsbuildMinifyPlugin({
transformOptions: {
target: 'es5',
keepNames: preserveNames,
},
}).apply(compiler);
},
},
].filter(Boolean),
}),
].filter(Boolean),
optimization: {
// 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
? {
emitOnErrors: false,
}
: null),
runtimeChunk: runtime === 'client' && {name: 'runtime'},
splitChunks:
runtime !== 'client'
? false
: 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:
// Always split node_modules in a separate chunk while in dev mode
dev && !isAnalyzerEnabled
? false
: {
minChunks: 2,
reuseExistingChunk: true,
},
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'initial',
enforce: true,
},
},
},
minimize: shouldMinify,
minimizer: shouldMinify
? [
isEsbuildMinifierEnabled
? (compiler /*: any */) => {
const EsbuildMinifyPlugin = require('./plugins/esbuild-minify-plugin.js');
new EsbuildMinifyPlugin({
transformOptions: {
// At this point everything should be transpiled by babel, so using
// higher target w/ esbuild to prevent any additional transpilations
// (e.g. rest/spread operators; async functions)
target: 'es2018',
keepNames: preserveNames,
},
}).apply(compiler);
}
: (compiler /*: any */) => {
const TerserPlugin = require('terser-webpack-plugin');
new TerserPlugin({
extractComments: false,
terserOptions: {
parse: {
ecma: 2017,
},
compress: {
ecma: 5,
// 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,
},
format: {
ecma: 5,
},
keep_fnames: preserveNames,
keep_classnames: preserveNames,
},
}).apply(compiler);
},
]
: [],
},
};
}
// Allow overrides with a warning for `dev` command. In production builds, throw if NODE_ENV is not `production`.
function getEnvBanner(env) {
return `
if (process.env.NODE_ENV && process.env.NODE_ENV !== '${env}') {
if (${env === 'production' ? 'true' : 'false'}) {
throw new Error(\`NODE_ENV (\${process.env.NODE_ENV}) does not match value for compiled assets: ${env}\`);
} else {
console.warn('Overriding NODE_ENV: ' + process.env.NODE_ENV + ' to ${env} in order to match value for compiled assets');
process.env.NODE_ENV = '${env}';
}
} else {
process.env.NODE_ENV = '${env}';
}
`;
}
function getModuleAbsolutePath(context, request) {
return resolveFrom.silent(context, request);
}
const srcPathCache /*: Map<string, string>*/ = new Map();
function getSrcPath(dir) /*: string*/ {
if (srcPathCache.has(dir)) {
// $FlowFixMe
return srcPathCache.get(dir);
}
let srcPath;
// resolving to the real path of a known top-level file is required to support Bazel, which symlinks source files individually
if (process.env.NODE_PRESERVE_SYMLINKS) {
srcPath = path.resolve(dir, 'src');
} else {
try {
const real = path.dirname(
fs.realpathSync(path.resolve(dir, 'package.json'))
);
srcPath = path.resolve(real, 'src');
} catch (e) {
srcPath = path.resolve(dir, 'src');
}
}
srcPathCache.set(dir, srcPath);
return srcPath;
}
function isEmptyDir(dir) {
try {
return fs.readdirSync(dir).length === 0;
} catch (e) {
return true;
}
}