diff --git a/package.json b/package.json index 1c42047b86de8..5accd7eac1076 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "shelljs": "^0.8.5", "signedsource": "^2.0.0", "targz": "^1.0.1", + "terser": "^5.30.3", "through2": "^3.0.1", "tmp": "^0.1.0", "typescript": "^3.7.5", diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index 6bfbb6ded6dca..533b682d814e1 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -23,6 +23,7 @@ const Packaging = require('./packaging'); const {asyncRimRaf} = require('./utils'); const codeFrame = require('@babel/code-frame'); const Wrappers = require('./wrappers'); +const minify = require('terser').minify; const RELEASE_CHANNEL = process.env.RELEASE_CHANNEL; @@ -455,137 +456,58 @@ function getPlugins( ); }, }, - // License and haste headers for artifacts with sourcemaps - // For artifacts with sourcemaps we apply these headers - // before passing sources to the Closure compiler, which will be building sourcemaps - needsSourcemaps && { - name: 'license-and-signature-header-for-artifacts-with-sourcemaps', - renderChunk(source) { - return Wrappers.wrapWithLicenseHeader( - source, - bundleType, - globalName, - filename, - moduleType - ); - }, - }, - // Apply dead code elimination and/or minification. - // closure doesn't yet support leaving ESM imports intact + // For production builds, compile with Closure. We do this even for the + // "non-minified" production builds because Closure is much better at + // minification than what most applications use. During this step, we do + // preserve the original symbol names, though, so the resulting code is + // relatively readable. + // + // For the minified builds, the names will be mangled later. + // + // We don't bother with sourcemaps at this step. The sourcemaps we publish + // are only for whitespace and symbol renaming; they don't map back to + // before Closure was applied. 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: true, - renaming: !shouldStayReadable, - }, - {needsSourcemaps} - ), - // Add the whitespace back if necessary. - shouldStayReadable && + 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: true, + + // Don't rename symbols (variable names, functions, etc). This will + // be handled in a later step. + renaming: false, + }), + needsMinifiedByClosure && + // Add the whitespace back prettier({ parser: 'flow', singleQuote: false, trailingComma: 'none', bracketSpacing: true, }), - needsSourcemaps && { - name: 'generate-prod-bundle-sourcemaps', - async renderChunk(minifiedCodeWithChangedHeader, 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); - const outputFolder = path.dirname(options.file); - - // Read the sourcemap that Closure wrote to disk - const sourcemapAfterClosure = JSON.parse( - fs.readFileSync(finalSourcemapPath, 'utf8') - ); - - // Represent the "original" bundle as a file with no `.min` in the name - const filenameWithoutMin = filename.replace('.min', ''); - // There's _one_ artifact where the incoming filename actually contains - // a folder name: "use-sync-external-store-shim/with-selector.production.js". - // The output path already has the right structure, but we need to strip this - // down to _just_ the JS filename. - const preMinifiedFilename = path.basename(filenameWithoutMin); - - // 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 = [preMinifiedFilename]; - sourcemapAfterClosure.file = filename; - - // All our code is considered "third-party" and should be ignored by default. - sourcemapAfterClosure.ignoreList = [0]; - - // We'll write the pre-minified source to disk as a separate file. - // Because it sits on disk, there's no need to have it in the `sourcesContent` array. - // That also makes the file easier to read, and available for use by scripts. - // This should be the only file in the array. - const [preMinifiedBundleSource] = - sourcemapAfterClosure.sourcesContent; - - // Remove this entirely - we're going to write the file to disk instead. - delete sourcemapAfterClosure.sourcesContent; - - const preMinifiedBundlePath = path.join( - outputFolder, - preMinifiedFilename - ); - - // Write the original source to disk as a separate file - fs.writeFileSync(preMinifiedBundlePath, preMinifiedBundleSource); - - // 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 = - minifiedCodeWithChangedHeader + - `\n//# sourceMappingURL=${finalSourcemapFilename}`; - - return { - code: sourceWithMappingUrl, - map: null, - }; - }, - }, - // License and haste headers for artifacts without sourcemaps - // Primarily used for FB-artifacts, which should preserve specific format of the header - // Which potentially can be changed by Closure minification - !needsSourcemaps && { - name: 'license-and-signature-header-for-artifacts-without-sourcemaps', + { + name: 'license-and-signature-header', renderChunk(source) { return Wrappers.wrapWithLicenseHeader( source, @@ -596,6 +518,89 @@ function getPlugins( ); }, }, + isProduction && + !shouldStayReadable && { + name: 'mangle-symbol-names', + async renderChunk(code, chunk, options, meta) { + // Minify the code by mangling symbol names. We already ran Closure + // on this code, so stuff like dead code elimination and inlining + // has already happened. This step is purely to rename the symbols, + // which we asked Closure to preserve. + // + // The only reason this is a separate step from Closure is so we + // can publish non-mangled versions of the code for easier debugging + // in production. We also publish sourcemaps that map back to the + // non-mangled code (*not* the pre-Closure code). + + const outputFolder = path.dirname(options.file); + + // Represent the "original" bundle as a file with no `.min` in the name + const filenameWithoutMin = filename.replace('.min', ''); + // There's _one_ artifact where the incoming filename actually contains + // a folder name: "use-sync-external-store-shim/with-selector.production.js". + // The output path already has the right structure, but we need to strip this + // down to _just_ the JS filename. + const preMinifiedFilename = path.basename(filenameWithoutMin); + const preMinifiedBundlePath = path.join( + outputFolder, + preMinifiedFilename + ); + + // 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); + + const terserOptions = { + // Don't bother compressing. Closure already did that. + compress: false, + toplevel: true, + // Mangle the symbol names. + mangle: { + toplevel: true, + }, + }; + if (needsSourcemaps) { + terserOptions.sourceMap = { + // Used to set the `file` field in the sourcemap + filename: filename, + // Used to set `# sourceMappingURL=` in the compiled code + url: finalSourcemapFilename, + }; + } + + const minifiedResult = await minify( + {[preMinifiedFilename]: code}, + terserOptions + ); + + // Create the directory if it doesn't already exist + fs.mkdirSync(outputFolder, {recursive: true}); + + if (needsSourcemaps) { + const sourcemapJSON = JSON.parse(minifiedResult.map); + + // All our code is considered "third-party" and should be ignored + // by default + sourcemapJSON.ignoreList = [0]; + + // Write the sourcemap to disk + fs.writeFileSync( + finalSourcemapPath, + JSON.stringify(sourcemapJSON) + ); + } + + // Write the original source to disk as a separate file + fs.writeFileSync(preMinifiedBundlePath, code); + + return { + code: minifiedResult.code, + // TODO: Maybe we should use Rollup's sourcemap feature instead + // of writing it to disk manually? + 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 5bb2ffb8b30be..43e2aa43156e9 100644 --- a/scripts/rollup/plugins/closure-plugin.js +++ b/scripts/rollup/plugins/closure-plugin.js @@ -19,20 +19,16 @@ function compile(flags) { }); } -module.exports = function closure(flags = {}, {needsSourcemaps}) { +module.exports = function closure(flags = {}) { return { name: 'scripts/rollup/plugins/closure-plugin', async renderChunk(code, chunk, options) { const inputFile = tmp.fileSync(); - // 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'); diff --git a/yarn.lock b/yarn.lock index 829993bb2cbb5..fd76ea8b5216a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2453,16 +2453,35 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + "@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + "@jridgewell/source-map@^0.3.2": version "0.3.3" resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.3.tgz#8108265659d4c33e72ffe14e33d6cc5eb59f2fda" @@ -2471,12 +2490,20 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + "@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13": version "1.4.14" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/sourcemap-codec@^1.4.15": +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== @@ -2497,6 +2524,14 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" @@ -3689,6 +3724,11 @@ acorn@^8.1.0, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +acorn@^8.8.2: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + adbkit-logcat@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/adbkit-logcat/-/adbkit-logcat-1.1.0.tgz#01d7f9b0cef9093a30bcb3b007efff301508962f" @@ -14555,7 +14595,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -14590,15 +14630,6 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -14659,7 +14690,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -14694,13 +14725,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -15014,6 +15038,16 @@ terser@^5.16.8: commander "^2.20.0" source-map-support "~0.5.20" +terser@^5.30.3: + version "5.30.3" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.3.tgz#f1bb68ded42408c316b548e3ec2526d7dd03f4d2" + integrity sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -16136,7 +16170,7 @@ workerize-loader@^2.0.2: dependencies: loader-utils "^2.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -16154,15 +16188,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"