MetaMask / MetaMask/metamask-extension
webpack: `--no-lavamoat` production builds emit a dead service worker — html-bundler-webpack-plugin deletes an SW chunk on chunk-id collision
- Dominant language
- TypeScript
- Stars
- 13.2k
- Forks
- 5.6k
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 451
Description
### What is this about?
Production webpack builds with `--no-lavamoat` (the configuration your own e2e builds use) deterministically emit a **dead extension**: the MV3 service worker's chunk map references a webpack chunk that was deleted from the output, so `importScripts` fails and the background never initializes. The build exits 0 — nothing in the pipeline notices.
Root cause is an upstream bug in `html-bundler-webpack-plugin` (pinned `4.23.2`; present in every version back to at least `3.17.3`), reported with a full analysis at **webdiscus/html-bundler-webpack-plugin#194**: its "remove generated unused split chunks" pass matches chunks by **id-set intersection** with the HTML pages' split-chunk ids but deletes by **filename**. Under `--no-lavamoat`, webpack's chunk de-duplication merges ids across runtimes — in v13.45.0/v13.45.1 the service worker's async chunk reports `ids=[1690, 8717]`, and `8717` is also the id of the HTML pages' `vendor-24e2b4df` split chunk — so the plugin deletes `1690.38d93430f4639781ca25.js` at `processAssets` stage ~2999, after `__webpack_require__.u` was already baked into `service-worker.js`. LavaMoat builds happen not to collide, which is why the default build is unaffected.
### Scenario
```bash
git clone --depth 1 --branch v13.45.1 https://github.com/MetaMask/metamask-extension
cd metamask-extension
corepack enable && yarn install --immutable
yarn webpack:tsc
node development/.webpack/launch.js --no-cache --mode production --no-lavamoat --no-snow --zip --browser chrome
```
Result: `dist/chrome`'s service worker references chunk `1690.38d93430f4639781ca25.js`; the file does not exist anywhere in the output. Loading the extension leaves the SW permanently uninitialized (`stateHooks` shows only `["lazyListener"]`, chunk fetch fails). Reproduces identically on v13.45.0.
### Technical Details
`processAssets`-stage trace of the deletion window:
```
stage 2501: renameAsset 1690.bb573385dbeb87c34e9c.js -> 1690.38d93430f4639781ca25.js
deleteAsset 1690.38d93430f4639781ca25.js
stage 2999: asset count 858 -> 856 (chunk file + its .map gone)
```
The deleted chunk was never injected into any HTML page — it belongs exclusively to the service-worker entry.
Two mitigations we field-tested while building automation artifacts from your source (happy to share either):
- a minimal correction of the plugin's deletion pass (filename membership in the HTML entries' own chunk groups instead of id intersection) — proposed upstream in webdiscus/html-bundler-webpack-plugin#194 as PR material;
- `optimization.mergeDuplicateChunks: false` in `development/webpack/webpack.config.ts` would remove the id-merge precondition, at some bundle-size cost (untested by us end-to-end).
A cheap defense regardless of the plugin fix: a post-build assertion that every chunk referenced by any emitted `__webpack_require__.u` runtime actually exists in the output — that check is what caught this.
Contributor guide
Research direction
Reproduce the production build with `development/.webpack/launch.js --no-lavamoat` and inspect `development/webpack/webpack.config.ts`, then compare emitted service-worker references with files in `dist/chrome`. Review the linked upstream plugin analysis and choose a project-side mitigation or verification path. Done means the build no longer emits a service worker that references a missing chunk, with the regression covered by the relevant build check.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript, webpack
- Domain
- build-system, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100