Skip to content

Commit fc846c2

Browse files
committed
Generate sourcemaps for production build artifacts
1 parent cc33448 commit fc846c2

File tree

2 files changed

+154
-36
lines changed

2 files changed

+154
-36
lines changed

scripts/rollup/build.js

+138-30
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ const replace = require('@rollup/plugin-replace');
1010
const stripBanner = require('rollup-plugin-strip-banner');
1111
const chalk = require('chalk');
1212
const resolve = require('@rollup/plugin-node-resolve').nodeResolve;
13+
const MagicString = require('magic-string');
14+
const remapping = require('@ampproject/remapping');
1315
const fs = require('fs');
16+
const path = require('path');
1417
const argv = require('minimist')(process.argv.slice(2));
1518
const Modules = require('./modules');
1619
const Bundles = require('./bundles');
@@ -148,6 +151,7 @@ function getBabelConfig(
148151
presets: [],
149152
plugins: [...babelPlugins],
150153
babelHelpers: 'bundled',
154+
sourcemap: false,
151155
};
152156
if (isDevelopment) {
153157
options.plugins.push(
@@ -382,6 +386,29 @@ function getPlugins(
382386

383387
const {isUMDBundle, shouldStayReadable} = getBundleTypeFlags(bundleType);
384388

389+
const needsMinifiedByClosure = isProduction && bundleType !== ESM_PROD;
390+
391+
// Any other packages that should specifically _not_ have sourcemaps
392+
const sourcemapPackageExcludes = [
393+
// Having `//#sourceMappingUrl` in this file breaks `ReactDevToolsHooksIntegration-test.js`,
394+
// and this is an internal
395+
'react-debug-tools',
396+
];
397+
398+
// Only generate sourcemaps for true "production" build artifacts
399+
// that will be used by bundlers, such as `react-dom.production.min.js`.
400+
// UMD and "profiling" builds are rarely used and not worth having sourcemaps.
401+
const needsSourcemaps =
402+
needsMinifiedByClosure &&
403+
!isProfiling &&
404+
!isUMDBundle &&
405+
!sourcemapPackageExcludes.includes(entry) &&
406+
!shouldStayReadable;
407+
408+
// For builds with sourcemaps, capture the minified code Closure generated
409+
// so it can be used to help construct the final sourcemap contents.
410+
let chunkCodeAfterClosureCompiler = undefined;
411+
385412
return [
386413
// Keep dynamic imports as externals
387414
dynamicImports(),
@@ -391,7 +418,7 @@ function getPlugins(
391418
const transformed = flowRemoveTypes(code);
392419
return {
393420
code: transformed.toString(),
394-
map: transformed.generateMap(),
421+
map: null,
395422
};
396423
},
397424
},
@@ -420,6 +447,7 @@ function getPlugins(
420447
),
421448
// Remove 'use strict' from individual source files.
422449
{
450+
name: "remove 'use strict'",
423451
transform(source) {
424452
return source.replace(/['"]use strict["']/g, '');
425453
},
@@ -441,35 +469,44 @@ function getPlugins(
441469
isUMDBundle && entry === 'react-art' && commonjs(),
442470
// Apply dead code elimination and/or minification.
443471
// closure doesn't yet support leaving ESM imports intact
444-
isProduction &&
445-
bundleType !== ESM_PROD &&
446-
closure({
447-
compilation_level: 'SIMPLE',
448-
language_in: 'ECMASCRIPT_2020',
449-
language_out:
450-
bundleType === NODE_ES2015
451-
? 'ECMASCRIPT_2020'
452-
: bundleType === BROWSER_SCRIPT
453-
? 'ECMASCRIPT5'
454-
: 'ECMASCRIPT5_STRICT',
455-
emit_use_strict:
456-
bundleType !== BROWSER_SCRIPT &&
457-
bundleType !== ESM_PROD &&
458-
bundleType !== ESM_DEV,
459-
env: 'CUSTOM',
460-
warning_level: 'QUIET',
461-
apply_input_source_maps: false,
462-
use_types_for_optimization: false,
463-
process_common_js_modules: false,
464-
rewrite_polyfills: false,
465-
inject_libraries: false,
466-
allow_dynamic_import: true,
467-
468-
// Don't let it create global variables in the browser.
469-
// https://github.com/facebook/react/issues/10909
470-
assume_function_wrapper: !isUMDBundle,
471-
renaming: !shouldStayReadable,
472-
}),
472+
needsMinifiedByClosure &&
473+
closure(
474+
{
475+
compilation_level: 'SIMPLE',
476+
language_in: 'ECMASCRIPT_2020',
477+
language_out:
478+
bundleType === NODE_ES2015
479+
? 'ECMASCRIPT_2020'
480+
: bundleType === BROWSER_SCRIPT
481+
? 'ECMASCRIPT5'
482+
: 'ECMASCRIPT5_STRICT',
483+
emit_use_strict:
484+
bundleType !== BROWSER_SCRIPT &&
485+
bundleType !== ESM_PROD &&
486+
bundleType !== ESM_DEV,
487+
env: 'CUSTOM',
488+
warning_level: 'QUIET',
489+
source_map_include_content: true,
490+
use_types_for_optimization: false,
491+
process_common_js_modules: false,
492+
rewrite_polyfills: false,
493+
inject_libraries: false,
494+
allow_dynamic_import: true,
495+
496+
// Don't let it create global variables in the browser.
497+
// https://github.com/facebook/react/issues/10909
498+
assume_function_wrapper: !isUMDBundle,
499+
renaming: !shouldStayReadable,
500+
},
501+
{needsSourcemaps}
502+
),
503+
needsSourcemaps && {
504+
name: 'chunk-after-closure',
505+
renderChunk(code, config, options) {
506+
// Side effect - grab the code as Closure mangled it
507+
chunkCodeAfterClosureCompiler = code;
508+
},
509+
},
473510
// Add the whitespace back if necessary.
474511
shouldStayReadable &&
475512
prettier({
@@ -480,6 +517,7 @@ function getPlugins(
480517
}),
481518
// License and haste headers, top-level `if` blocks.
482519
{
520+
name: 'license-and-headers',
483521
renderChunk(source) {
484522
return Wrappers.wrapBundle(
485523
source,
@@ -491,6 +529,76 @@ function getPlugins(
491529
);
492530
},
493531
},
532+
needsSourcemaps && {
533+
name: 'generate-prod-bundle-sourcemaps',
534+
async renderChunk(codeAfterLicense, chunk, options, meta) {
535+
// We want to generate a sourcemap that shows the production bundle source
536+
// as it existed before Closure Compiler minified that chunk.
537+
// We also need to apply any license/wrapper text adjustments to that
538+
// sourcemap, so that the mapped locations line up correctly.
539+
540+
// We can split the final chunk code to figure out what got added around
541+
// the code from the Closure step.
542+
const [licensePrefix, licensePostfix] = codeAfterLicense.split(
543+
chunkCodeAfterClosureCompiler
544+
);
545+
546+
const transformedSource = new MagicString(
547+
chunkCodeAfterClosureCompiler
548+
);
549+
550+
// Apply changes so we can generate a sourcemap for this step
551+
if (licensePrefix) {
552+
transformedSource.prepend(licensePrefix);
553+
}
554+
555+
if (licensePostfix) {
556+
transformedSource.append(licensePostfix);
557+
}
558+
559+
// Use a path like `node_modules/react/cjs/react.production.min.js.map` for the sourcemap file
560+
const finalSourcemapPath = options.file.replace('.js', '.js.map');
561+
const finalSourcemapFilename = path.basename(finalSourcemapPath);
562+
563+
// Read the sourcemap that Closure wrote to disk
564+
const sourcemapAfterClosure = JSON.parse(
565+
fs.readFileSync(finalSourcemapPath, 'utf8')
566+
);
567+
568+
// CC generated a file list that only contains the tempfile name.
569+
// Replace that with a more meaningful "source" name for this bundle.
570+
sourcemapAfterClosure.sources = [filename];
571+
sourcemapAfterClosure.file = filename;
572+
573+
// Create an additional sourcemap adjusted for the license header contents
574+
const mapAfterLicense = transformedSource.generateMap({
575+
file: filename,
576+
includeContent: true,
577+
hires: true,
578+
});
579+
580+
// Merge the Closure sourcemap and the with-license sourcemap together
581+
const finalCombinedSourcemap = remapping(
582+
[mapAfterLicense, sourcemapAfterClosure],
583+
() => null
584+
);
585+
586+
// Overwrite the Closure-generated file with the final combined sourcemap
587+
fs.writeFileSync(
588+
finalSourcemapPath,
589+
JSON.stringify(finalCombinedSourcemap)
590+
);
591+
592+
// Add the sourcemap URL to the actual bundle, so that tools pick it up
593+
const sourceWithMappingUrl =
594+
codeAfterLicense + `\n//# sourceMappingURL=${finalSourcemapFilename}`;
595+
596+
return {
597+
code: sourceWithMappingUrl,
598+
map: null,
599+
};
600+
},
601+
},
494602
// Record bundle size.
495603
sizes({
496604
getSize: (size, gzip) => {

scripts/rollup/plugins/closure-plugin.js

+16-6
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,25 @@ function compile(flags) {
1919
});
2020
}
2121

22-
module.exports = function closure(flags = {}) {
22+
module.exports = function closure(flags = {}, {needsSourcemaps}) {
2323
return {
2424
name: 'scripts/rollup/plugins/closure-plugin',
25-
async renderChunk(code) {
25+
async renderChunk(code, chunk, options) {
2626
const inputFile = tmp.fileSync();
27-
const tempPath = inputFile.name;
28-
flags = Object.assign({}, flags, {js: tempPath});
29-
await writeFileAsync(tempPath, code, 'utf8');
30-
const compiledCode = await compile(flags);
27+
28+
// Use a path like `node_modules/react/cjs/react.production.min.js.map` for the sourcemap file
29+
const sourcemapPath = options.file.replace('.js', '.js.map');
30+
31+
// Tell Closure what JS source file to read, and optionally what sourcemap file to write
32+
const finalFlags = {
33+
...flags,
34+
js: inputFile.name,
35+
...(needsSourcemaps && {create_source_map: sourcemapPath}),
36+
};
37+
38+
await writeFileAsync(inputFile.name, code, 'utf8');
39+
const compiledCode = await compile(finalFlags);
40+
3141
inputFile.removeCallback();
3242
return {code: compiledCode};
3343
},

0 commit comments

Comments
 (0)