UNPKG

23.8 kBJavaScriptView Raw
1/** Copyright (c) 2018 Uber Technologies, Inc.
2 *
3 * This source code is licensed under the MIT license found in the
4 * LICENSE file in the root directory of this source tree.
5 *
6 * @flow
7 */
8
9/* eslint-env node */
10
11const fs = require('fs');
12const path = require('path');
13
14const webpack = require('webpack');
15const TerserPlugin = require('terser-webpack-plugin');
16const PnpWebpackPlugin = require('pnp-webpack-plugin');
17const ProgressBarPlugin = require('progress-bar-webpack-plugin');
18const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
19const DefaultNoImportSideEffectsPlugin = require('default-no-import-side-effects-webpack-plugin');
20const ChunkIdPrefixPlugin = require('./plugins/chunk-id-prefix-plugin.js');
21const {
22 gzipWebpackPlugin,
23 brotliWebpackPlugin,
24 svgoWebpackPlugin,
25} = require('../lib/compression');
26const resolveFrom = require('../lib/resolve-from');
27const LoaderContextProviderPlugin = require('./plugins/loader-context-provider-plugin.js');
28const ChildCompilationPlugin = require('./plugins/child-compilation-plugin.js');
29const {
30 chunkIdsLoader,
31 fileLoader,
32 babelLoader,
33 i18nManifestLoader,
34 chunkUrlMapLoader,
35 syncChunkIdsLoader,
36 syncChunkPathsLoader,
37 swLoader,
38 workerLoader,
39} = require('./loaders/index.js');
40const {
41 translationsManifestContextKey,
42 clientChunkMetadataContextKey,
43 devContextKey,
44 workerKey,
45} = require('./loaders/loader-context.js');
46const ClientChunkMetadataStateHydratorPlugin = require('./plugins/client-chunk-metadata-state-hydrator-plugin.js');
47const InstrumentedImportDependencyTemplatePlugin = require('./plugins/instrumented-import-dependency-template-plugin');
48const I18nDiscoveryPlugin = require('./plugins/i18n-discovery-plugin.js');
49const SourceMapPlugin = require('./plugins/source-map-plugin.js');
50
51/*::
52type Runtime = "server" | "client" | "sw";
53*/
54
55const COMPILATIONS /*: {[string]: Runtime} */ = {
56 server: 'server',
57 serverless: 'server',
58 'client-modern': 'client',
59 sw: 'sw',
60};
61const EXCLUDE_TRANSPILATION_PATTERNS = [
62 /node_modules\/mapbox-gl\//,
63 /node_modules\/react-dom\//,
64 /node_modules\/react\//,
65 /node_modules\/core-js\//,
66];
67const JS_EXT_PATTERN = /\.(mjs|js|jsx)$/;
68
69/*::
70import type {
71 ClientChunkMetadataState,
72 TranslationsManifest,
73 TranslationsManifestState,
74 LegacyBuildEnabledState,
75} from "./types.js";
76
77import type {
78 FusionRC
79} from "./load-fusionrc.js";
80
81export type WebpackConfigOpts = {|
82 id: $Keys<typeof COMPILATIONS>,
83 dir: string,
84 dev: boolean,
85 hmr: boolean,
86 watch: boolean,
87 preserveNames: boolean,
88 zopfli: boolean,
89 gzip: boolean,
90 brotli: boolean,
91 minify: boolean,
92 skipSourceMaps: boolean,
93 state: {
94 clientChunkMetadata: ClientChunkMetadataState,
95 legacyClientChunkMetadata: ClientChunkMetadataState,
96 mergedClientChunkMetadata: ClientChunkMetadataState,
97 i18nManifest: TranslationsManifest,
98 i18nDeferredManifest: TranslationsManifestState,
99 legacyBuildEnabled: LegacyBuildEnabledState,
100 },
101 fusionConfig: FusionRC,
102 legacyPkgConfig?: {
103 node?: Object
104 },
105 worker: Object
106|};
107*/
108const isProjectCode = (modulePath /*:string*/, dir /*:string*/) =>
109 modulePath.startsWith(getSrcPath(dir)) ||
110 /fusion-cli(\/|\\)(entries|plugins)/.test(modulePath);
111
112const getTransformDefault = (modulePath /*:string*/, dir /*:string*/) =>
113 isProjectCode(modulePath, dir) ? 'all' : 'spec';
114module.exports = {
115 getWebpackConfig,
116 getTransformDefault,
117};
118
119function getWebpackConfig(opts /*: WebpackConfigOpts */) {
120 const {
121 id,
122 dev,
123 dir,
124 hmr,
125 watch,
126 state,
127 fusionConfig,
128 zopfli, // TODO: Remove redundant zopfli option
129 gzip,
130 brotli,
131 minify,
132 skipSourceMaps,
133 legacyPkgConfig = {},
134 worker,
135 } = opts;
136 const main = 'src/main.js';
137
138 if (!fs.existsSync(path.join(dir, main))) {
139 throw new Error(`Project directory must contain a ${main} file`);
140 }
141
142 const runtime = COMPILATIONS[id];
143 const env = dev ? 'development' : 'production';
144 const shouldMinify = !dev && minify;
145
146 // Both options default to true, but if `--zopfli=false`
147 // it should be respected for backwards compatibility
148 const shouldGzip = zopfli && gzip;
149
150 const babelConfigData = {
151 target: runtime === 'server' ? 'node-bundled' : 'browser-modern',
152 specOnly: true,
153 plugins:
154 fusionConfig.babel && fusionConfig.babel.plugins
155 ? fusionConfig.babel.plugins
156 : [],
157 presets:
158 fusionConfig.babel && fusionConfig.babel.presets
159 ? fusionConfig.babel.presets
160 : [],
161 };
162
163 const babelOverridesData = {
164 dev: dev,
165 fusionTransforms: true,
166 assumeNoImportSideEffects: fusionConfig.assumeNoImportSideEffects,
167 target: runtime === 'server' ? 'node-bundled' : 'browser-modern',
168 specOnly: false,
169 };
170
171 const legacyBabelOverridesData = {
172 dev: dev,
173 fusionTransforms: true,
174 assumeNoImportSideEffects: fusionConfig.assumeNoImportSideEffects,
175 target: runtime === 'server' ? 'node-bundled' : 'browser-legacy',
176 specOnly: false,
177 };
178
179 const {experimentalBundleTest, experimentalTransformTest} = fusionConfig;
180 const babelTester = experimentalTransformTest
181 ? modulePath => {
182 if (!JS_EXT_PATTERN.test(modulePath)) {
183 return false;
184 }
185 const transform = experimentalTransformTest(
186 modulePath,
187 getTransformDefault(modulePath, dir)
188 );
189 if (transform === 'none') {
190 return false;
191 } else if (transform === 'all' || transform === 'spec') {
192 return true;
193 } else {
194 throw new Error(
195 `Unexpected value from experimentalTransformTest ${transform}. Expected 'spec' | 'all' | 'none'`
196 );
197 }
198 }
199 : JS_EXT_PATTERN;
200
201 return {
202 name: runtime,
203 target: {server: 'node', client: 'web', sw: 'webworker'}[runtime],
204 entry: {
205 main: [
206 runtime === 'client' &&
207 path.join(__dirname, '../entries/client-public-path.js'),
208 runtime === 'server' &&
209 path.join(__dirname, '../entries/server-public-path.js'),
210 dev &&
211 hmr &&
212 watch &&
213 runtime !== 'server' &&
214 `${require.resolve('webpack-hot-middleware/client')}?name=client`,
215 // TODO(#46): use 'webpack/hot/signal' instead
216 dev &&
217 hmr &&
218 watch &&
219 runtime === 'server' &&
220 `${require.resolve('webpack/hot/poll')}?1000`,
221 runtime === 'server' &&
222 path.join(__dirname, `../entries/${id}-entry.js`), // server-entry or serverless-entry
223 runtime === 'client' &&
224 path.join(__dirname, '../entries/client-entry.js'),
225 ].filter(Boolean),
226 },
227 mode: dev ? 'development' : 'production',
228 // TODO(#47): Do we need to do something different here for production?
229 stats: 'minimal',
230 /**
231 * `cheap-module-source-map` is best supported by Chrome DevTools
232 * See: https://github.com/webpack/webpack/issues/2145#issuecomment-294361203
233 *
234 * We use `source-map` in production but effectively create a
235 * `hidden-source-map` using SourceMapPlugin to strip the comment.
236 *
237 * Chrome DevTools support doesn't matter in these case.
238 * We only use it for generating nice stack traces
239 */
240 // TODO(#6): what about node v8 inspector?
241 devtool: skipSourceMaps
242 ? false
243 : runtime === 'client' && !dev
244 ? 'source-map'
245 : runtime === 'sw'
246 ? 'hidden-source-map'
247 : 'cheap-module-source-map',
248 output: {
249 path: path.join(dir, `.fusion/dist/${env}/${runtime}`),
250 filename:
251 runtime === 'server'
252 ? 'server-main.js'
253 : dev
254 ? 'client-[name].js'
255 : 'client-[name]-[chunkhash].js',
256 libraryTarget: runtime === 'server' ? 'commonjs2' : 'var',
257 // This is the recommended default.
258 // See https://webpack.js.org/configuration/output/#output-sourcemapfilename
259 sourceMapFilename: `[file].map`,
260 // We will set __webpack_public_path__ at runtime, so this should be set to undefined
261 publicPath: void 0,
262 crossOriginLoading: 'anonymous',
263 devtoolModuleFilenameTemplate: (info /*: Object */) => {
264 // always return absolute paths in order to get sensible source map explorer visualization
265 return path.isAbsolute(info.absoluteResourcePath)
266 ? info.absoluteResourcePath
267 : path.join(dir, info.absoluteResourcePath);
268 },
269 },
270 performance: {
271 hints: false,
272 },
273 context: dir,
274 node: Object.assign(
275 getNodeConfig(runtime),
276 legacyPkgConfig.node,
277 fusionConfig.nodeBuiltins
278 ),
279 module: {
280 /**
281 * Compile-time error for importing a non-existent export
282 * https://github.com/facebookincubator/create-react-app/issues/1559
283 */
284 strictExportPresence: true,
285 rules: [
286 /**
287 * Global transforms (including ES2017+ transpilations)
288 */
289 runtime === 'server' && {
290 compiler: id => id === 'server',
291 test: babelTester,
292 exclude: EXCLUDE_TRANSPILATION_PATTERNS,
293 use: [
294 {
295 loader: babelLoader.path,
296 options: {
297 dir,
298 configCacheKey: 'server-config',
299 overrideCacheKey: 'server-override',
300 babelConfigData: {...babelConfigData},
301 /**
302 * Fusion-specific transforms (not applied to node_modules)
303 */
304 overrides: [
305 {
306 ...babelOverridesData,
307 },
308 ],
309 },
310 },
311 ],
312 },
313 /**
314 * Global transforms (including ES2017+ transpilations)
315 */
316 (runtime === 'client' || runtime === 'sw') && {
317 compiler: id => id === 'client' || id === 'sw',
318 test: babelTester,
319 exclude: EXCLUDE_TRANSPILATION_PATTERNS,
320 use: [
321 {
322 loader: babelLoader.path,
323 options: {
324 dir,
325 configCacheKey: 'client-config',
326 overrideCacheKey: 'client-override',
327 babelConfigData: {...babelConfigData},
328 /**
329 * Fusion-specific transforms (not applied to node_modules)
330 */
331 overrides: [
332 {
333 ...babelOverridesData,
334 },
335 ],
336 },
337 },
338 ],
339 },
340 /**
341 * Global transforms (including ES2017+ transpilations)
342 */
343 runtime === 'client' && {
344 compiler: id => id === 'client-legacy',
345 test: babelTester,
346 exclude: EXCLUDE_TRANSPILATION_PATTERNS,
347 use: [
348 {
349 loader: babelLoader.path,
350 options: {
351 dir,
352 configCacheKey: 'legacy-config',
353 overrideCacheKey: 'legacy-override',
354 babelConfigData: {
355 target:
356 runtime === 'server' ? 'node-bundled' : 'browser-legacy',
357 specOnly: true,
358 plugins:
359 fusionConfig.babel && fusionConfig.babel.plugins
360 ? fusionConfig.babel.plugins
361 : [],
362 presets:
363 fusionConfig.babel && fusionConfig.babel.presets
364 ? fusionConfig.babel.presets
365 : [],
366 },
367 /**
368 * Fusion-specific transforms (not applied to node_modules)
369 */
370 overrides: [
371 {
372 ...legacyBabelOverridesData,
373 },
374 ],
375 },
376 },
377 ],
378 },
379 {
380 test: /\.json$/,
381 type: 'javascript/auto',
382 loader: require.resolve('./loaders/json-loader.js'),
383 },
384 {
385 test: /\.ya?ml$/,
386 type: 'json',
387 loader: require.resolve('yaml-loader'),
388 },
389 {
390 test: /\.graphql$|.gql$/,
391 loader: require.resolve('graphql-tag/loader'),
392 },
393 fusionConfig.assumeNoImportSideEffects && {
394 sideEffects: false,
395 test: modulePath => {
396 if (
397 modulePath.includes('core-js/modules') ||
398 modulePath.includes('regenerator-runtime/runtime')
399 ) {
400 return false;
401 }
402
403 return true;
404 },
405 },
406 ].filter(Boolean),
407 },
408 externals: [
409 runtime === 'server' &&
410 ((context, request, callback) => {
411 if (/^[@a-z\-0-9]+/.test(request)) {
412 const absolutePath = resolveFrom.silent(context, request);
413 // do not bundle external packages and those not whitelisted
414 if (typeof absolutePath !== 'string') {
415 // if module is missing, skip rewriting to absolute path
416 return callback(null, request);
417 }
418 if (experimentalBundleTest) {
419 const bundle = experimentalBundleTest(
420 absolutePath,
421 'browser-only'
422 );
423 if (bundle === 'browser-only') {
424 // don't bundle on the server
425 return callback(null, 'commonjs ' + absolutePath);
426 } else if (bundle === 'universal') {
427 // bundle on the server
428 return callback();
429 } else {
430 throw new Error(
431 `Unexpected value: ${bundle} from experimentalBundleTest. Expected 'browser-only' | 'universal'.`
432 );
433 }
434 }
435 return callback(null, 'commonjs ' + absolutePath);
436 }
437 // bundle everything else (local files, __*)
438 return callback();
439 }),
440 ].filter(Boolean),
441 resolve: {
442 symlinks: process.env.NODE_PRESERVE_SYMLINKS ? false : true,
443 aliasFields: [
444 (runtime === 'client' || runtime === 'sw') && 'browser',
445 'es2015',
446 'es2017',
447 ].filter(Boolean),
448 alias: {
449 // we replace need to set the path to user application at build-time
450 __FUSION_ENTRY_PATH__: path.join(dir, main),
451 __ENV__: env,
452 },
453 plugins: [PnpWebpackPlugin],
454 },
455 resolveLoader: {
456 symlinks: process.env.NODE_PRESERVE_SYMLINKS ? false : true,
457 alias: {
458 [fileLoader.alias]: fileLoader.path,
459 [chunkIdsLoader.alias]: chunkIdsLoader.path,
460 [syncChunkIdsLoader.alias]: syncChunkIdsLoader.path,
461 [syncChunkPathsLoader.alias]: syncChunkPathsLoader.path,
462 [chunkUrlMapLoader.alias]: chunkUrlMapLoader.path,
463 [i18nManifestLoader.alias]: i18nManifestLoader.path,
464 [swLoader.alias]: swLoader.path,
465 [workerLoader.alias]: workerLoader.path,
466 },
467 plugins: [PnpWebpackPlugin.moduleLoader(module)],
468 },
469
470 plugins: [
471 runtime === 'client' && !dev && new SourceMapPlugin(),
472 runtime === 'client' &&
473 new webpack.optimize.RuntimeChunkPlugin({
474 name: 'runtime',
475 }),
476 (fusionConfig.defaultImportSideEffects === false ||
477 Array.isArray(fusionConfig.defaultImportSideEffects)) &&
478 new DefaultNoImportSideEffectsPlugin(
479 Array.isArray(fusionConfig.defaultImportSideEffects)
480 ? {ignoredPackages: fusionConfig.defaultImportSideEffects}
481 : {}
482 ),
483 new webpack.optimize.SideEffectsFlagPlugin(),
484 runtime === 'server' &&
485 new webpack.optimize.LimitChunkCountPlugin({maxChunks: 1}),
486 new ProgressBarPlugin(),
487 runtime === 'server' &&
488 new LoaderContextProviderPlugin('optsContext', opts),
489 new LoaderContextProviderPlugin(devContextKey, dev),
490 runtime === 'server' &&
491 new LoaderContextProviderPlugin(
492 clientChunkMetadataContextKey,
493 state.mergedClientChunkMetadata
494 ),
495 runtime === 'client'
496 ? new I18nDiscoveryPlugin(
497 state.i18nDeferredManifest,
498 state.i18nManifest
499 )
500 : new LoaderContextProviderPlugin(
501 translationsManifestContextKey,
502 state.i18nDeferredManifest
503 ),
504 new LoaderContextProviderPlugin(workerKey, worker),
505 !dev && shouldGzip && gzipWebpackPlugin,
506 !dev && brotli && brotliWebpackPlugin,
507 !dev && svgoWebpackPlugin,
508 // In development, skip the emitting phase on errors to ensure there are
509 // no assets emitted that include errors. This fixes an issue with hot reloading
510 // server side code and recovering from errors correctly. We only want to do this
511 // in dev because the CLI will not exit with an error code if the option is enabled,
512 // so failed builds would look like successful ones.
513 watch && new webpack.NoEmitOnErrorsPlugin(),
514 runtime === 'server'
515 ? // Server
516 new InstrumentedImportDependencyTemplatePlugin({
517 compilation: 'server',
518 clientChunkMetadata: state.mergedClientChunkMetadata,
519 })
520 : /**
521 * Client
522 * Don't wait for the client manifest on the client.
523 * The underlying plugin is able determine client chunk metadata on its own.
524 */
525 new InstrumentedImportDependencyTemplatePlugin({
526 compilation: 'client',
527 i18nManifest: state.i18nManifest,
528 }),
529 dev && hmr && watch && new webpack.HotModuleReplacementPlugin(),
530 !dev && runtime === 'client' && new webpack.HashedModuleIdsPlugin(),
531 runtime === 'client' &&
532 // case-insensitive paths can cause problems
533 new CaseSensitivePathsPlugin(),
534 runtime === 'server' &&
535 new webpack.BannerPlugin({
536 raw: true,
537 entryOnly: true,
538 // Enforce NODE_ENV at runtime
539 banner: getEnvBanner(env),
540 }),
541 new webpack.EnvironmentPlugin({NODE_ENV: env}),
542 id === 'client-modern' &&
543 new ClientChunkMetadataStateHydratorPlugin(state.clientChunkMetadata),
544 id === 'client-modern' &&
545 new ChildCompilationPlugin({
546 name: 'client-legacy',
547 entry: [
548 path.resolve(__dirname, '../entries/client-public-path.js'),
549 path.resolve(__dirname, '../entries/client-entry.js'),
550 // EVENTUALLY HAVE HMR
551 ],
552 enabledState: opts.state.legacyBuildEnabled,
553 outputOptions: {
554 filename: opts.dev
555 ? 'client-legacy-[name].js'
556 : 'client-legacy-[name]-[chunkhash].js',
557 chunkFilename: opts.dev
558 ? 'client-legacy-[name].js'
559 : 'client-legacy-[name]-[chunkhash].js',
560 },
561 plugins: options => [
562 new webpack.optimize.RuntimeChunkPlugin(
563 options.optimization.runtimeChunk
564 ),
565 new webpack.optimize.SplitChunksPlugin(
566 options.optimization.splitChunks
567 ),
568 // need to re-apply template
569 new InstrumentedImportDependencyTemplatePlugin({
570 compilation: 'client',
571 i18nManifest: state.i18nManifest,
572 }),
573 new ClientChunkMetadataStateHydratorPlugin(
574 state.legacyClientChunkMetadata
575 ),
576 new ChunkIdPrefixPlugin(),
577 ],
578 }),
579 ].filter(Boolean),
580 optimization: {
581 runtimeChunk: runtime === 'client' && {name: 'runtime'},
582 splitChunks:
583 runtime !== 'client'
584 ? void 0
585 : fusionConfig.splitChunks
586 ? // Tilde character in filenames is not well supported
587 // https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html
588 {...fusionConfig.splitChunks, automaticNameDelimiter: '-'}
589 : {
590 chunks: 'async',
591 automaticNameDelimiter: '-',
592 cacheGroups: {
593 default: {
594 minChunks: 2,
595 reuseExistingChunk: true,
596 },
597 vendor: {
598 test: /[\\/]node_modules[\\/]/,
599 name: 'vendor',
600 chunks: 'initial',
601 enforce: true,
602 },
603 },
604 },
605 minimize: shouldMinify,
606 minimizer: shouldMinify
607 ? [
608 new TerserPlugin({
609 sourceMap: skipSourceMaps ? false : true, // default from webpack (see https://github.com/webpack/webpack/blob/aab3554cad2ebc5d5e9645e74fb61842e266da34/lib/WebpackOptionsDefaulter.js#L290-L297)
610 cache: true, // default from webpack
611 parallel: true, // default from webpack
612 extractComments: false,
613 terserOptions: {
614 compress: {
615 // typeofs: true (default) transforms typeof foo == "undefined" into foo === void 0.
616 // This mangles mapbox-gl creating an error when used alongside with window global mangling:
617 // https://github.com/webpack-contrib/uglifyjs-webpack-plugin/issues/189
618 typeofs: false,
619
620 // inline=2 can cause const reassignment
621 // https://github.com/mishoo/UglifyJS2/issues/2842
622 inline: 1,
623 },
624
625 keep_fnames: opts.preserveNames,
626 keep_classnames: opts.preserveNames,
627 },
628 }),
629 ]
630 : undefined,
631 },
632 };
633}
634
635// Allow overrides with a warning for `dev` command. In production builds, throw if NODE_ENV is not `production`.
636function getEnvBanner(env) {
637 return `
638if (process.env.NODE_ENV && process.env.NODE_ENV !== '${env}') {
639 if (${env === 'production' ? 'true' : 'false'}) {
640 throw new Error(\`NODE_ENV (\${process.env.NODE_ENV}) does not match value for compiled assets: ${env}\`);
641 } else {
642 console.warn('Overriding NODE_ENV: ' + process.env.NODE_ENV + ' to ${env} in order to match value for compiled assets');
643 process.env.NODE_ENV = '${env}';
644 }
645} else {
646 process.env.NODE_ENV = '${env}';
647}
648 `;
649}
650
651function getNodeConfig(runtime) {
652 const emptyForWeb = runtime === 'client' ? 'empty' : false;
653 return {
654 // Polyfilling process involves lots of cruft. Better to explicitly inline env value statically
655 process: false,
656 // We definitely don't want automatic Buffer polyfills. This should be explicit and in userland code
657 Buffer: false,
658 // We definitely don't want automatic setImmediate polyfills. This should be explicit and in userland code
659 setImmediate: false,
660 // We want these to resolve to the original file source location, not the compiled location
661 // in the future, we may want to consider using `import.meta`
662 __filename: true,
663 __dirname: true,
664 // This is required until we have better tree shaking. See https://github.com/fusionjs/fusion-cli/issues/254
665 child_process: emptyForWeb,
666 cluster: emptyForWeb,
667 crypto: emptyForWeb,
668 dgram: emptyForWeb,
669 dns: emptyForWeb,
670 fs: emptyForWeb,
671 module: emptyForWeb,
672 net: emptyForWeb,
673 readline: emptyForWeb,
674 repl: emptyForWeb,
675 tls: emptyForWeb,
676 };
677}
678
679function getSrcPath(dir) {
680 // resolving to the real path of a known top-level file is required to support Bazel, which symlinks source files individually
681 if (process.env.NODE_PRESERVE_SYMLINKS) {
682 return path.resolve(dir, 'src');
683 }
684 try {
685 const real = path.dirname(
686 fs.realpathSync(path.resolve(dir, 'package.json'))
687 );
688 return path.resolve(real, 'src');
689 } catch (e) {
690 return path.resolve(dir, 'src');
691 }
692}