From 44ca36d90acbd6736a57dda9768f4c46f2c514d1 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 11 Apr 2024 12:53:51 -0400 Subject: [PATCH] Run Closure on non-minified prod builds, too In #26446 we started publishing non-minified versions of our production build artifacts, along with source maps, for easier debugging of React when running in production mode. The way it's currently set up is that these builds are generated *before* Closure compiler has run. Which means it's missing many of the optimizations that are in the final build, like dead code elimination. This PR changes the build process to run Closure on the non-minified production builds, too, by moving the sourcemap generation to later in the pipeline. The non-minified builds will still preserve the original symbol names, and we'll use Prettier to add back whitespace. This is the exact same approach we've been using for years to generate production builds for Meta. The idea is that the only difference between the minified and non- minified builds is whitespace and symbol mangling. The semantic structure of the program should be identical. To implement this, I disabled symbol mangling when running Closure compiler. Then, in a later step, the symbols are mangled by Terser. This is when the source maps are generated. --- package.json | 1 + scripts/rollup/build.js | 253 ++++++++++++----------- scripts/rollup/plugins/closure-plugin.js | 6 +- yarn.lock | 83 +++++--- 4 files changed, 185 insertions(+), 158 deletions(-) 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"