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 Aug 27, 2023
1 parent 0a2a8b2 commit 8291597
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 46 deletions.
149 changes: 109 additions & 40 deletions scripts/rollup/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const stripBanner = require('rollup-plugin-strip-banner');
const chalk = require('chalk');
const resolve = require('@rollup/plugin-node-resolve').nodeResolve;
const fs = require('fs');
const path = require('path');
const argv = require('minimist')(process.argv.slice(2));
const Modules = require('./modules');
const Bundles = require('./bundles');
Expand Down Expand Up @@ -148,6 +149,7 @@ function getBabelConfig(
presets: [],
plugins: [...babelPlugins],
babelHelpers: 'bundled',
sourcemap: false,
};
if (isDevelopment) {
options.plugins.push(
Expand Down Expand Up @@ -387,6 +389,27 @@ function getPlugins(

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

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

// Any other packages that should specifically _not_ have sourcemaps
const sourcemapPackageExcludes = [
// Having `//#sourceMappingUrl` for the `react-debug-tools` prod bundle breaks
// `ReactDevToolsHooksIntegration-test.js`, because it changes Node's generated
// stack traces and thus alters the hook name parsing behavior.
// Also, this is an internal-only package that doesn't need sourcemaps anyway
'react-debug-tools',
];

// 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 &&
!sourcemapPackageExcludes.includes(entry) &&
!shouldStayReadable;

return [
// Keep dynamic imports as externals
dynamicImports(),
Expand All @@ -396,7 +419,7 @@ function getPlugins(
const transformed = flowRemoveTypes(code);
return {
code: transformed.toString(),
map: transformed.generateMap(),
map: null,
};
},
},
Expand Down Expand Up @@ -425,6 +448,7 @@ function getPlugins(
),
// Remove 'use strict' from individual source files.
{
name: "remove 'use strict'",
transform(source) {
return source.replace(/['"]use strict["']/g, '');
},
Expand All @@ -446,47 +470,9 @@ function getPlugins(
// I'm going to port "art" to ES modules to avoid this problem.
// Please don't enable this for anything else!
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,
}),
// Add the whitespace back if necessary.
shouldStayReadable &&
prettier({
parser: 'flow',
singleQuote: false,
trailingComma: 'none',
bracketSpacing: true,
}),
// License and haste headers, top-level `if` blocks.
{
name: 'license-and-headers',
renderChunk(source) {
return Wrappers.wrapBundle(
source,
Expand All @@ -498,6 +484,89 @@ function getPlugins(
);
},
},
// Apply dead code elimination and/or minification.
// closure doesn't yet support leaving ESM imports intact
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}
),
// Add the whitespace back if necessary.
shouldStayReadable &&
prettier({
parser: 'flow',
singleQuote: false,
trailingComma: 'none',
bracketSpacing: true,
}),
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, rather than
// showing the "original" individual source files. This better shows
// what is actually running in the app.

// 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');
const finalSourcemapFilename = path.basename(finalSourcemapPath);

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

const filenameWithoutMin = 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
// that represents "the bundled source before minification".
sourcemapAfterClosure.sources = [filenameWithoutMin];
sourcemapAfterClosure.file = filename;

// Overwrite the Closure-generated file with the final combined sourcemap
fs.writeFileSync(
finalSourcemapPath,
JSON.stringify(sourcemapAfterClosure)
);

// Add the sourcemap URL to the actual bundle, so that tools pick it up
const sourceWithMappingUrl =
codeAfterLicense +
`\n//# sourceMappingURL=${finalSourcemapFilename}`;

return {
code: sourceWithMappingUrl,
map: null,
};
},
},
// 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
9 changes: 9 additions & 0 deletions scripts/rollup/wrappers.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ ${source}`;
/****************** FB_WWW_DEV ******************/
[FB_WWW_DEV](source, globalName, filename, moduleType) {
return `/**
* @preserve
${license}
*
* @noflow
Expand All @@ -212,6 +213,7 @@ ${source}
/****************** FB_WWW_PROD ******************/
[FB_WWW_PROD](source, globalName, filename, moduleType) {
return `/**
* @preserve
${license}
*
* @noflow
Expand All @@ -226,6 +228,7 @@ ${source}`;
/****************** FB_WWW_PROFILING ******************/
[FB_WWW_PROFILING](source, globalName, filename, moduleType) {
return `/**
* @preserve
${license}
*
* @noflow
Expand All @@ -240,6 +243,7 @@ ${source}`;
/****************** RN_OSS_DEV ******************/
[RN_OSS_DEV](source, globalName, filename, moduleType) {
return signFile(`/**
* @preserve
${license}
*
* @noflow
Expand All @@ -261,6 +265,7 @@ ${source}
/****************** RN_OSS_PROD ******************/
[RN_OSS_PROD](source, globalName, filename, moduleType) {
return signFile(`/**
* @preserve
${license}
*
* @noflow
Expand All @@ -276,6 +281,7 @@ ${source}`);
/****************** RN_OSS_PROFILING ******************/
[RN_OSS_PROFILING](source, globalName, filename, moduleType) {
return signFile(`/**
* @preserve
${license}
*
* @noflow
Expand All @@ -291,6 +297,7 @@ ${source}`);
/****************** RN_FB_DEV ******************/
[RN_FB_DEV](source, globalName, filename, moduleType) {
return signFile(`/**
* @preserve
${license}
*
* @noflow
Expand All @@ -311,6 +318,7 @@ ${source}
/****************** RN_FB_PROD ******************/
[RN_FB_PROD](source, globalName, filename, moduleType) {
return signFile(`/**
* @preserve
${license}
*
* @noflow
Expand All @@ -325,6 +333,7 @@ ${source}`);
/****************** RN_FB_PROFILING ******************/
[RN_FB_PROFILING](source, globalName, filename, moduleType) {
return signFile(`/**
* @preserve
${license}
*
* @noflow
Expand Down

0 comments on commit 8291597

Please sign in to comment.