Skip to content

Commit f389e3d

Browse files
committed
Generate sourcemaps for production build artifacts
1 parent 9135f17 commit f389e3d

File tree

2 files changed

+138
-36
lines changed

2 files changed

+138
-36
lines changed

scripts/rollup/build.js

+122-30
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ 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');
1416
const argv = require('minimist')(process.argv.slice(2));
1517
const Modules = require('./modules');
@@ -148,6 +150,7 @@ function getBabelConfig(
148150
presets: [],
149151
plugins: [...babelPlugins],
150152
babelHelpers: 'bundled',
153+
sourcemap: false,
151154
};
152155
if (isDevelopment) {
153156
options.plugins.push(
@@ -381,6 +384,21 @@ function getPlugins(
381384

382385
const {isUMDBundle, shouldStayReadable} = getBundleTypeFlags(bundleType);
383386

387+
const needsMinifiedByClosure = isProduction && bundleType !== ESM_PROD;
388+
389+
// Only generate sourcemaps for true "production" build artifacts
390+
// that will be used by bundlers, such as `react-dom.production.min.js`.
391+
// UMD and "profiling" builds are rarely used and not worth having sourcemaps.
392+
const needsSourcemaps =
393+
needsMinifiedByClosure &&
394+
!isProfiling &&
395+
!isUMDBundle &&
396+
!shouldStayReadable;
397+
398+
// For builds with sourcemaps, capture the minified code Closure generated
399+
// so it can be used to help construct the final sourcemap contents.
400+
let chunkCodeAfterClosureCompiler = undefined;
401+
384402
return [
385403
// Keep dynamic imports as externals
386404
dynamicImports(),
@@ -390,7 +408,7 @@ function getPlugins(
390408
const transformed = flowRemoveTypes(code);
391409
return {
392410
code: transformed.toString(),
393-
map: transformed.generateMap(),
411+
map: null,
394412
};
395413
},
396414
},
@@ -419,6 +437,7 @@ function getPlugins(
419437
),
420438
// Remove 'use strict' from individual source files.
421439
{
440+
name: "remove 'use strict'",
422441
transform(source) {
423442
return source.replace(/['"]use strict["']/g, '');
424443
},
@@ -440,35 +459,44 @@ function getPlugins(
440459
isUMDBundle && entry === 'react-art' && commonjs(),
441460
// Apply dead code elimination and/or minification.
442461
// closure doesn't yet support leaving ESM imports intact
443-
isProduction &&
444-
bundleType !== ESM_PROD &&
445-
closure({
446-
compilation_level: 'SIMPLE',
447-
language_in: 'ECMASCRIPT_2020',
448-
language_out:
449-
bundleType === NODE_ES2015
450-
? 'ECMASCRIPT_2020'
451-
: bundleType === BROWSER_SCRIPT
452-
? 'ECMASCRIPT5'
453-
: 'ECMASCRIPT5_STRICT',
454-
emit_use_strict:
455-
bundleType !== BROWSER_SCRIPT &&
456-
bundleType !== ESM_PROD &&
457-
bundleType !== ESM_DEV,
458-
env: 'CUSTOM',
459-
warning_level: 'QUIET',
460-
apply_input_source_maps: false,
461-
use_types_for_optimization: false,
462-
process_common_js_modules: false,
463-
rewrite_polyfills: false,
464-
inject_libraries: false,
465-
allow_dynamic_import: true,
466-
467-
// Don't let it create global variables in the browser.
468-
// https://github.com/facebook/react/issues/10909
469-
assume_function_wrapper: !isUMDBundle,
470-
renaming: !shouldStayReadable,
471-
}),
462+
needsMinifiedByClosure &&
463+
closure(
464+
{
465+
compilation_level: 'SIMPLE',
466+
language_in: 'ECMASCRIPT_2020',
467+
language_out:
468+
bundleType === NODE_ES2015
469+
? 'ECMASCRIPT_2020'
470+
: bundleType === BROWSER_SCRIPT
471+
? 'ECMASCRIPT5'
472+
: 'ECMASCRIPT5_STRICT',
473+
emit_use_strict:
474+
bundleType !== BROWSER_SCRIPT &&
475+
bundleType !== ESM_PROD &&
476+
bundleType !== ESM_DEV,
477+
env: 'CUSTOM',
478+
warning_level: 'QUIET',
479+
source_map_include_content: true,
480+
use_types_for_optimization: false,
481+
process_common_js_modules: false,
482+
rewrite_polyfills: false,
483+
inject_libraries: false,
484+
allow_dynamic_import: true,
485+
486+
// Don't let it create global variables in the browser.
487+
// https://github.com/facebook/react/issues/10909
488+
assume_function_wrapper: !isUMDBundle,
489+
renaming: !shouldStayReadable,
490+
},
491+
{needsSourcemaps}
492+
),
493+
needsSourcemaps && {
494+
name: 'chunk-after-closure',
495+
renderChunk(code, config, options) {
496+
// Side effect - grab the code as Closure mangled it
497+
chunkCodeAfterClosureCompiler = code;
498+
},
499+
},
472500
// Add the whitespace back if necessary.
473501
shouldStayReadable &&
474502
prettier({
@@ -479,6 +507,7 @@ function getPlugins(
479507
}),
480508
// License and haste headers, top-level `if` blocks.
481509
{
510+
name: 'license-and-headers',
482511
renderChunk(source) {
483512
return Wrappers.wrapBundle(
484513
source,
@@ -490,6 +519,69 @@ function getPlugins(
490519
);
491520
},
492521
},
522+
needsSourcemaps && {
523+
name: 'generate-prod-bundle-sourcemaps',
524+
async renderChunk(codeAfterLicense, chunk, options, meta) {
525+
// We want to generate a sourcemap that shows the production bundle source
526+
// as it existed before Closure Compiler minified that chunk.
527+
// We also need to apply any license/wrapper text adjustments to that
528+
// sourcemap, so that the mapped locations line up correctly.
529+
530+
// We can split the final chunk code to figure out what got added around
531+
// the code from the Closure step.
532+
const [licensePrefix, licensePostfix] = codeAfterLicense.split(
533+
chunkCodeAfterClosureCompiler
534+
);
535+
536+
const transformedSource = new MagicString(
537+
chunkCodeAfterClosureCompiler
538+
);
539+
540+
// Apply changes so we can generate a sourcemap for this step
541+
if (licensePrefix) {
542+
transformedSource.prepend(licensePrefix);
543+
}
544+
545+
if (licensePostfix) {
546+
transformedSource.append(licensePostfix);
547+
}
548+
549+
// Use a path like `node_modules/react/cjs/react.production.min.js.map` for the sourcemap file
550+
const finalSourcemapPath = options.file.replace('.js', '.js.map');
551+
552+
// Read the sourcemap that Closure wrote to disk
553+
const sourcemapAfterClosure = JSON.parse(
554+
fs.readFileSync(finalSourcemapPath, 'utf8')
555+
);
556+
557+
// Use a name like `react.production.js` for the "pre-minified" sourcemap contents
558+
const fileWithoutMin = filename.replace('.min', '');
559+
560+
// CC generated a file list that only contains the tempfile name.
561+
// Replace that with a more meaningful "source" name for this bundle.
562+
sourcemapAfterClosure.sources = [fileWithoutMin];
563+
sourcemapAfterClosure.file = filename;
564+
565+
// Create an additional sourcemap adjusted for the license header contents
566+
const mapAfterLicense = transformedSource.generateMap({
567+
file: filename,
568+
includeContent: true,
569+
hires: true,
570+
});
571+
572+
// Merge the Closure sourcemap and the with-license sourcemap together
573+
const finalCombinedSourcemap = remapping(
574+
[mapAfterLicense, sourcemapAfterClosure],
575+
() => null
576+
);
577+
578+
// Overwrite the Closure-generated file with the final combined sourcemap
579+
fs.writeFileSync(
580+
finalSourcemapPath,
581+
JSON.stringify(finalCombinedSourcemap)
582+
);
583+
},
584+
},
493585
// Record bundle size.
494586
sizes({
495587
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)