Skip to content

Commit c55dbf9

Browse files
committed
Generate sourcemaps for production build artifacts
1 parent 8677c3a commit c55dbf9

File tree

2 files changed

+146
-36
lines changed

2 files changed

+146
-36
lines changed

scripts/rollup/build.js

Lines changed: 130 additions & 30 deletions
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,21 @@ function getPlugins(
382386

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

389+
const needsMinifiedByClosure = isProduction && bundleType !== ESM_PROD;
390+
391+
// Only generate sourcemaps for true "production" build artifacts
392+
// that will be used by bundlers, such as `react-dom.production.min.js`.
393+
// UMD and "profiling" builds are rarely used and not worth having sourcemaps.
394+
const needsSourcemaps =
395+
needsMinifiedByClosure &&
396+
!isProfiling &&
397+
!isUMDBundle &&
398+
!shouldStayReadable;
399+
400+
// For builds with sourcemaps, capture the minified code Closure generated
401+
// so it can be used to help construct the final sourcemap contents.
402+
let chunkCodeAfterClosureCompiler = undefined;
403+
385404
return [
386405
// Keep dynamic imports as externals
387406
dynamicImports(),
@@ -391,7 +410,7 @@ function getPlugins(
391410
const transformed = flowRemoveTypes(code);
392411
return {
393412
code: transformed.toString(),
394-
map: transformed.generateMap(),
413+
map: null,
395414
};
396415
},
397416
},
@@ -420,6 +439,7 @@ function getPlugins(
420439
),
421440
// Remove 'use strict' from individual source files.
422441
{
442+
name: "remove 'use strict'",
423443
transform(source) {
424444
return source.replace(/['"]use strict["']/g, '');
425445
},
@@ -441,35 +461,44 @@ function getPlugins(
441461
isUMDBundle && entry === 'react-art' && commonjs(),
442462
// Apply dead code elimination and/or minification.
443463
// 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-
}),
464+
needsMinifiedByClosure &&
465+
closure(
466+
{
467+
compilation_level: 'SIMPLE',
468+
language_in: 'ECMASCRIPT_2020',
469+
language_out:
470+
bundleType === NODE_ES2015
471+
? 'ECMASCRIPT_2020'
472+
: bundleType === BROWSER_SCRIPT
473+
? 'ECMASCRIPT5'
474+
: 'ECMASCRIPT5_STRICT',
475+
emit_use_strict:
476+
bundleType !== BROWSER_SCRIPT &&
477+
bundleType !== ESM_PROD &&
478+
bundleType !== ESM_DEV,
479+
env: 'CUSTOM',
480+
warning_level: 'QUIET',
481+
source_map_include_content: true,
482+
use_types_for_optimization: false,
483+
process_common_js_modules: false,
484+
rewrite_polyfills: false,
485+
inject_libraries: false,
486+
allow_dynamic_import: true,
487+
488+
// Don't let it create global variables in the browser.
489+
// https://github.com/facebook/react/issues/10909
490+
assume_function_wrapper: !isUMDBundle,
491+
renaming: !shouldStayReadable,
492+
},
493+
{needsSourcemaps}
494+
),
495+
needsSourcemaps && {
496+
name: 'chunk-after-closure',
497+
renderChunk(code, config, options) {
498+
// Side effect - grab the code as Closure mangled it
499+
chunkCodeAfterClosureCompiler = code;
500+
},
501+
},
473502
// Add the whitespace back if necessary.
474503
shouldStayReadable &&
475504
prettier({
@@ -480,6 +509,7 @@ function getPlugins(
480509
}),
481510
// License and haste headers, top-level `if` blocks.
482511
{
512+
name: 'license-and-headers',
483513
renderChunk(source) {
484514
return Wrappers.wrapBundle(
485515
source,
@@ -491,6 +521,76 @@ function getPlugins(
491521
);
492522
},
493523
},
524+
needsSourcemaps && {
525+
name: 'generate-prod-bundle-sourcemaps',
526+
async renderChunk(codeAfterLicense, chunk, options, meta) {
527+
// We want to generate a sourcemap that shows the production bundle source
528+
// as it existed before Closure Compiler minified that chunk.
529+
// We also need to apply any license/wrapper text adjustments to that
530+
// sourcemap, so that the mapped locations line up correctly.
531+
532+
// We can split the final chunk code to figure out what got added around
533+
// the code from the Closure step.
534+
const [licensePrefix, licensePostfix] = codeAfterLicense.split(
535+
chunkCodeAfterClosureCompiler
536+
);
537+
538+
const transformedSource = new MagicString(
539+
chunkCodeAfterClosureCompiler
540+
);
541+
542+
// Apply changes so we can generate a sourcemap for this step
543+
if (licensePrefix) {
544+
transformedSource.prepend(licensePrefix);
545+
}
546+
547+
if (licensePostfix) {
548+
transformedSource.append(licensePostfix);
549+
}
550+
551+
// Use a path like `node_modules/react/cjs/react.production.min.js.map` for the sourcemap file
552+
const finalSourcemapPath = options.file.replace('.js', '.js.map');
553+
const finalSourcemapFilename = path.basename(finalSourcemapPath);
554+
555+
// Read the sourcemap that Closure wrote to disk
556+
const sourcemapAfterClosure = JSON.parse(
557+
fs.readFileSync(finalSourcemapPath, 'utf8')
558+
);
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 = [filename];
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+
// Add the sourcemap URL to the actual bundle, so that tools pick it up
585+
const sourceWithMappingUrl =
586+
codeAfterLicense + `\n//# sourceMappingURL=${finalSourcemapFilename}`;
587+
588+
return {
589+
code: sourceWithMappingUrl,
590+
map: null,
591+
};
592+
},
593+
},
494594
// Record bundle size.
495595
sizes({
496596
getSize: (size, gzip) => {

scripts/rollup/plugins/closure-plugin.js

Lines changed: 16 additions & 6 deletions
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)