diff --git a/config/env.js b/config/env.js index 211711b2..f04c21ad 100644 --- a/config/env.js +++ b/config/env.js @@ -1,4 +1,3 @@ -'use strict'; const fs = require('fs'); const path = require('path'); @@ -7,7 +6,8 @@ const paths = require('./paths'); // Make sure that including paths.js after env.js will read .env variables. delete require.cache[require.resolve('./paths')]; -const NODE_ENV = process.env.NODE_ENV; +const { NODE_ENV } = process.env; + if (!NODE_ENV) { throw new Error( 'The NODE_ENV environment variable is required but was not specified.' @@ -18,6 +18,7 @@ if (!NODE_ENV) { const dotenvFiles = [ `${paths.dotenv}.${NODE_ENV}.local`, `${paths.dotenv}.${NODE_ENV}`, + // Don't include `.env.local` for `test` environment // since normally you expect tests to produce the same // results for everyone @@ -30,7 +31,7 @@ const dotenvFiles = [ // that have already been set. Variable expansion is supported in .env files. // https://github.com/motdotla/dotenv // https://github.com/motdotla/dotenv-expand -dotenvFiles.forEach(dotenvFile => { +dotenvFiles.forEach((dotenvFile) => { if (fs.existsSync(dotenvFile)) { require('dotenv-expand')( require('dotenv').config({ @@ -50,10 +51,11 @@ dotenvFiles.forEach(dotenvFile => { // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 // We also resolve them to make sure all tools using them work consistently. const appDirectory = fs.realpathSync(process.cwd()); + process.env.NODE_PATH = (process.env.NODE_PATH || '') .split(path.delimiter) - .filter(folder => folder && !path.isAbsolute(folder)) - .map(folder => path.resolve(appDirectory, folder)) + .filter((folder) => folder && !path.isAbsolute(folder)) + .map((folder) => path.resolve(appDirectory, folder)) .join(path.delimiter); // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be @@ -62,16 +64,19 @@ const REACT_APP = /^REACT_APP_/i; function getClientEnvironment(publicUrl) { const raw = Object.keys(process.env) - .filter(key => REACT_APP.test(key)) + .filter((key) => REACT_APP.test(key)) .reduce( (env, key) => { env[key] = process.env[key]; + return env; }, { + // Useful for determining whether we’re running in production mode. // Most importantly, it switches React into the correct mode. NODE_ENV: process.env.NODE_ENV || 'development', + // Useful for resolving the correct path to static assets in `public`. // For example, . // This should only be used as an escape hatch. Normally you would put @@ -79,10 +84,12 @@ function getClientEnvironment(publicUrl) { PUBLIC_URL: publicUrl, } ); + // Stringify all values so we can feed into Webpack DefinePlugin const stringified = { 'process.env': Object.keys(raw).reduce((env, key) => { env[key] = JSON.stringify(raw[key]); + return env; }, {}), }; diff --git a/config/jest/cssTransform.js b/config/jest/cssTransform.js index 8f651148..4e138d18 100644 --- a/config/jest/cssTransform.js +++ b/config/jest/cssTransform.js @@ -1,4 +1,3 @@ -'use strict'; // This is a custom Jest transformer turning style imports into empty objects. // http://facebook.github.io/jest/docs/en/webpack.html diff --git a/config/jest/fileTransform.js b/config/jest/fileTransform.js index 07010e33..1567f853 100644 --- a/config/jest/fileTransform.js +++ b/config/jest/fileTransform.js @@ -1,4 +1,3 @@ -'use strict'; const path = require('path'); diff --git a/config/paths.js b/config/paths.js index 65bd4599..b34d9058 100644 --- a/config/paths.js +++ b/config/paths.js @@ -1,4 +1,3 @@ -'use strict'; const path = require('path'); const fs = require('fs'); @@ -7,22 +6,23 @@ const url = require('url'); // Make sure any symlinks in the project folder are resolved: // https://github.com/facebook/create-react-app/issues/637 const appDirectory = fs.realpathSync(process.cwd()); -const resolveApp = relativePath => path.resolve(appDirectory, relativePath); +const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath); const envPublicUrl = process.env.PUBLIC_URL; function ensureSlash(inputPath, needsSlash) { const hasSlash = inputPath.endsWith('/'); + if (hasSlash && !needsSlash) { return inputPath.substr(0, inputPath.length - 1); } else if (!hasSlash && needsSlash) { return `${inputPath}/`; - } else { - return inputPath; } + + return inputPath; } -const getPublicUrl = appPackageJson => +const getPublicUrl = (appPackageJson) => envPublicUrl || require(appPackageJson).homepage; // We use `PUBLIC_URL` environment variable or "homepage" field to infer @@ -35,6 +35,7 @@ function getServedPath(appPackageJson) { const publicUrl = getPublicUrl(appPackageJson); const servedUrl = envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/'); + return ensureSlash(servedUrl, true); } @@ -54,9 +55,8 @@ const moduleFileExtensions = [ // Resolve file paths in the same order as webpack const resolveModule = (resolveFn, filePath) => { - const extension = moduleFileExtensions.find(extension => - fs.existsSync(resolveFn(`${filePath}.${extension}`)) - ); + const extension = moduleFileExtensions.find((extension) => + fs.existsSync(resolveFn(`${filePath}.${extension}`))); if (extension) { return resolveFn(`${filePath}.${extension}`); diff --git a/config/webpack.config.js b/config/webpack.config.js index 184c4dbb..babf3b02 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -1,4 +1,3 @@ -'use strict'; const fs = require('fs'); const path = require('path'); @@ -18,15 +17,15 @@ const WorkboxWebpackPlugin = require('workbox-webpack-plugin'); const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent'); -const paths = require('./paths'); -const getClientEnvironment = require('./env'); const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin-alt'); const typescriptFormatter = require('react-dev-utils/typescriptFormatter'); - +const getClientEnvironment = require('./env'); +const paths = require('./paths'); // Source maps are resource heavy and can cause out of memory issue for large source files. const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false'; + // Some apps do not need the benefits of saving a web request, so not inlining the chunk // makes for a smoother build process. const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false'; @@ -42,7 +41,8 @@ const sassModuleRegex = /\.module\.(scss|sass)$/; // This is the production and development configuration. // It is focused on developer experience, fast rebuilds, and a minimal bundle. -module.exports = function(webpackEnv) { +/* eslint-disable complexity */ +module.exports = (webpackEnv) => { const isEnvDevelopment = webpackEnv === 'development'; const isEnvProduction = webpackEnv === 'production'; @@ -52,6 +52,7 @@ module.exports = function(webpackEnv) { const publicPath = isEnvProduction ? paths.servedPath : isEnvDevelopment && '/'; + // Some apps do not use client-side routing with pushState. // For these, "homepage" can be set to "." to enable relative asset paths. const shouldUseRelativeAssetPaths = publicPath === './'; @@ -62,6 +63,7 @@ module.exports = function(webpackEnv) { const publicUrl = isEnvProduction ? publicPath.slice(0, -1) : isEnvDevelopment && ''; + // Get environment variables to inject into our app. const env = getClientEnvironment(publicUrl); @@ -81,11 +83,13 @@ module.exports = function(webpackEnv) { options: cssOptions, }, { + // Options for PostCSS as we reference these options twice // Adds vendor prefixing based on your specified browser support in // package.json loader: require.resolve('postcss-loader'), options: { + // Necessary for external CSS imports to work // https://github.com/facebook/create-react-app/issues/2677 ident: 'postcss', @@ -102,6 +106,7 @@ module.exports = function(webpackEnv) { }, }, ].filter(Boolean); + if (preProcessor) { loaders.push({ loader: require.resolve(preProcessor), @@ -110,11 +115,13 @@ module.exports = function(webpackEnv) { }, }); } + return loaders; }; return { mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development', + // Stop compilation early in production bail: isEnvProduction, devtool: isEnvProduction @@ -122,9 +129,11 @@ module.exports = function(webpackEnv) { ? 'source-map' : false : isEnvDevelopment && 'cheap-module-source-map', + // These are the "entry points" to our application. // This means they will be the "root" imports that are included in JS bundle. entry: [ + // Include an alternative client for WebpackDevServer. A client's job is to // connect to WebpackDevServer by a socket and get notified about changes. // When you save a file, the client will either apply hot updates (in case @@ -137,45 +146,55 @@ module.exports = function(webpackEnv) { // require.resolve('webpack/hot/dev-server'), isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient'), + // Finally, this is your app's code: paths.appIndexJs, + // We include the app code last so that if there is a runtime error during // initialization, it doesn't blow up the WebpackDevServer client, and // changing JS code would still trigger a refresh. ].filter(Boolean), output: { + // The build folder. path: isEnvProduction ? paths.appBuild : undefined, + // Add /* filename */ comments to generated require()s in the output. pathinfo: isEnvDevelopment, + // There will be one main bundle, and one file per asynchronous chunk. // In development, it does not produce real files. filename: isEnvProduction ? 'static/js/[name].[chunkhash:8].js' : isEnvDevelopment && 'static/js/bundle.js', + // There are also additional JS chunk files if you use code splitting. chunkFilename: isEnvProduction ? 'static/js/[name].[chunkhash:8].chunk.js' : isEnvDevelopment && 'static/js/[name].chunk.js', + // We inferred the "public path" (such as / or /my-project) from homepage. // We use "/" in development. - publicPath: publicPath, + publicPath, + // Point sourcemap entries to original disk location (format as URL on Windows) devtoolModuleFilenameTemplate: isEnvProduction - ? info => - path - .relative(paths.appSrc, info.absoluteResourcePath) - .replace(/\\/g, '/') + ? (info) => + path + .relative(paths.appSrc, info.absoluteResourcePath) + .replace(/\\/g, '/') : isEnvDevelopment && - (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')), + ((info) => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')), }, optimization: { minimize: isEnvProduction, minimizer: [ + // This is only used in production mode new TerserPlugin({ terserOptions: { parse: { + // we want terser to parse ecma 8 code. However, we don't want it // to apply any minfication steps that turns valid ecma 5 code // into invalid ecma 5 code. This is why the 'compress' and 'output' @@ -186,11 +205,13 @@ module.exports = function(webpackEnv) { compress: { ecma: 5, warnings: false, + // Disabled because of an issue with Uglify breaking seemingly valid code: // https://github.com/facebook/create-react-app/issues/2376 // Pending further investigation: // https://github.com/mishoo/UglifyJS2/issues/2011 comparisons: false, + // Disabled because of an issue with Terser breaking valid code: // https://github.com/facebook/create-react-app/issues/5250 // Pending futher investigation: @@ -203,35 +224,42 @@ module.exports = function(webpackEnv) { output: { ecma: 5, comments: false, + // Turned on because emoji and regex is not minified properly using default // https://github.com/facebook/create-react-app/issues/2488 ascii_only: true, }, }, + // Use multi-process parallel running to improve the build speed // Default number of concurrent runs: os.cpus().length - 1 parallel: true, + // Enable file caching cache: true, sourceMap: shouldUseSourceMap, }), + // This is only used in production mode new OptimizeCSSAssetsPlugin({ cssProcessorOptions: { parser: safePostCssParser, map: shouldUseSourceMap ? { - // `inline: false` forces the sourcemap to be output into a - // separate file - inline: false, - // `annotation: true` appends the sourceMappingURL to the end of - // the css file, helping the browser find the sourcemap - annotation: true, - } + + // `inline: false` forces the sourcemap to be output into a + // separate file + inline: false, + + // `annotation: true` appends the sourceMappingURL to the end of + // the css file, helping the browser find the sourcemap + annotation: true, + } : false, }, }), ], + // Automatically split vendor and commons // https://twitter.com/wSokra/status/969633336732905474 // https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366 @@ -239,19 +267,23 @@ module.exports = function(webpackEnv) { chunks: 'all', name: false, }, + // Keep the runtime chunk separated to enable long term caching // https://twitter.com/wSokra/status/969679223278505985 runtimeChunk: true, }, 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/facebook/create-react-app/issues/253 - modules: ['node_modules'].concat( + modules: [ 'node_modules' ].concat( + // It is guaranteed to exist because we tweak it in `env.js` 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: @@ -259,27 +291,31 @@ module.exports = function(webpackEnv) { // `web` extension prefixes have been added for better support // for React Native Web. extensions: paths.moduleFileExtensions - .map(ext => `.${ext}`) - .filter(ext => useTypeScript || !ext.includes('ts')), + .map((ext) => `.${ext}`) + .filter((ext) => useTypeScript || !ext.includes('ts')), 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: [ + // Adds support for installing with Plug'n'Play, leading to faster installs and adding // guards against forgotten dependencies and such. PnpWebpackPlugin, + // 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 ModuleScopePlugin(paths.appSrc, [ paths.appPackageJson ]), ], }, resolveLoader: { plugins: [ + // Also related to Plug'n'Play, but this time it tells Webpack to load its loaders // from the current package. PnpWebpackPlugin.moduleLoader(module), @@ -288,6 +324,7 @@ module.exports = function(webpackEnv) { module: { strictExportPresence: true, rules: [ + // Disable require.ensure as it's not a standard language feature. { parser: { requireEnsure: false } }, @@ -301,7 +338,7 @@ module.exports = function(webpackEnv) { options: { formatter: require.resolve('react-dev-utils/eslintFormatter'), eslintPath: require.resolve('eslint'), - + }, loader: require.resolve('eslint-loader'), }, @@ -309,21 +346,24 @@ module.exports = function(webpackEnv) { 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 like "file" loader except that it embeds assets // smaller than specified limit in bytes as data URLs to avoid requests. // A missing `test` is equivalent to a match. { - test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], + test: [ /\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/ ], loader: require.resolve('url-loader'), options: { limit: 10000, name: 'static/media/[name].[hash:8].[ext]', }, }, + // Process application JS with Babel. // The preset includes JSX, Flow, TypeScript, and some ESnext features. { @@ -334,7 +374,7 @@ module.exports = function(webpackEnv) { customize: require.resolve( 'babel-preset-react-app/webpack-overrides' ), - + plugins: [ [ require.resolve('babel-plugin-named-asset-import'), @@ -348,6 +388,7 @@ module.exports = function(webpackEnv) { }, ], ], + // This is a feature of `babel-loader` for webpack (not Babel itself). // It enables caching results in ./node_modules/.cache/babel-loader/ // directory for faster rebuilds. @@ -356,6 +397,7 @@ module.exports = function(webpackEnv) { compact: isEnvProduction, }, }, + // Process any JS outside of the app with Babel. // Unlike the application JS, we only compile the standard ES features. { @@ -374,7 +416,7 @@ module.exports = function(webpackEnv) { ], cacheDirectory: true, cacheCompression: isEnvProduction, - + // If an error happens in a package, it's possible to be // because it was compiled. Thus, we don't want the browser // debugger to show the original code. Instead, the code @@ -382,6 +424,7 @@ module.exports = function(webpackEnv) { sourceMaps: false, }, }, + // "postcss" loader applies autoprefixer to our CSS. // "css" loader resolves paths in CSS and adds assets as dependencies. // "style" loader turns CSS into JS modules that inject