diff --git a/packages/babel-preset-react-app/create.js b/packages/babel-preset-react-app/create.js index 74514baa65..eecb91cf0d 100644 --- a/packages/babel-preset-react-app/create.js +++ b/packages/babel-preset-react-app/create.js @@ -20,6 +20,9 @@ const validateBoolOption = (name, value, defaultValue) => { return value; }; +const legacyTargets = { ie: 9 }; +const modernTargets = { esmodules: true }; + module.exports = function(api, opts, env) { if (!opts) { opts = {}; @@ -47,6 +50,13 @@ module.exports = function(api, opts, env) { true ); + var isModern = validateBoolOption('modern', opts.modern, false); + var shouldBuildModern = validateBoolOption( + 'shouldBuildModernAndLegacy', + opts.shouldBuildModernAndLegacy, + false + ); + var absoluteRuntimePath = undefined; if (useAbsoluteRuntime) { absoluteRuntimePath = path.dirname( @@ -79,8 +89,15 @@ module.exports = function(api, opts, env) { // Latest stable ECMAScript features require('@babel/preset-env').default, { - // Allow importing @babel/polyfill in entrypoint and use browserlist to select polyfills - useBuiltIns: 'entry', + // When building normal we take the build we're in being modern or legacy + // If not we respect the users browserslist. + targets: shouldBuildModern + ? isModern + ? modernTargets + : legacyTargets + : undefined, + ignoreBrowserslistConfig: shouldBuildModern, + useBuiltIns: isModern ? 'usage' : 'entry', // Do not transform modules to CJS modules: false, // Exclude transforms that make all code slower diff --git a/packages/babel-preset-react-app/dependencies.js b/packages/babel-preset-react-app/dependencies.js index 60c76fb5b3..143a27a3fc 100644 --- a/packages/babel-preset-react-app/dependencies.js +++ b/packages/babel-preset-react-app/dependencies.js @@ -20,6 +20,9 @@ const validateBoolOption = (name, value, defaultValue) => { return value; }; +const legacyTargets = { ie: 9 }; +const modernTargets = { esmodules: true }; + module.exports = function(api, opts) { if (!opts) { opts = {}; @@ -60,6 +63,13 @@ module.exports = function(api, opts) { ); } + var isModern = validateBoolOption('modern', opts.modern, false); + var shouldBuildModern = validateBoolOption( + 'shouldBuildModernAndLegacy', + opts.shouldBuildModernAndLegacy, + false + ); + return { // Babel assumes ES Modules, which isn't safe until CommonJS // dies. This changes the behavior to assume CommonJS unless @@ -86,15 +96,15 @@ module.exports = function(api, opts) { { // We want Create React App to be IE 9 compatible until React itself // no longer works with IE 9 - targets: { - ie: 9, - }, + targets: shouldBuildModern + ? isModern + ? modernTargets + : legacyTargets + : undefined, // Users cannot override this behavior because this Babel // configuration is highly tuned for ES5 support ignoreBrowserslistConfig: true, - // If users import all core-js they're probably not concerned with - // bundle size. We shouldn't rely on magic to try and shrink it. - useBuiltIns: false, + useBuiltIns: 'entry', // Do not transform modules to CJS modules: false, // Exclude transforms that make all code slower diff --git a/packages/react-dev-utils/HtmlWebpackEsModulesPlugin.js b/packages/react-dev-utils/HtmlWebpackEsModulesPlugin.js new file mode 100644 index 0000000000..42473e1cf2 --- /dev/null +++ b/packages/react-dev-utils/HtmlWebpackEsModulesPlugin.js @@ -0,0 +1,71 @@ +'use strict'; + +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const fs = require('fs-extra'); + +const ID = 'html-webpack-esmodules-plugin'; + +const safariFix = `(function(){var d=document;var c=d.createElement('script');if(!('noModule' in c)&&'onbeforeload' in c){var s=!1;d.addEventListener('beforeload',function(e){if(e.target===c){s=!0}else if(!e.target.hasAttribute('nomodule')||!s){return}e.preventDefault()},!0);c.type='module';c.src='.';d.head.appendChild(c);c.remove()}}())`; + +class HtmlWebpackEsmodulesPlugin { + constructor() {} + + apply(compiler) { + compiler.hooks.compilation.tap(ID, compilation => { + HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync( + ID, + ({ plugin, bodyTags: body }, cb) => { + const targetDir = compiler.options.output.path; + // get stats, write to disk + const htmlName = path.basename(plugin.options.filename); + // Watch out for output files in sub directories + const htmlPath = path.dirname(plugin.options.filename); + const tempFilename = path.join( + targetDir, + htmlPath, + `assets-${htmlName}.json` + ); + + if (!fs.existsSync(tempFilename)) { + fs.mkdirpSync(path.dirname(tempFilename)); + const newBody = body.filter( + a => a.tagName === 'script' && a.attributes + ); + newBody.forEach(a => (a.attributes.nomodule = '')); + fs.writeFileSync(tempFilename, JSON.stringify(newBody)); + return cb(); + } + + const legacyAssets = JSON.parse( + fs.readFileSync(tempFilename, 'utf-8') + ); + // TODO: to discuss, an improvement would be to + // Inject these into the head tag together with the + // Safari script. + body.forEach(tag => { + if (tag.tagName === 'script' && tag.attributes) { + tag.attributes.type = 'module'; + } + }); + + body.push({ + tagName: 'script', + closeTag: true, + innerHTML: safariFix, + }); + + body.push(...legacyAssets); + fs.removeSync(tempFilename); + cb(); + } + ); + + HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tap(ID, data => { + data.html = data.html.replace(/\snomodule="">/g, ' nomodule>'); + }); + }); + } +} + +module.exports = HtmlWebpackEsmodulesPlugin; diff --git a/packages/react-dev-utils/README.md b/packages/react-dev-utils/README.md index d89dc95f66..6e6b0242d0 100644 --- a/packages/react-dev-utils/README.md +++ b/packages/react-dev-utils/README.md @@ -56,6 +56,38 @@ module.exports = { }; ``` +#### `new HtmlWebpackEsModulesPlugin()` + +This Webpack plugin allows you to build two builds, the first one a legacy build and the +second being a modern one. +This plugin is used to use the first build as a buffer and writes it to a temporairy file, +then when the second build completes it merges the two and adds a shim to support safari. + +This way of working relies on [module-nomodule differential serving](https://jakearchibald.com/2017/es-modules-in-browsers/). + +```js +var path = require('path'); +var HtmlWebpackPlugin = require('html-webpack-plugin'); +var HtmlWebpackEsModulesPlugin = require('react-dev-utils/HtmlWebpackEsModulesPlugin'); + +module.exports = { + output: { + // ... + }, + // ... + plugins: [ + // Generates an `index.html` file with the