diff --git a/packages/react-dev-utils/detectMissingVendors.js b/packages/react-dev-utils/detectMissingVendors.js new file mode 100644 index 00000000000..93978a29c69 --- /dev/null +++ b/packages/react-dev-utils/detectMissingVendors.js @@ -0,0 +1,34 @@ +// @remove-on-eject-begin +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +// @remove-on-eject-end +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); + +function detectMissingVendors(pathToPackageJson, pathToVendors) { + const packageJson = require(pathToPackageJson); + const dependencies = Object.keys(packageJson.dependencies || {}) + .concat(Object.keys(packageJson.devDependencies || {})) + .concat(Object.keys(packageJson.peerDependencies || {})); + const vendors = fs.existsSync(pathToVendors) ? require(pathToVendors) : []; + const missingVendors = vendors.filter( + vendor => dependencies.indexOf(vendor) === -1 + ); + if (missingVendors.length > 0) { + throw new Error( + 'Error: Unknown vendors: ' + + chalk.yellow(missingVendors) + + " should be listed in the project's dependencies.\n" + + `(Vendors defined in '${path.resolve(pathToVendors)}')` + ); + } +} + +module.exports = detectMissingVendors; diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 1961bf1fc8d..a830f2b0794 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -16,6 +16,7 @@ "clearConsole.js", "crashOverlay.js", "crossSpawn.js", + "detectMissingVendors.js", "eslintFormatter.js", "errorOverlayMiddleware.js", "FileSizeReporter.js", diff --git a/packages/react-scripts/config/paths.js b/packages/react-scripts/config/paths.js index b9dd95051f2..c532d5e9689 100644 --- a/packages/react-scripts/config/paths.js +++ b/packages/react-scripts/config/paths.js @@ -54,6 +54,7 @@ module.exports = { appPublic: resolveApp('public'), appHtml: resolveApp('public/index.html'), appIndexJs: resolveApp('src/index.js'), + appVendors: resolveApp('src/vendors.json'), appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), yarnLockFile: resolveApp('yarn.lock'), @@ -74,6 +75,7 @@ module.exports = { appPublic: resolveApp('public'), appHtml: resolveApp('public/index.html'), appIndexJs: resolveApp('src/index.js'), + appVendors: resolveApp('src/vendors.json'), appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), yarnLockFile: resolveApp('yarn.lock'), @@ -104,6 +106,7 @@ if ( appPublic: resolveOwn('template/public'), appHtml: resolveOwn('template/public/index.html'), appIndexJs: resolveOwn('template/src/index.js'), + appVendors: resolveOwn('template/src/vendors.json'), appPackageJson: resolveOwn('package.json'), appSrc: resolveOwn('template/src'), yarnLockFile: resolveOwn('template/yarn.lock'), diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index c0c1071cbd0..662de59fcb3 100644 --- a/packages/react-scripts/config/webpack.config.prod.js +++ b/packages/react-scripts/config/webpack.config.prod.js @@ -19,6 +19,9 @@ const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin'); const eslintFormatter = require('react-dev-utils/eslintFormatter'); const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); +const NameAllModulesPlugin = require('name-all-modules-plugin'); + +const fs = require('fs'); const paths = require('./paths'); const getClientEnvironment = require('./env'); @@ -79,8 +82,20 @@ module.exports = { // 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: [require.resolve('./polyfills'), paths.appIndexJs], + // In production, we only want to load the polyfills, the app code and the vendors. + entry: Object.assign( + { + // Load the app and all its dependencies + main: paths.appIndexJs, + // Add the polyfills + polyfills: require.resolve('./polyfills'), + }, + // Only add the vendors if the file "src/vendors.js" exists + fs.existsSync(paths.appVendors) + ? // List of all the node modules that should be excluded from the app + { vendors: require(paths.appVendors) } + : {} + ), output: { // The build folder. path: paths.appBuild, @@ -455,7 +470,51 @@ module.exports = { // 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$/), - ], + + // For some reason, Webpack adds some ids of all the modules that exist to our vendor chunk + // Instead of using numerical ids it uses a unique path to map our request to a module. + // Thanks to this change the vendor hash will now always stay the same + new webpack.NamedModulesPlugin(), + + // Ensure that every chunks have an actual name and not an id + // If the chunk has a name, this name is used + // otherwise the name of the file is used + new webpack.NamedChunksPlugin(chunk => { + if (chunk.name) { + return chunk.name; + } + const chunkNames = chunk.mapModules(m => m); + // Sort the chunks by their depths + // The chunk with the lower depth is the imported one + // The others are its dependencies + chunkNames.sort((chunkA, chunkB) => chunkA.depth - chunkB.depth); + // Get the absolute path of the file + const fileName = chunkNames[0].resource; + // Return the name of the file without the extension + return path.basename(fileName, path.extname(fileName)); + }), + + // Avoid having the vendors in the rest of the app + // Only execute if the vendors file exists + + fs.existsSync(paths.appVendors) + ? new webpack.optimize.CommonsChunkPlugin({ + name: 'vendors', + minChunks: Infinity, + }) + : null, + // The runtime is the part of Webpack that resolves modules + // at runtime and handles async loading and more + new webpack.optimize.CommonsChunkPlugin({ + name: 'runtime', + }), + + // https://medium.com/webpack/predictable-long-term-caching-with-webpack-d3eee1d3fa31 + // Name the modules that were not named by the previous plugins + new NameAllModulesPlugin(), + ] + // Remove null elements + .filter(Boolean), // 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: { diff --git a/packages/react-scripts/fixtures/kitchensink/src/App.js b/packages/react-scripts/fixtures/kitchensink/src/App.js index d1affb48af9..36bb95feb95 100644 --- a/packages/react-scripts/fixtures/kitchensink/src/App.js +++ b/packages/react-scripts/fixtures/kitchensink/src/App.js @@ -82,9 +82,9 @@ class App extends Component { ); break; case 'css-modules-inclusion': - import( - './features/webpack/CssModulesInclusion' - ).then(f => this.setFeature(f.default)); + import('./features/webpack/CssModulesInclusion').then(f => + this.setFeature(f.default) + ); break; case 'custom-interpolation': import('./features/syntax/CustomInterpolation').then(f => diff --git a/packages/react-scripts/fixtures/kitchensink/src/vendors.json b/packages/react-scripts/fixtures/kitchensink/src/vendors.json new file mode 100644 index 00000000000..31a306a1686 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/vendors.json @@ -0,0 +1 @@ +["react", "react-dom"] diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 3c670a23dd6..1d55deef845 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -10,13 +10,7 @@ "bugs": { "url": "https://github.com/facebookincubator/create-react-app/issues" }, - "files": [ - "bin", - "config", - "scripts", - "template", - "utils" - ], + "files": ["bin", "config", "scripts", "template", "utils"], "bin": { "react-scripts": "./bin/react-scripts.js" }, @@ -47,6 +41,7 @@ "html-webpack-plugin": "2.30.1", "identity-obj-proxy": "3.0.0", "jest": "22.1.2", + "name-all-modules-plugin": "1.0.1", "object-assign": "4.1.1", "postcss-flexbugs-fixes": "3.2.0", "postcss-loader": "2.0.10", @@ -77,11 +72,6 @@ "last 2 firefox versions", "last 2 edge versions" ], - "production": [ - ">1%", - "last 4 versions", - "Firefox ESR", - "not ie < 11" - ] + "production": [">1%", "last 4 versions", "Firefox ESR", "not ie < 11"] } } diff --git a/packages/react-scripts/scripts/build.js b/packages/react-scripts/scripts/build.js index cca5ff28313..2a76ec164d3 100644 --- a/packages/react-scripts/scripts/build.js +++ b/packages/react-scripts/scripts/build.js @@ -36,6 +36,7 @@ const webpack = require('webpack'); const config = require('../config/webpack.config.prod'); const paths = require('../config/paths'); const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); +const detectMissingVendors = require('react-dev-utils/detectMissingVendors'); const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); @@ -71,6 +72,14 @@ checkBrowsers(paths.appPath) fs.emptyDirSync(paths.appBuild); // Merge with the public folder copyPublicFolder(); + + // Check if every vendors are defined in the package.json + // @remove-on-eject-begin + // The devDepencencies shouldn't be available for vendors + // But otherwise the tests fail. + // @remove-on-eject-end + detectMissingVendors(paths.appPackageJson, paths.appVendors); + // Start the webpack build return build(previousFileSizes); }) diff --git a/packages/react-scripts/template/src/vendors.json b/packages/react-scripts/template/src/vendors.json new file mode 100644 index 00000000000..31a306a1686 --- /dev/null +++ b/packages/react-scripts/template/src/vendors.json @@ -0,0 +1 @@ +["react", "react-dom"]