@samitouri / QOS-React-1 / commits / 2c8a139a59

Generate sourcemaps for production build artifacts (#26446)

<!-- Thanks for submitting a pull request! We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The three fields below are mandatory. Before submitting a pull request, please make sure the following is done: 1. Fork [the repository](https://github.com/facebook/react) and create your branch from `main`. 2. Run `yarn` in the repository root. 3. If you've fixed a bug or added code that should be tested, add tests! 4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development. 5. Run `yarn test --prod` to test in the production environment. It supports the same options as `yarn test`. 6. If you need a debugger, run `yarn test --debug --watch TestName`, open `chrome://inspect`, and press "Inspect". 7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`). 8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files. 9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`). 10. If you haven't already, complete the CLA. Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html --> ## Summary This PR updates the Rollup build pipeline to generate sourcemaps for production build artifacts like `react-dom.production.min.js`. It requires the Rollup v3 changes that were just merged in #26442 . Sourcemaps are currently _only_ generated for build artifacts that are _truly_ "production" - no sourcemaps will be generated for development, profiling, UMD, or `shouldStayReadable` artifacts. The generated sourcemaps contain the bundled source contents right before that chunk was minified by Closure, and _not_ the original source files like `react-reconciler/src/*`. This better reflects the actual code that is running as part of the bundle, with all the feature flags and transformations that were applied to the source files to generate that bundle. The sourcemaps _do_ still show comments and original function names, thus improving debuggability for production usage. Fixes #20186 . <!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? --> This allows React users to actually debug a readable version of the React bundle in production scenarios. It also allows other tools like [Replay](https://replay.io) to do a better job inspecting the React source when stepping through. ## How did you test this change? - Generated numerous sourcemaps with various combinations of the React bundle selections - Viewed those sourcemaps in https://evanw.github.io/source-map-visualization/ and confirmed via the visualization that the generated mappings appear to be correct I've attached a set of production files + their sourcemaps here: [react-sourcemap-examples.zip](https://github.com/facebook/react/files/11023466/react-sourcemap-examples.zip) You can drag JS+sourcemap file pairs into https://evanw.github.io/source-map-visualization/ for viewing. Examples: - `react.production.min.js`: ![image](https://user-images.githubusercontent.com/1128784/226478247-e5cbdee0-83fd-4a19-bcf1-09961d3c7da4.png) - `react-dom.production.min.js`: ![image](https://user-images.githubusercontent.com/1128784/226478433-b5ccbf0f-8f68-42fe-9db9-9ecb97770d46.png) - `use-sync-external-store/with-selector.production.min.js`: ![image](https://user-images.githubusercontent.com/1128784/226478565-bc74699d-db14-4c39-9e2d-b775f8755561.png) <!-- Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes the user interface. How exactly did you verify that your PR solves the issue you wanted to solve? If you leave this empty, your PR will very likely be closed. -->

Mark Erikson committed Nov 7, 2023 at 13:59 UTC 2c8a139a593e0294c3a6953d74b451bd05fdcfca
3 files changed +203 -75
scripts/rollup/build.js
+178 -69
@@ -11,6 +11,7 @@ const stripBanner = require('rollup-plugin-strip-banner');
11 const chalk = require('chalk');
12 const resolve = require('@rollup/plugin-node-resolve').nodeResolve;
13 const fs = require('fs');
14 +const path = require('path');
15 const argv = require('minimist')(process.argv.slice(2));
16 const Modules = require('./modules');
17 const Bundles = require('./bundles');
@@ -148,6 +149,7 @@ function getBabelConfig(
149 presets: [],
150 plugins: [...babelPlugins],
151 babelHelpers: 'bundled',
152 + sourcemap: false,
153 };
154 if (isDevelopment) {
155 options.plugins.push(
@@ -315,6 +317,45 @@ function isProfilingBundleType(bundleType) {
317 }
318 }
319
320 +function getBundleTypeFlags(bundleType) {
321 + const isUMDBundle =
322 + bundleType === UMD_DEV ||
323 + bundleType === UMD_PROD ||
324 + bundleType === UMD_PROFILING;
325 + const isFBWWWBundle =
326 + bundleType === FB_WWW_DEV ||
327 + bundleType === FB_WWW_PROD ||
328 + bundleType === FB_WWW_PROFILING;
329 + const isRNBundle =
330 + bundleType === RN_OSS_DEV ||
331 + bundleType === RN_OSS_PROD ||
332 + bundleType === RN_OSS_PROFILING ||
333 + bundleType === RN_FB_DEV ||
334 + bundleType === RN_FB_PROD ||
335 + bundleType === RN_FB_PROFILING;
336 +
337 + const isFBRNBundle =
338 + bundleType === RN_FB_DEV ||
339 + bundleType === RN_FB_PROD ||
340 + bundleType === RN_FB_PROFILING;
341 +
342 + const shouldStayReadable = isFBWWWBundle || isRNBundle || forcePrettyOutput;
343 +
344 + const shouldBundleDependencies =
345 + bundleType === UMD_DEV ||
346 + bundleType === UMD_PROD ||
347 + bundleType === UMD_PROFILING;
348 +
349 + return {
350 + isUMDBundle,
351 + isFBWWWBundle,
352 + isRNBundle,
353 + isFBRNBundle,
354 + shouldBundleDependencies,
355 + shouldStayReadable,
356 + };
357 +}
358 +
359 function forbidFBJSImports() {
360 return {
361 name: 'forbidFBJSImports',
@@ -345,22 +386,30 @@ function getPlugins(
386 const forks = Modules.getForks(bundleType, entry, moduleType, bundle);
387 const isProduction = isProductionBundleType(bundleType);
388 const isProfiling = isProfilingBundleType(bundleType);
348 - const isUMDBundle =
349 - bundleType === UMD_DEV ||
350 - bundleType === UMD_PROD ||
351 - bundleType === UMD_PROFILING;
352 - const isFBWWWBundle =
353 - bundleType === FB_WWW_DEV ||
354 - bundleType === FB_WWW_PROD ||
355 - bundleType === FB_WWW_PROFILING;
356 - const isRNBundle =
357 - bundleType === RN_OSS_DEV ||
358 - bundleType === RN_OSS_PROD ||
359 - bundleType === RN_OSS_PROFILING ||
360 - bundleType === RN_FB_DEV ||
361 - bundleType === RN_FB_PROD ||
362 - bundleType === RN_FB_PROFILING;
363 - const shouldStayReadable = isFBWWWBundle || isRNBundle || forcePrettyOutput;
389 +
390 + const {isUMDBundle, shouldStayReadable} = getBundleTypeFlags(bundleType);
391 +
392 + const needsMinifiedByClosure = isProduction && bundleType !== ESM_PROD;
393 +
394 + // Any other packages that should specifically _not_ have sourcemaps
395 + const sourcemapPackageExcludes = [
396 + // Having `//#sourceMappingUrl` for the `react-debug-tools` prod bundle breaks
397 + // `ReactDevToolsHooksIntegration-test.js`, because it changes Node's generated
398 + // stack traces and thus alters the hook name parsing behavior.
399 + // Also, this is an internal-only package that doesn't need sourcemaps anyway
400 + 'react-debug-tools',
401 + ];
402 +
403 + // Generate sourcemaps for true "production" build artifacts
404 + // that will be used by bundlers, such as `react-dom.production.min.js`.
405 + // Also include profiling builds as well.
406 + // UMD builds are rarely used and not worth having sourcemaps.
407 + const needsSourcemaps =
408 + needsMinifiedByClosure &&
409 + !isUMDBundle &&
410 + !sourcemapPackageExcludes.includes(entry) &&
411 + !shouldStayReadable;
412 +
413 return [
414 // Keep dynamic imports as externals
415 dynamicImports(),
@@ -370,7 +419,7 @@ function getPlugins(
419 const transformed = flowRemoveTypes(code);
420 return {
421 code: transformed.toString(),
373 - map: transformed.generateMap(),
422 + map: null,
423 };
424 },
425 },
@@ -399,6 +448,7 @@ function getPlugins(
448 ),
449 // Remove 'use strict' from individual source files.
450 {
451 + name: "remove 'use strict'",
452 transform(source) {
453 return source.replace(/['"]use strict["']/g, '');
454 },
@@ -420,47 +470,9 @@ function getPlugins(
470 // I'm going to port "art" to ES modules to avoid this problem.
471 // Please don't enable this for anything else!
472 isUMDBundle && entry === 'react-art' && commonjs(),
423 - // Apply dead code elimination and/or minification.
424 - // closure doesn't yet support leaving ESM imports intact
425 - isProduction &&
426 - bundleType !== ESM_PROD &&
427 - closure({
428 - compilation_level: 'SIMPLE',
429 - language_in: 'ECMASCRIPT_2020',
430 - language_out:
431 - bundleType === NODE_ES2015
432 - ? 'ECMASCRIPT_2020'
433 - : bundleType === BROWSER_SCRIPT
434 - ? 'ECMASCRIPT5'
435 - : 'ECMASCRIPT5_STRICT',
436 - emit_use_strict:
437 - bundleType !== BROWSER_SCRIPT &&
438 - bundleType !== ESM_PROD &&
439 - bundleType !== ESM_DEV,
440 - env: 'CUSTOM',
441 - warning_level: 'QUIET',
442 - apply_input_source_maps: false,
443 - use_types_for_optimization: false,
444 - process_common_js_modules: false,
445 - rewrite_polyfills: false,
446 - inject_libraries: false,
447 - allow_dynamic_import: true,
448 -
449 - // Don't let it create global variables in the browser.
450 - // https://github.com/facebook/react/issues/10909
451 - assume_function_wrapper: !isUMDBundle,
452 - renaming: !shouldStayReadable,
453 - }),
454 - // Add the whitespace back if necessary.
455 - shouldStayReadable &&
456 - prettier({
457 - parser: 'flow',
458 - singleQuote: false,
459 - trailingComma: 'none',
460 - bracketSpacing: true,
461 - }),
473 // License and haste headers, top-level `if` blocks.
474 {
475 + name: 'license-and-headers',
476 renderChunk(source) {
477 return Wrappers.wrapBundle(
478 source,
@@ -472,6 +484,114 @@ function getPlugins(
484 );
485 },
486 },
487 + // Apply dead code elimination and/or minification.
488 + // closure doesn't yet support leaving ESM imports intact
489 + needsMinifiedByClosure &&
490 + closure(
491 + {
492 + compilation_level: 'SIMPLE',
493 + language_in: 'ECMASCRIPT_2020',
494 + language_out:
495 + bundleType === NODE_ES2015
496 + ? 'ECMASCRIPT_2020'
497 + : bundleType === BROWSER_SCRIPT
498 + ? 'ECMASCRIPT5'
499 + : 'ECMASCRIPT5_STRICT',
500 + emit_use_strict:
501 + bundleType !== BROWSER_SCRIPT &&
502 + bundleType !== ESM_PROD &&
503 + bundleType !== ESM_DEV,
504 + env: 'CUSTOM',
505 + warning_level: 'QUIET',
506 + source_map_include_content: true,
507 + use_types_for_optimization: false,
508 + process_common_js_modules: false,
509 + rewrite_polyfills: false,
510 + inject_libraries: false,
511 + allow_dynamic_import: true,
512 +
513 + // Don't let it create global variables in the browser.
514 + // https://github.com/facebook/react/issues/10909
515 + assume_function_wrapper: !isUMDBundle,
516 + renaming: !shouldStayReadable,
517 + },
518 + {needsSourcemaps}
519 + ),
520 + // Add the whitespace back if necessary.
521 + shouldStayReadable &&
522 + prettier({
523 + parser: 'flow',
524 + singleQuote: false,
525 + trailingComma: 'none',
526 + bracketSpacing: true,
527 + }),
528 + needsSourcemaps && {
529 + name: 'generate-prod-bundle-sourcemaps',
530 + async renderChunk(codeAfterLicense, chunk, options, meta) {
531 + // We want to generate a sourcemap that shows the production bundle source
532 + // as it existed before Closure Compiler minified that chunk, rather than
533 + // showing the "original" individual source files. This better shows
534 + // what is actually running in the app.
535 +
536 + // Use a path like `node_modules/react/cjs/react.production.min.js.map` for the sourcemap file
537 + const finalSourcemapPath = options.file.replace('.js', '.js.map');
538 + const finalSourcemapFilename = path.basename(finalSourcemapPath);
539 + const outputFolder = path.dirname(options.file);
540 +
541 + // Read the sourcemap that Closure wrote to disk
542 + const sourcemapAfterClosure = JSON.parse(
543 + fs.readFileSync(finalSourcemapPath, 'utf8')
544 + );
545 +
546 + // Represent the "original" bundle as a file with no `.min` in the name
547 + const filenameWithoutMin = filename.replace('.min', '');
548 + // There's _one_ artifact where the incoming filename actually contains
549 + // a folder name: "use-sync-external-store-shim/with-selector.production.js".
550 + // The output path already has the right structure, but we need to strip this
551 + // down to _just_ the JS filename.
552 + const preMinifiedFilename = path.basename(filenameWithoutMin);
553 +
554 + // CC generated a file list that only contains the tempfile name.
555 + // Replace that with a more meaningful "source" name for this bundle
556 + // that represents "the bundled source before minification".
557 + sourcemapAfterClosure.sources = [preMinifiedFilename];
558 + sourcemapAfterClosure.file = filename;
559 +
560 + // We'll write the pre-minified source to disk as a separate file.
561 + // Because it sits on disk, there's no need to have it in the `sourcesContent` array.
562 + // That also makes the file easier to read, and available for use by scripts.
563 + // This should be the only file in the array.
564 + const [preMinifiedBundleSource] =
565 + sourcemapAfterClosure.sourcesContent;
566 +
567 + // Remove this entirely - we're going to write the file to disk instead.
568 + delete sourcemapAfterClosure.sourcesContent;
569 +
570 + const preMinifiedBundlePath = path.join(
571 + outputFolder,
572 + preMinifiedFilename
573 + );
574 +
575 + // Write the original source to disk as a separate file
576 + fs.writeFileSync(preMinifiedBundlePath, preMinifiedBundleSource);
577 +
578 + // Overwrite the Closure-generated file with the final combined sourcemap
579 + fs.writeFileSync(
580 + finalSourcemapPath,
581 + JSON.stringify(sourcemapAfterClosure)
582 + );
583 +
584 + // Add the sourcemap URL to the actual bundle, so that tools pick it up
585 + const sourceWithMappingUrl =
586 + codeAfterLicense +
587 + `\n//# sourceMappingURL=${finalSourcemapFilename}`;
588 +
589 + return {
590 + code: sourceWithMappingUrl,
591 + map: null,
592 + };
593 + },
594 + },
595 // Record bundle size.
596 sizes({
597 getSize: (size, gzip) => {
@@ -577,25 +697,14 @@ async function createBundle(bundle, bundleType) {
697 const format = getFormat(bundleType);
698 const packageName = Packaging.getPackageName(bundle.entry);
699
580 - const isFBWWWBundle =
581 - bundleType === FB_WWW_DEV ||
582 - bundleType === FB_WWW_PROD ||
583 - bundleType === FB_WWW_PROFILING;
584 -
585 - const isFBRNBundle =
586 - bundleType === RN_FB_DEV ||
587 - bundleType === RN_FB_PROD ||
588 - bundleType === RN_FB_PROFILING;
700 + const {isFBWWWBundle, isFBRNBundle, shouldBundleDependencies} =
701 + getBundleTypeFlags(bundleType);
702
703 let resolvedEntry = resolveEntryFork(
704 require.resolve(bundle.entry),
705 isFBWWWBundle || isFBRNBundle
706 );
707
595 - const shouldBundleDependencies =
596 - bundleType === UMD_DEV ||
597 - bundleType === UMD_PROD ||
598 - bundleType === UMD_PROFILING;
708 const peerGlobals = Modules.getPeerGlobals(bundle.externals, bundleType);
709 let externals = Object.keys(peerGlobals);
710 if (!shouldBundleDependencies) {
scripts/rollup/plugins/closure-plugin.js
+16 -6
@@ -19,15 +19,25 @@ function compile(flags) {
19 });
20 }
21
22 -module.exports = function closure(flags = {}) {
22 +module.exports = function closure(flags = {}, {needsSourcemaps}) {
23 return {
24 name: 'scripts/rollup/plugins/closure-plugin',
25 - async renderChunk(code) {
25 + async renderChunk(code, chunk, options) {
26 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 +
41 inputFile.removeCallback();
42 return {code: compiledCode};
43 },
scripts/rollup/wrappers.js
+9
@@ -192,6 +192,7 @@ ${source}`;
192 /****************** FB_WWW_DEV ******************/
193 [FB_WWW_DEV](source, globalName, filename, moduleType) {
194 return `/**
195 + * @preserve
196 ${license}
197 *
198 * @noflow
@@ -212,6 +213,7 @@ ${source}
213 /****************** FB_WWW_PROD ******************/
214 [FB_WWW_PROD](source, globalName, filename, moduleType) {
215 return `/**
216 + * @preserve
217 ${license}
218 *
219 * @noflow
@@ -226,6 +228,7 @@ ${source}`;
228 /****************** FB_WWW_PROFILING ******************/
229 [FB_WWW_PROFILING](source, globalName, filename, moduleType) {
230 return `/**
231 + * @preserve
232 ${license}
233 *
234 * @noflow
@@ -240,6 +243,7 @@ ${source}`;
243 /****************** RN_OSS_DEV ******************/
244 [RN_OSS_DEV](source, globalName, filename, moduleType) {
245 return signFile(`/**
246 + * @preserve
247 ${license}
248 *
249 * @noflow
@@ -261,6 +265,7 @@ ${source}
265 /****************** RN_OSS_PROD ******************/
266 [RN_OSS_PROD](source, globalName, filename, moduleType) {
267 return signFile(`/**
268 + * @preserve
269 ${license}
270 *
271 * @noflow
@@ -276,6 +281,7 @@ ${source}`);
281 /****************** RN_OSS_PROFILING ******************/
282 [RN_OSS_PROFILING](source, globalName, filename, moduleType) {
283 return signFile(`/**
284 + * @preserve
285 ${license}
286 *
287 * @noflow
@@ -291,6 +297,7 @@ ${source}`);
297 /****************** RN_FB_DEV ******************/
298 [RN_FB_DEV](source, globalName, filename, moduleType) {
299 return signFile(`/**
300 + * @preserve
301 ${license}
302 *
303 * @noflow
@@ -311,6 +318,7 @@ ${source}
318 /****************** RN_FB_PROD ******************/
319 [RN_FB_PROD](source, globalName, filename, moduleType) {
320 return signFile(`/**
321 + * @preserve
322 ${license}
323 *
324 * @noflow
@@ -325,6 +333,7 @@ ${source}`);
333 /****************** RN_FB_PROFILING ******************/
334 [RN_FB_PROFILING](source, globalName, filename, moduleType) {
335 return signFile(`/**
336 + * @preserve
337 ${license}
338 *
339 * @noflow