From 8291597a4115b085d243867810ceb015c279fa31 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 20 Mar 2023 18:10:00 -0400 Subject: [PATCH] Generate sourcemaps for production build artifacts --- scripts/rollup/build.js | 149 +++++++++++++++++------ scripts/rollup/plugins/closure-plugin.js | 22 +++- scripts/rollup/wrappers.js | 9 ++ 3 files changed, 134 insertions(+), 46 deletions(-) diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index bef4d0292c8c9..b8456fa478c13 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -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'); @@ -148,6 +149,7 @@ function getBabelConfig( presets: [], plugins: [...babelPlugins], babelHelpers: 'bundled', + sourcemap: false, }; if (isDevelopment) { options.plugins.push( @@ -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(), @@ -396,7 +419,7 @@ function getPlugins( const transformed = flowRemoveTypes(code); return { code: transformed.toString(), - map: transformed.generateMap(), + map: null, }; }, }, @@ -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, ''); }, @@ -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, @@ -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) => { diff --git a/scripts/rollup/plugins/closure-plugin.js b/scripts/rollup/plugins/closure-plugin.js index 62eba8e687800..5bb2ffb8b30be 100644 --- a/scripts/rollup/plugins/closure-plugin.js +++ b/scripts/rollup/plugins/closure-plugin.js @@ -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}; }, diff --git a/scripts/rollup/wrappers.js b/scripts/rollup/wrappers.js index aecb2bb1398f3..0b30e3b9298f6 100644 --- a/scripts/rollup/wrappers.js +++ b/scripts/rollup/wrappers.js @@ -192,6 +192,7 @@ ${source}`; /****************** FB_WWW_DEV ******************/ [FB_WWW_DEV](source, globalName, filename, moduleType) { return `/** + * @preserve ${license} * * @noflow @@ -212,6 +213,7 @@ ${source} /****************** FB_WWW_PROD ******************/ [FB_WWW_PROD](source, globalName, filename, moduleType) { return `/** + * @preserve ${license} * * @noflow @@ -226,6 +228,7 @@ ${source}`; /****************** FB_WWW_PROFILING ******************/ [FB_WWW_PROFILING](source, globalName, filename, moduleType) { return `/** + * @preserve ${license} * * @noflow @@ -240,6 +243,7 @@ ${source}`; /****************** RN_OSS_DEV ******************/ [RN_OSS_DEV](source, globalName, filename, moduleType) { return signFile(`/** + * @preserve ${license} * * @noflow @@ -261,6 +265,7 @@ ${source} /****************** RN_OSS_PROD ******************/ [RN_OSS_PROD](source, globalName, filename, moduleType) { return signFile(`/** + * @preserve ${license} * * @noflow @@ -276,6 +281,7 @@ ${source}`); /****************** RN_OSS_PROFILING ******************/ [RN_OSS_PROFILING](source, globalName, filename, moduleType) { return signFile(`/** + * @preserve ${license} * * @noflow @@ -291,6 +297,7 @@ ${source}`); /****************** RN_FB_DEV ******************/ [RN_FB_DEV](source, globalName, filename, moduleType) { return signFile(`/** + * @preserve ${license} * * @noflow @@ -311,6 +318,7 @@ ${source} /****************** RN_FB_PROD ******************/ [RN_FB_PROD](source, globalName, filename, moduleType) { return signFile(`/** + * @preserve ${license} * * @noflow @@ -325,6 +333,7 @@ ${source}`); /****************** RN_FB_PROFILING ******************/ [RN_FB_PROFILING](source, globalName, filename, moduleType) { return signFile(`/** + * @preserve ${license} * * @noflow