Repository navigation
fix(build): make Firefox builds reproducible across paths and remove the runtime-chunk mangling workaround - #46845
gauthierpetetin wants to merge 14 commits into
Conversation
Brings in swc-project/swc#12129 (drop spans of cached `globals` values) and #12166 (key the optimizer env cache by its configured values). Together they stop SWC's `process.env` inlining from attaching stale, thread-timing dependent source-map positions to inlined values, which perturbed module hashes between otherwise identical builds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Extract TYPESCRIPT_NON_TSX_FILE_RE and TYPESCRIPT_TSX_FILE_RE so webpack and envValidationLoader reuse the same .ts/.tsx matching rules. Co-authored-by: Cursor <cursoragent@cursor.com>
Match the SWC loader split so webpack unit tests expect separate non-TSX and TSX React Refresh rules. Co-authored-by: Cursor <cursoragent@cursor.com>
Avoid hanging when Save closes a modal before the detached wait starts, which the SWC 1.16 build made consistently reproducible. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the 1s custom-network block-tracker interval in test builds so subscribe E2E does not wait on the 20s production poll. Co-authored-by: Cursor <cursoragent@cursor.com>
Satisfy webpack tsc for the IN_TEST npm loader map, and drop trailing newlines so regenerated MV2/MV3 policies match CI. Co-authored-by: Cursor <cursoragent@cursor.com>
Reverts the runtime-chunk special case from #46236. With the two root causes of the `runtime.[contenthash].js` non-determinism fixed at the source (the swc loader no longer leaks the absolute build path into module hashes, and `@swc/core` 1.16.2 no longer attaches stale positions to inlined `process.env` values), SWC's frequency-ordered mangling is deterministic again and the runtime chunk can be mangled like every other chunk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
webpack only rewrites a loader source map's `sources` relative to the build context when the map is an object (`NormalModule.contextifySourceMap` passes strings through), and the map is then hashed into `buildInfo.hash` verbatim. Because `sourceFileName` is the absolute `resourcePath`, returning SWC's map as a JSON string made every module's hash, and so every chunk's provisional `[contenthash]`, depend on where the project lives on disk. The runtime chunk embeds those provisional hashes as string literals, which shifted SWC's character-frequency mangling alphabet between build paths. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`html-bundler-webpack-plugin`'s loader turns `<script src="./x.ts">` into
`require('/absolute/path/to/x.ts')` in the generated HTML entry module.
webpack resolves it fine, but it hashes that source text into
`buildInfo.hash`, so the seven HTML entry chunks' provisional
`[contenthash]` depended on where the project lives on disk — enough to
tip the runtime chunk's character-frequency mangling alphabet on v13.47.1
even with the swc loader and `@swc/core` fixes in place.
Extend our existing yarn patch so `requireExpression()` emits the request
relative to the template's directory (`require('../../scripts/x.ts')`),
which webpack resolves identically.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`yarn lavamoat:auto`. With swc-project/swc#12166 the optimizer's env cache is keyed by its configured values, so `node_modules` code no longer inherits the first-party `process.env` inlining and the policy generator now sees `process.env.NODE_ENV` in those packages (webpack's `optimization.nodeEnv` still replaces it in the emitted bundle). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`reactCompilerLoaderWrapper` recorded the compiler's per-function events on `module.buildMeta`, including the absolute `filename`. webpack hashes `JSON.stringify(buildMeta)` into every module's `buildInfo.hash`, so the ~1,800 `ui/**` modules that go through the React Compiler had hashes that depended on the absolute build path — and on whether the loader happened to run in-process or in a thread-loader worker (where `this._module` is unavailable and nothing is recorded), which thread-loader decides at runtime when its pool is unavailable. Both shifted the provisional chunk hashes embedded in the runtime chunk and so SWC's character-frequency mangling alphabet. Store the events on `buildInfo` instead, which webpack does not hash, and record the filename relative to the build context. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes. |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning MetaMask internal reviewing guidelines:
|
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Background: why four unrelated bugs all showed up as the same
|
Fix 1 of 4: the build folder's name was baked into every module's fingerprintFile: What was happeningWhen our swc loader compiles a file, it hands webpack two things: the compiled code and a source map (the debugging metadata that says "this output line came from that input line"). The source map names the original file it describes, and SWC writes that as the full absolute path: { "sources": ["/Users/alice/Repositories/metamask-extension/app/scripts/background.ts"], ... }webpack knows this is a problem and normally scrubs it: it rewrites the path relative to the project ( if (typeof sourceMap === "string" || !Array.isArray(sourceMap.sources)) {
return sourceMap; // ← hands the string back untouched
}So the raw string, absolute path included, went straight into the fingerprint of every module we compile: This is why the reviewers' rebuild, which runs in a different directory than our release build, kept failing while our own CI builds never did. The fixGive webpack the parsed object instead of the string, so its own scrubbing runs: -transform(src, options).then(({ code, map }) => cb(null, code, map), cb);
+transform(src, options).then(
+ ({ code, map }) => cb(null, code, map === undefined ? map : JSON.parse(map)),
+ cb,
+);The existing unit test asserted the string form. It now asserts the object form and explains why. How we know it worksPer-module fingerprint dumps of the same tree built in a 43-character and a 115-character path: before the fix, 11,331 modules had different fingerprints. After it, only the modules affected by fixes 2 to 4 still did. |
Fix 2 of 4:
|
Fix 3 of 4: the HTML pages were compiled with absolute paths inside themFile: our existing yarn patch for What was happeningOur HTML pages are webpack entry points. module.exports = '…<script src="' + require('/Users/alice/Repositories/metamask-extension/app/scripts/load/bootstrap.ts') + '" defer>…'
↑ absolute path, written into the module's codewebpack resolves that Only 7 modules out of 11,435, but the margin between The fixMake the generated code say The plugin, however, also executes that generated module itself later, with its own mini The scope rule matters: a stylesheet's How we know it worksWith this fix, the two-path fingerprint comparison shows 0 differing modules. All 663 output files, including the HTML pages, are byte-identical across paths, and the pages' |
Fix 4 of 4: React Compiler statistics were being hashed along with the codeFiles: What was happeningOur React Compiler wrapper collects statistics for the
"__reactCompilerStatus__": { "events": [
{ "filename": "/home/runner/work/metamask-extension/metamask-extension/work/i1-xxxxxx/metamask-extension/ui/contexts/assetPolling.tsx", … }
]}made 1,800 fingerprints depend on the build directory. Worse, they also depended on timing. The wrapper can only record events when it runs in the main process (it needs This is why this one can only be seen on CI: on a laptop it is literally not there. The fixStore the events on - const buildMeta = this._module?.buildMeta
+ const buildInfo = this._module?.buildInfo
…
- filename,
+ filename: rootContext ? relative(rootContext, filename) : filename,
…
- buildMeta[REACT_COMPILER_STATUS_KEY] = { events };
+ buildInfo[REACT_COMPILER_STATUS_KEY] = { events };How we know it worksThis branch, built unmodified at 20 different paths on the CI runners: 1 |
✨ Files requiring CODEOWNER review ✨👨🔧 @MetaMask/extension-platform (9 files, +125 -60)
📜 @MetaMask/policy-reviewers (8 files, +640 -120)
Tip Follow the policy review process outlined in the LavaMoat Policy Review Process doc before expecting an approval from Policy Reviewers. 👨🔧 @itsyoboieltr (9 files, +125 -60)
|
Builds ready [c802969]
⚡ Performance Benchmarks (Total: 🟢 8 pass · 🟡 3 warn · 🔴 2 fail)
Bundle Size Diffs [🚀 Bundle size reduced!]
|
|


Description
Firefox (AMO) reviewers rebuild MetaMask from source and compare it byte for byte with what we
submitted.
runtime.jssometimes came out different (190 bytes, allc/lswaps in variablenames), which has been blocking Firefox releases. #46236 worked around it by not minifying
variable names in that one file.
This PR fixes the four root causes and removes the workaround. All four are ways that something
build-specific (the absolute path of the build directory, or which thread happened to compile a
file) leaked into webpack's module fingerprints, which in turn tipped SWC's letter-frequency-based
variable naming. Each one is explained in plain language, for readers with no context, in a
comment:
c/lswap (read first, 2 min)swcLoader.ts@swc/corecachedprocess.envvalues with positions from the first file compiled, bump to 1.16.2require()paths inside,html-bundler-webpack-pluginyarn patchbuildMeta, inreactCompilerLoaderWrapper.tsruntime.jsare minified again (12.7 KB raw / 1.9 KB gzipped smaller thanmain)The commits are split one per fix, so each can be reviewed on its own.
Proof
We built this branch 20 times on CI, each time in a directory with a different name, and compared
the outputs: all 20 builds are byte-identical. The same test on v13.47.1 as released gives two
different
runtime.jsfiles (11 builds one way, 9 the other).CI run
Changelog
CHANGELOG entry: null
Related issues
Fixes: INFRA-3920
Manual testing steps
yarn install, thenSOURCE_DATE_EPOCH=1700000000 ENABLE_MV3=false yarn webpack:lavamoat:build:mv2 --env productiondiff -r <copyA>/dist/firefox <copyB>/dist/firefoxand verify there are no differences.dist/firefoxas a temporary add-on in Firefox (about:debugging), unlock or onboard, andverify the UI renders with no
LavaDome/lockdown/module is undefinedconsole errors.Pre-merge author checklist
Pre-merge reviewer checklist
🤖 Generated with Claude Code