Skip to content

Commit

Permalink
Generate sourcemaps for production build artifacts
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson committed Mar 20, 2023
1 parent 9135f17 commit f389e3d
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 36 deletions.
152 changes: 122 additions & 30 deletions scripts/rollup/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const replace = require('@rollup/plugin-replace');
const stripBanner = require('rollup-plugin-strip-banner');
const chalk = require('chalk');
const resolve = require('@rollup/plugin-node-resolve').nodeResolve;
const MagicString = require('magic-string');
const remapping = require('@ampproject/remapping');
const fs = require('fs');
const argv = require('minimist')(process.argv.slice(2));
const Modules = require('./modules');
Expand Down Expand Up @@ -148,6 +150,7 @@ function getBabelConfig(
presets: [],
plugins: [...babelPlugins],
babelHelpers: 'bundled',
sourcemap: false,
};
if (isDevelopment) {
options.plugins.push(
Expand Down Expand Up @@ -381,6 +384,21 @@ function getPlugins(

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

const needsMinifiedByClosure = isProduction && bundleType !== ESM_PROD;

// Only generate sourcemaps for true "production" build artifacts
// that will be used by bundlers, such as `react-dom.production.min.js`.
// UMD and "profiling" builds are rarely used and not worth having sourcemaps.
const needsSourcemaps =
needsMinifiedByClosure &&
!isProfiling &&
!isUMDBundle &&
!shouldStayReadable;

// For builds with sourcemaps, capture the minified code Closure generated
// so it can be used to help construct the final sourcemap contents.
let chunkCodeAfterClosureCompiler = undefined;

return [
// Keep dynamic imports as externals
dynamicImports(),
Expand All @@ -390,7 +408,7 @@ function getPlugins(
const transformed = flowRemoveTypes(code);
return {
code: transformed.toString(),
map: transformed.generateMap(),
map: null,
};
},
},
Expand Down Expand Up @@ -419,6 +437,7 @@ function getPlugins(
),
// Remove 'use strict' from individual source files.
{
name: "remove 'use strict'",
transform(source) {
return source.replace(/['"]use strict["']/g, '');
},
Expand All @@ -440,35 +459,44 @@ function getPlugins(
isUMDBundle && entry === 'react-art' && commonjs(),
// Apply dead code elimination and/or minification.
// closure doesn't yet support leaving ESM imports intact
isProduction &&
bundleType !== ESM_PROD &&
closure({
compilation_level: 'SIMPLE',
language_in: 'ECMASCRIPT_2020',
language_out:
bundleType === NODE_ES2015
? 'ECMASCRIPT_2020'
: bundleType === BROWSER_SCRIPT
? 'ECMASCRIPT5'
: 'ECMASCRIPT5_STRICT',
emit_use_strict:
bundleType !== BROWSER_SCRIPT &&
bundleType !== ESM_PROD &&
bundleType !== ESM_DEV,
env: 'CUSTOM',
warning_level: 'QUIET',
apply_input_source_maps: false,
use_types_for_optimization: false,
process_common_js_modules: false,
rewrite_polyfills: false,
inject_libraries: false,
allow_dynamic_import: true,

// Don't let it create global variables in the browser.
// https://github.com/facebook/react/issues/10909
assume_function_wrapper: !isUMDBundle,
renaming: !shouldStayReadable,
}),
needsMinifiedByClosure &&
closure(
{
compilation_level: 'SIMPLE',
language_in: 'ECMASCRIPT_2020',
language_out:
bundleType === NODE_ES2015
? 'ECMASCRIPT_2020'
: bundleType === BROWSER_SCRIPT
? 'ECMASCRIPT5'
: 'ECMASCRIPT5_STRICT',
emit_use_strict:
bundleType !== BROWSER_SCRIPT &&
bundleType !== ESM_PROD &&
bundleType !== ESM_DEV,
env: 'CUSTOM',
warning_level: 'QUIET',
source_map_include_content: true,
use_types_for_optimization: false,
process_common_js_modules: false,
rewrite_polyfills: false,
inject_libraries: false,
allow_dynamic_import: true,

// Don't let it create global variables in the browser.
// https://github.com/facebook/react/issues/10909
assume_function_wrapper: !isUMDBundle,
renaming: !shouldStayReadable,
},
{needsSourcemaps}
),
needsSourcemaps && {
name: 'chunk-after-closure',
renderChunk(code, config, options) {
// Side effect - grab the code as Closure mangled it
chunkCodeAfterClosureCompiler = code;
},
},
// Add the whitespace back if necessary.
shouldStayReadable &&
prettier({
Expand All @@ -479,6 +507,7 @@ function getPlugins(
}),
// License and haste headers, top-level `if` blocks.
{
name: 'license-and-headers',
renderChunk(source) {
return Wrappers.wrapBundle(
source,
Expand All @@ -490,6 +519,69 @@ function getPlugins(
);
},
},
needsSourcemaps && {
name: 'generate-prod-bundle-sourcemaps',
async renderChunk(codeAfterLicense, chunk, options, meta) {
// We want to generate a sourcemap that shows the production bundle source
// as it existed before Closure Compiler minified that chunk.
// We also need to apply any license/wrapper text adjustments to that
// sourcemap, so that the mapped locations line up correctly.

// We can split the final chunk code to figure out what got added around
// the code from the Closure step.
const [licensePrefix, licensePostfix] = codeAfterLicense.split(
chunkCodeAfterClosureCompiler
);

const transformedSource = new MagicString(
chunkCodeAfterClosureCompiler
);

// Apply changes so we can generate a sourcemap for this step
if (licensePrefix) {
transformedSource.prepend(licensePrefix);
}

if (licensePostfix) {
transformedSource.append(licensePostfix);
}

// Use a path like `node_modules/react/cjs/react.production.min.js.map` for the sourcemap file
const finalSourcemapPath = options.file.replace('.js', '.js.map');

// Read the sourcemap that Closure wrote to disk
const sourcemapAfterClosure = JSON.parse(
fs.readFileSync(finalSourcemapPath, 'utf8')
);

// Use a name like `react.production.js` for the "pre-minified" sourcemap contents
const fileWithoutMin = filename.replace('.min', '');

// CC generated a file list that only contains the tempfile name.
// Replace that with a more meaningful "source" name for this bundle.
sourcemapAfterClosure.sources = [fileWithoutMin];
sourcemapAfterClosure.file = filename;

// Create an additional sourcemap adjusted for the license header contents
const mapAfterLicense = transformedSource.generateMap({
file: filename,
includeContent: true,
hires: true,
});

// Merge the Closure sourcemap and the with-license sourcemap together
const finalCombinedSourcemap = remapping(
[mapAfterLicense, sourcemapAfterClosure],
() => null
);

// Overwrite the Closure-generated file with the final combined sourcemap
fs.writeFileSync(
finalSourcemapPath,
JSON.stringify(finalCombinedSourcemap)
);
},
},
// Record bundle size.
sizes({
getSize: (size, gzip) => {
Expand Down
22 changes: 16 additions & 6 deletions scripts/rollup/plugins/closure-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,25 @@ function compile(flags) {
});
}

module.exports = function closure(flags = {}) {
module.exports = function closure(flags = {}, {needsSourcemaps}) {
return {
name: 'scripts/rollup/plugins/closure-plugin',
async renderChunk(code) {
async renderChunk(code, chunk, options) {
const inputFile = tmp.fileSync();
const tempPath = inputFile.name;
flags = Object.assign({}, flags, {js: tempPath});
await writeFileAsync(tempPath, code, 'utf8');
const compiledCode = await compile(flags);

// Use a path like `node_modules/react/cjs/react.production.min.js.map` for the sourcemap file
const sourcemapPath = options.file.replace('.js', '.js.map');

// Tell Closure what JS source file to read, and optionally what sourcemap file to write
const finalFlags = {
...flags,
js: inputFile.name,
...(needsSourcemaps && {create_source_map: sourcemapPath}),
};

await writeFileAsync(inputFile.name, code, 'utf8');
const compiledCode = await compile(finalFlags);

inputFile.removeCallback();
return {code: compiledCode};
},
Expand Down

0 comments on commit f389e3d

Please sign in to comment.