Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: differential serving allow modern ES2015 and legacy ES5 build #6590

Closed
wants to merge 15 commits into from
18 changes: 17 additions & 1 deletion packages/babel-preset-react-app/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -47,6 +50,13 @@ module.exports = function(api, opts, env) {
true
);

var isModern = validateBoolOption('modern', opts.modern, false);
var shouldBuildModern = validateBoolOption(
'shouldBuildModernAndLegacy',
opts.shouldBuildModernAndLegacy,
true
JoviDeCroock marked this conversation as resolved.
Show resolved Hide resolved
);

var absoluteRuntimePath = undefined;
if (useAbsoluteRuntime) {
absoluteRuntimePath = path.dirname(
Expand Down Expand Up @@ -79,7 +89,13 @@ 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
// 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,
useBuiltIns: 'entry',
// Do not transform modules to CJS
modules: false,
Expand Down
1 change: 1 addition & 0 deletions packages/create-react-app/createReactApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const program = new commander.Command(packageJson.name)
)
.option('--use-npm')
.option('--use-pnp')
.option('--modern')
ianschmitz marked this conversation as resolved.
Show resolved Hide resolved
.option('--typescript')
.allowUnknownOption()
.on('--help', () => {
Expand Down
71 changes: 71 additions & 0 deletions packages/react-dev-utils/HtmlWebpackEsModulesPlugin.js
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions packages/react-dev-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"getCSSModuleLocalIdent.js",
"getProcessForPort.js",
"globby.js",
"HtmlWebpackEsModulesPlugin.js",
"ignoredFiles.js",
"immer.js",
"InlineChunkHtmlPlugin.js",
Expand Down
26 changes: 20 additions & 6 deletions packages/react-scripts/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const getClientEnvironment = require('./env');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
const HtmlWebpackEsModulesPlugin = require('react-dev-utils/HtmlWebpackEsModulesPlugin');
// @remove-on-eject-begin
const getCacheIdentifier = require('react-dev-utils/getCacheIdentifier');
// @remove-on-eject-end
Expand All @@ -52,7 +53,10 @@ 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) {
module.exports = function(
webpackEnv,
{ shouldBuildModernAndLegacy, isModernOutput } = {}
) {
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';

Expand Down Expand Up @@ -161,11 +165,15 @@ module.exports = function(webpackEnv) {
// 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].[contenthash:8].js'
? `static/js/[name].[contenthash:8]${
isModernOutput ? '.modern' : ''
}.js`
: isEnvDevelopment && 'static/js/bundle.js',
// There are also additional JS chunk files if you use code splitting.
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
? `static/js/[name].[contenthash:8]${
isModernOutput ? '.modern' : ''
}.chunk.js`
: isEnvDevelopment && 'static/js/[name].chunk.js',
// We inferred the "public path" (such as / or /my-project) from homepage.
// We use "/" in development.
Expand Down Expand Up @@ -194,7 +202,7 @@ module.exports = function(webpackEnv) {
ecma: 8,
},
compress: {
ecma: 5,
ecma: isModernOutput ? 6 : 5,
warnings: false,
// Disabled because of an issue with Uglify breaking seemingly valid code:
// https://github.com/facebook/create-react-app/issues/2376
Expand All @@ -211,7 +219,7 @@ module.exports = function(webpackEnv) {
safari10: true,
},
output: {
ecma: 5,
ecma: isModernOutput ? 6 : 5,
comments: false,
// Turned on because emoji and regex is not minified properly using default
// https://github.com/facebook/create-react-app/issues/2488
Expand Down Expand Up @@ -353,7 +361,12 @@ module.exports = function(webpackEnv) {
// @remove-on-eject-begin
babelrc: false,
configFile: false,
presets: [require.resolve('babel-preset-react-app')],
presets: [
[
require.resolve('babel-preset-react-app'),
{ modern: isModernOutput, shouldBuildModernAndLegacy },
],
],
// Make sure we have a unique cache identifier, erring on the
// side of caution.
// We remove this when the user ejects because the default
Expand Down Expand Up @@ -542,6 +555,7 @@ module.exports = function(webpackEnv) {
: undefined
)
),
shouldBuildModernAndLegacy && new HtmlWebpackEsModulesPlugin(),
// Inlines the webpack runtime script. This script is too small to warrant
// a network request.
isEnvProduction &&
Expand Down
15 changes: 12 additions & 3 deletions packages/react-scripts/scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,17 @@ if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
// Process CLI arguments
const argv = process.argv.slice(2);
const writeStatsJson = argv.indexOf('--stats') !== -1;
const buildModern = argv.indexOf('--modern') !== -1;

// Generate configuration
const config = configFactory('production');
const modernConfig = configFactory('production', {
shouldBuildModernAndLegacy: buildModern,
isModernOutput: true,
});
const leagcyConfig = configFactory('production', {
shouldBuildModernAndLegacy: buildModern,
isModernOutput: false,
});

// We require that you explicitly set browsers and do not fall back to
// browserslist defaults.
Expand Down Expand Up @@ -116,7 +124,7 @@ checkBrowsers(paths.appPath, isInteractive)

const appPackage = require(paths.appPackageJson);
const publicUrl = paths.publicUrl;
const publicPath = config.output.publicPath;
const publicPath = leagcyConfig.output.publicPath;
const buildFolder = path.relative(process.cwd(), paths.appBuild);
printHostingInstructions(
appPackage,
Expand All @@ -142,8 +150,9 @@ checkBrowsers(paths.appPath, isInteractive)
// Create the production build and print the deployment instructions.
function build(previousFileSizes) {
console.log('Creating an optimized production build...');
const configs = [buildModern && modernConfig, leagcyConfig].filter(Boolean);

const compiler = webpack(config);
const compiler = webpack(configs);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
let messages;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ const immer = require('react-dev-utils/immer').produce;
const globby = require('react-dev-utils/globby').sync;

function writeJson(fileName, object) {
fs.writeFileSync(fileName, JSON.stringify(object, null, 2).replace(/\n/g, os.EOL) + os.EOL);
fs.writeFileSync(
fileName,
JSON.stringify(object, null, 2).replace(/\n/g, os.EOL) + os.EOL
);
}

function verifyNoTypeScript() {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-scripts/template/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ const App = () => {
</header>
</div>
);
}
};

export default App;