From 7bf0ed3c84bb1803121049d9c534c51163ab3b9a Mon Sep 17 00:00:00 2001 From: William Chargin Date: Mon, 30 Jul 2018 16:22:55 -0700 Subject: [PATCH] new-webpack: functionize (#564) Summary: This will enable us to differentiate the production and development behavior where necessary (primarily, only running minification in prod). Best reviewed with `git show -w`. Test Plan: The diff with `git show -w` and the fact that `yarn flow` passes should be sufficient. If you really want to be thorough, run Webpack with this config file and `NODE_ENV` set to `production`. wchargin-branch: webpack-functionize --- config/makeWebpackConfig.js | 469 +++++++++++++++++++----------------- 1 file changed, 242 insertions(+), 227 deletions(-) diff --git a/config/makeWebpackConfig.js b/config/makeWebpackConfig.js index 7cc2f56..d1f7084 100644 --- a/config/makeWebpackConfig.js +++ b/config/makeWebpackConfig.js @@ -30,234 +30,249 @@ if (env.stringified["process.env"].NODE_ENV !== '"production"') { // This is the production configuration. // It compiles slowly and is focused on producing a fast and minimal bundle. // The development configuration is different and lives in a separate file. -module.exports = { - // Don't attempt to continue if there are any errors. - bail: true, - // We generate sourcemaps in production. This is slow but gives good results. - // You can exclude the *.map files from the build during deployment. - devtool: shouldUseSourceMap ? "source-map" : false, - // In production, we only want to load the polyfills and the app code. - entry: { - main: [require.resolve("./polyfills"), paths.appIndexJs], - ssr: [require.resolve("./polyfills"), paths.appServerSideRenderingIndexJs], - }, - output: { - // The build folder. - path: paths.appBuild, - // Generated JS file names (with nested folders). - // There will be one main bundle, and one file per asynchronous chunk. - // We don't currently advertise code splitting but Webpack supports it. - filename: "static/js/[name].[chunkhash:8].js", - chunkFilename: "static/js/[name].[chunkhash:8].chunk.js", - // We inferred the "public path" (such as / or /my-project) from homepage. - publicPath: publicPath, - // Point sourcemap entries to original disk location (format as URL on Windows) - devtoolModuleFilenameTemplate: ( - info /*: - {| - // https://webpack.js.org/configuration/output/#output-devtoolmodulefilenametemplate - +absoluteResourcePath: string, - +allLoaders: string, - +hash: string, - +id: string, - +loaders: string, - +resource: string, - +resourcePath: string, - +namespace: string, - |} - */ - ) => - path - .relative(paths.appSrc, info.absoluteResourcePath) - .replace(/\\/g, "/"), - // We need to use a UMD module to build the static site. - libraryTarget: "umd", - }, - resolve: { - // This allows you to set a fallback for where Webpack should look for modules. - // We placed these paths second because we want `node_modules` to "win" - // if there are any conflicts. This matches Node resolution mechanism. - // https://github.com/facebookincubator/create-react-app/issues/253 - modules: [ - "node_modules", - paths.appNodeModules, - ...(process.env.NODE_PATH || "").split(path.delimiter).filter(Boolean), - ], - // These are the reasonable defaults supported by the Node ecosystem. - // We also include JSX as a common component filename extension to support - // some tools, although we do not recommend using it, see: - // https://github.com/facebookincubator/create-react-app/issues/290 - // `web` extension prefixes have been added for better support - // for React Native Web. - extensions: [".web.js", ".mjs", ".js", ".json", ".web.jsx", ".jsx"], - alias: { - // Support React Native Web - // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ - "react-native": "react-native-web", +function makeConfig(_unused_mode /*: "production" | "development" */) { + return { + // Don't attempt to continue if there are any errors. + bail: true, + // We generate sourcemaps in production. This is slow but gives good results. + // You can exclude the *.map files from the build during deployment. + devtool: shouldUseSourceMap ? "source-map" : false, + // In production, we only want to load the polyfills and the app code. + entry: { + main: [require.resolve("./polyfills"), paths.appIndexJs], + ssr: [ + require.resolve("./polyfills"), + paths.appServerSideRenderingIndexJs, + ], + }, + output: { + // The build folder. + path: paths.appBuild, + // Generated JS file names (with nested folders). + // There will be one main bundle, and one file per asynchronous chunk. + // We don't currently advertise code splitting but Webpack supports it. + filename: "static/js/[name].[chunkhash:8].js", + chunkFilename: "static/js/[name].[chunkhash:8].chunk.js", + // We inferred the "public path" (such as / or /my-project) from homepage. + publicPath: publicPath, + // Point sourcemap entries to original disk location (format as URL on Windows) + devtoolModuleFilenameTemplate: ( + info /*: + {| + // https://webpack.js.org/configuration/output/#output-devtoolmodulefilenametemplate + +absoluteResourcePath: string, + +allLoaders: string, + +hash: string, + +id: string, + +loaders: string, + +resource: string, + +resourcePath: string, + +namespace: string, + |} + */ + ) => + path + .relative(paths.appSrc, info.absoluteResourcePath) + .replace(/\\/g, "/"), + // We need to use a UMD module to build the static site. + libraryTarget: "umd", + }, + resolve: { + // This allows you to set a fallback for where Webpack should look for modules. + // We placed these paths second because we want `node_modules` to "win" + // if there are any conflicts. This matches Node resolution mechanism. + // https://github.com/facebookincubator/create-react-app/issues/253 + modules: [ + "node_modules", + paths.appNodeModules, + ...(process.env.NODE_PATH || "").split(path.delimiter).filter(Boolean), + ], + // These are the reasonable defaults supported by the Node ecosystem. + // We also include JSX as a common component filename extension to support + // some tools, although we do not recommend using it, see: + // https://github.com/facebookincubator/create-react-app/issues/290 + // `web` extension prefixes have been added for better support + // for React Native Web. + extensions: [".web.js", ".mjs", ".js", ".json", ".web.jsx", ".jsx"], + alias: { + // Support React Native Web + // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ + "react-native": "react-native-web", + }, + plugins: [ + // Prevents users from importing files from outside of src/ (or node_modules/). + // This often causes confusion because we only process files within src/ with babel. + // To fix this, we prevent you from importing files out of src/ -- if you'd like to, + // please link the files into your node_modules/ and let module-resolution kick in. + // Make sure your source files are compiled, as they will not be processed in any way. + new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]), + ], + }, + module: { + strictExportPresence: true, + rules: [ + // TODO: Disable require.ensure as it's not a standard language feature. + // We are waiting for https://github.com/facebookincubator/create-react-app/issues/2176. + // { parser: { requireEnsure: false } }, + + // First, run the linter. + // It's important to do this before Babel processes the JS. + { + test: /\.(js|jsx|mjs)$/, + enforce: "pre", + use: [ + { + options: { + formatter: eslintFormatter, + eslintPath: require.resolve("eslint"), + }, + loader: require.resolve("eslint-loader"), + }, + ], + include: paths.appSrc, + }, + { + // "oneOf" will traverse all following loaders until one will + // match the requirements. When no loader matches it will fall + // back to the "file" loader at the end of the loader list. + oneOf: [ + // "url" loader works just like "file" loader but it also embeds + // assets smaller than specified size as data URLs to avoid requests. + { + test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], + loader: require.resolve("url-loader"), + options: { + limit: 10000, + name: "static/media/[name].[hash:8].[ext]", + }, + }, + // Process JS with Babel. + { + test: /\.(js|jsx|mjs)$/, + include: paths.appSrc, + loader: require.resolve("babel-loader"), + options: { + compact: true, + }, + }, + { + test: /\.css$/, + loader: "css-loader", // TODO(@wchargin): add csso-loader + }, + // "file" loader makes sure assets end up in the `build` folder. + // When you `import` an asset, you get its filename. + // This loader doesn't use a "test" so it will catch all modules + // that fall through the other loaders. + { + loader: require.resolve("file-loader"), + // Exclude `js` files to keep "css" loader working as it injects + // it's runtime that would otherwise processed through "file" loader. + // Also exclude `html` and `json` extensions so they get processed + // by webpacks internal loaders. + exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/], + options: { + name: "static/media/[name].[hash:8].[ext]", + }, + }, + // ** STOP ** Are you adding a new loader? + // Make sure to add the new loader(s) before the "file" loader. + ], + }, + ], }, plugins: [ - // Prevents users from importing files from outside of src/ (or node_modules/). - // This often causes confusion because we only process files within src/ with babel. - // To fix this, we prevent you from importing files out of src/ -- if you'd like to, - // please link the files into your node_modules/ and let module-resolution kick in. - // Make sure your source files are compiled, as they will not be processed in any way. - new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]), + new StaticSiteGeneratorPlugin({ + entry: "ssr", + paths: require(paths.appRouteData).routeData.map(({path}) => path), + locals: {}, + }), + // Makes some environment variables available to the JS code, for example: + // if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`. + // It is absolutely essential that NODE_ENV was set to production here. + // Otherwise React will be compiled in the very slow development mode. + new webpack.DefinePlugin(env.stringified), + // Minify the code. + new webpack.optimize.UglifyJsPlugin({ + compress: { + warnings: false, + // Disabled because of an issue with Uglify breaking seemingly valid code: + // https://github.com/facebookincubator/create-react-app/issues/2376 + // Pending further investigation: + // https://github.com/mishoo/UglifyJS2/issues/2011 + comparisons: false, + }, + mangle: { + safari10: true, + }, + output: { + comments: false, + // Turned on because emoji and regex is not minified properly using default + // https://github.com/facebookincubator/create-react-app/issues/2488 + ascii_only: true, + }, + sourceMap: shouldUseSourceMap, + }), + // Generate a manifest file which contains a mapping of all asset filenames + // to their corresponding output file so that tools can pick it up without + // having to parse `index.html`. + new ManifestPlugin({ + fileName: "asset-manifest.json", + }), + // Generate a service worker script that will precache, and keep up to date, + // the HTML & assets that are part of the Webpack build. + new SWPrecacheWebpackPlugin({ + // By default, a cache-busting query parameter is appended to requests + // used to populate the caches, to ensure the responses are fresh. + // If a URL is already hashed by Webpack, then there is no concern + // about it being stale, and the cache-busting can be skipped. + dontCacheBustUrlsMatching: /\.\w{8}\./, + filename: "service-worker.js", + logger(message) { + if (message.indexOf("Total precache size is") === 0) { + // This message occurs for every build and is a bit too noisy. + return; + } + if (message.indexOf("Skipping static resource") === 0) { + // This message obscures real errors so we ignore it. + // https://github.com/facebookincubator/create-react-app/issues/2612 + return; + } + console.log(message); + }, + minify: true, + // For unknown URLs, fallback to the index page + navigateFallback: publicUrl + "/index.html", + // Ignores URLs starting from /__ (useful for Firebase): + // https://github.com/facebookincubator/create-react-app/issues/2237#issuecomment-302693219 + navigateFallbackWhitelist: [/^(?!\/__).*/], + // Don't precache sourcemaps (they're large) and build asset manifest: + staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/], + }), + // Moment.js is an extremely popular library that bundles large locale files + // by default due to how Webpack interprets its code. This is a practical + // solution that requires the user to opt into importing specific locales. + // https://github.com/jmblog/how-to-optimize-momentjs-with-webpack + // You can remove this if you don't use Moment.js: + new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), ], - }, - module: { - strictExportPresence: true, - rules: [ - // TODO: Disable require.ensure as it's not a standard language feature. - // We are waiting for https://github.com/facebookincubator/create-react-app/issues/2176. - // { parser: { requireEnsure: false } }, + // Some libraries import Node modules but don't use them in the browser. + // Tell Webpack to provide empty mocks for them so importing them works. + node: { + dgram: "empty", + fs: "empty", + net: "empty", + tls: "empty", + child_process: "empty", + }, + }; +} - // First, run the linter. - // It's important to do this before Babel processes the JS. - { - test: /\.(js|jsx|mjs)$/, - enforce: "pre", - use: [ - { - options: { - formatter: eslintFormatter, - eslintPath: require.resolve("eslint"), - }, - loader: require.resolve("eslint-loader"), - }, - ], - include: paths.appSrc, - }, - { - // "oneOf" will traverse all following loaders until one will - // match the requirements. When no loader matches it will fall - // back to the "file" loader at the end of the loader list. - oneOf: [ - // "url" loader works just like "file" loader but it also embeds - // assets smaller than specified size as data URLs to avoid requests. - { - test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], - loader: require.resolve("url-loader"), - options: { - limit: 10000, - name: "static/media/[name].[hash:8].[ext]", - }, - }, - // Process JS with Babel. - { - test: /\.(js|jsx|mjs)$/, - include: paths.appSrc, - loader: require.resolve("babel-loader"), - options: { - compact: true, - }, - }, - { - test: /\.css$/, - loader: "css-loader", // TODO(@wchargin): add csso-loader - }, - // "file" loader makes sure assets end up in the `build` folder. - // When you `import` an asset, you get its filename. - // This loader doesn't use a "test" so it will catch all modules - // that fall through the other loaders. - { - loader: require.resolve("file-loader"), - // Exclude `js` files to keep "css" loader working as it injects - // it's runtime that would otherwise processed through "file" loader. - // Also exclude `html` and `json` extensions so they get processed - // by webpacks internal loaders. - exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/], - options: { - name: "static/media/[name].[hash:8].[ext]", - }, - }, - // ** STOP ** Are you adding a new loader? - // Make sure to add the new loader(s) before the "file" loader. - ], - }, - ], - }, - plugins: [ - new StaticSiteGeneratorPlugin({ - entry: "ssr", - paths: require(paths.appRouteData).routeData.map(({path}) => path), - locals: {}, - }), - // Makes some environment variables available to the JS code, for example: - // if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`. - // It is absolutely essential that NODE_ENV was set to production here. - // Otherwise React will be compiled in the very slow development mode. - new webpack.DefinePlugin(env.stringified), - // Minify the code. - new webpack.optimize.UglifyJsPlugin({ - compress: { - warnings: false, - // Disabled because of an issue with Uglify breaking seemingly valid code: - // https://github.com/facebookincubator/create-react-app/issues/2376 - // Pending further investigation: - // https://github.com/mishoo/UglifyJS2/issues/2011 - comparisons: false, - }, - mangle: { - safari10: true, - }, - output: { - comments: false, - // Turned on because emoji and regex is not minified properly using default - // https://github.com/facebookincubator/create-react-app/issues/2488 - ascii_only: true, - }, - sourceMap: shouldUseSourceMap, - }), - // Generate a manifest file which contains a mapping of all asset filenames - // to their corresponding output file so that tools can pick it up without - // having to parse `index.html`. - new ManifestPlugin({ - fileName: "asset-manifest.json", - }), - // Generate a service worker script that will precache, and keep up to date, - // the HTML & assets that are part of the Webpack build. - new SWPrecacheWebpackPlugin({ - // By default, a cache-busting query parameter is appended to requests - // used to populate the caches, to ensure the responses are fresh. - // If a URL is already hashed by Webpack, then there is no concern - // about it being stale, and the cache-busting can be skipped. - dontCacheBustUrlsMatching: /\.\w{8}\./, - filename: "service-worker.js", - logger(message) { - if (message.indexOf("Total precache size is") === 0) { - // This message occurs for every build and is a bit too noisy. - return; - } - if (message.indexOf("Skipping static resource") === 0) { - // This message obscures real errors so we ignore it. - // https://github.com/facebookincubator/create-react-app/issues/2612 - return; - } - console.log(message); - }, - minify: true, - // For unknown URLs, fallback to the index page - navigateFallback: publicUrl + "/index.html", - // Ignores URLs starting from /__ (useful for Firebase): - // https://github.com/facebookincubator/create-react-app/issues/2237#issuecomment-302693219 - navigateFallbackWhitelist: [/^(?!\/__).*/], - // Don't precache sourcemaps (they're large) and build asset manifest: - staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/], - }), - // Moment.js is an extremely popular library that bundles large locale files - // by default due to how Webpack interprets its code. This is a practical - // solution that requires the user to opt into importing specific locales. - // https://github.com/jmblog/how-to-optimize-momentjs-with-webpack - // You can remove this if you don't use Moment.js: - new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), - ], - // Some libraries import Node modules but don't use them in the browser. - // Tell Webpack to provide empty mocks for them so importing them works. - node: { - dgram: "empty", - fs: "empty", - net: "empty", - tls: "empty", - child_process: "empty", - }, -}; +function getMode() { + const mode = process.env.NODE_ENV; + if (mode !== "production" && mode !== "development") { + throw new Error("unknown mode: " + String(mode)); + } + return mode; +} + +module.exports = makeConfig(getMode());