Identical app code that runs fine on vite 6 (rollup) crashes on vite 8 (rolldown).
When two ES modules form a static-import cycle AND both are
dynamic-import targets, vite 8 (rolldown) merges them into one
chunk and lets rolldown name the chunk after one of the modules'
basenames. As a result, import('./action') resolves to a namespace
that contains both action's exports AND form's exports (renamed to
n, t).
App code that validates the namespace contract — like
Object.entries(mod).forEach(([k, v]) => { if (!isExpected(v)) throw … }),
which is the standard pattern for a module-as-registry — then throws,
crashing the app.
This is the underlying rolldown behavior that caused
Shopify Admin's iPhone WebContent OOM crashes during the
f_vite_8_client_bundle rollout (incident #22422). In our codebase
the same cycle-prevention pass (rolldown #9093) snowballed through
1,067 unrelated modules — 65 admin sections + refractor's 279 prismjs
grammars + vendor packages — into a single 38 MB loaders-common
chunk that tripped iPhone WKWebView's 1,536 MB WebContent process limit.
This repository extracts the underlying namespace-pollution mechanism into a 4-file standalone repro you can build and inspect locally without any of the admin-web tooling.
src/main.ts ─▶ await import('./form.ts') // dynamic-import target
─▶ await import('./action.ts') // dynamic-import target
src/form.ts ◀─── statically import each other ───▶ src/action.ts
(CYCLE)
main.ts represents app code: it dynamic-imports each module and
validates that the returned namespace contains exactly the keys it
declared. If extra keys appear, the app throws.
| script | bundler | result |
|---|---|---|
pnpm run build:v6 |
vite 6 / rollup | ✅ app runs — namespaces are clean |
pnpm run build:v8 |
vite 8 / rolldown | 💥 app crashes — action's namespace contains form's exports renamed to n, t |
pnpm install
pnpm run all========================================================================
vite 6 (rollup)
========================================================================
form.ts body in: form-<hash>.js
action.ts body in: form-<hash>.js
form namespace exports : [ 'callActionFromForm', 'formImpl' ]
action namespace exports: [ 'actionImpl', 'callFormFromAction' ]
✅ both modules pass their contract — app continues
✅ app ran successfully — no contract violation
========================================================================
vite 8 (rolldown)
========================================================================
form.ts body in: action-<hash>.js
action.ts body in: action-<hash>.js
form namespace exports : [ 'callActionFromForm', 'formImpl' ]
action namespace exports: [ 'actionImpl', 'callFormFromAction', 'n', 't' ]
💥 App crashed: action module contract violated:
expected exports [actionImpl, callFormFromAction],
got [actionImpl, callFormFromAction, n, t]
💥 APP CRASHED on vite 8: namespace pollution broke the contract
In the vite 8 build, open dist-v8/assets/action-<hash>.js and look at
the bottom:
export { actionImpl, callFormFromAction, formImpl as n, callActionFromForm as t };
// ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
// form's exports leaking out as n, tThat single physical chunk contains both form.ts and action.ts
(rolldown's cycle-prevention pass merged them). Because the chunk is
named action-<hash>.js (matching action.ts's basename), rolldown
makes the chunk's top-level exports the union of all members' symbols.
When main.ts does import('./action'), it resolves to that chunk
and gets the polluted namespace. Validation throws. App crashes.