You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I am using Vite's SSR module loader (both server.ssrLoadModule and the Vite 8 environment API server.environments.ssr.runner.import) to evaluate modules that use export * as X from './…' in an import cycle.
What I expect — per ES module spec, and as demonstrated by native import() under both Node and Bun — is that accessing the re-exported namespace during the cycle window throws ReferenceError: Cannot access 'X' before initialization. The namespace object itself is stable; reading a member whose inner binding is in TDZ throws loudly.
What actually happens — Vite's SSR transform wraps every exported binding in try { return ${local} } catch {} (the defineExport helper in packages/vite/src/node/ssr/ssrTransform.ts, hoisted above the await __vite_ssr_import__(...) lines). When the cycle peer reads the re-export before its const is initialized, the catch {} silently swallows the TDZ ReferenceError and returns undefined — for the entire namespace binding.
Consumer code like const Root = Namespace.Root at module top-level then crashes downstream with a confusing TypeError: undefined is not an object (evaluating '…') far from the real cause.
The try/catch is self-flagged as provisional in PR #19959 (merged 2025-05-02, commit fd38d07, first shipped in 6.3.5):
We'll revisit whether we should remove try/catch after investigating that further.
This issue is the follow-up — with a deterministic repro and a confirmation that it is not specific to the runtime (both node and bun), not specific to the vite version (reproduces on 7.3.2 stable and 8.0.0), and not specific to the legacy API (reproduces on the new environment API too).
Not submitting a PR — leaving the design call to maintainers (options in the "Steps" notes below).
Drafted and researched by Claude Opus 4.7; reviewed and filed by me.
Reproduction
Minimal repro (4 JS files + driver, ~30 lines of user code total). I can push to a public repo and paste the URL here if needed. Full source included in Steps.
Steps to reproduce
Create a project:
mkdir ssr-cycle-repro &&cd ssr-cycle-repro
bun init -y
bun add -d [email protected]
file:///.../modules/cyc-b.js:7
export const NsKeys = NsFromA ? Object.keys(NsFromA) : 'NsFromA_IS_UNDEFINED';
^
ReferenceError: Cannot access 'a' before initialization
at Object.keys (<anonymous>)
at file:///.../modules/cyc-b.js:7:40
Spec-correct TDZ throw. Vite's SSR loader swallows it.
Coverage matrix (reproduced identically)
Runtime
Vite version
API
Result
Node 25.6.1
8.0.0
server.ssrLoadModule
silent undefined
Bun 1.3.13
8.0.0
server.ssrLoadModule
silent undefined
Node 25.6.1
8.0.0
server.environments.ssr.runner.import
silent undefined
Bun 1.3.13
8.0.0
server.environments.ssr.runner.import
silent undefined
Node 25.6.1
7.3.2 stable
server.ssrLoadModule
silent undefined
Bun 1.3.13
7.3.2 stable
server.ssrLoadModule
silent undefined
Not rolldown-vite-specific. Not runtime-specific. defineExport helper unchanged on main as of 2026-04-21.
Production build (vite build --ssr) — also silently-undefines, different mechanism
For completeness: running the same three files through vite build --ssr and executing the built artifact under node or bun prints the same fallback strings:
This is a different mechanism (rolldown var hoisting vs. SSR transform getter with try/catch) but the same spec-divergent outcome. I'm filing this issue specifically about the SSR transform because that's the one with the self-flagged try/catch TODO — but flagging the build-side behavior in case it's relevant to a holistic fix.
Transform output from Vite (identical across 7.3.2 and 8.0.0)
The getter for Ns is hoisted above the const __vite_ssr_import_1__ = await …. When cyc-b evaluates inside the cycle and reads Ns on the partially-initialized cyc-a module, the getter hits TDZ on __vite_ssr_import_1__, the catch {} swallows, and the getter returns undefined.
Root cause pointer
packages/vite/src/node/ssr/ssrTransform.ts, defineExport helper in transformExports (current main as of 2026-04-21):
functiondefineExport(name: string,local=name){// wrap with try/catch to fallback to 'undefined' for backward compat.s.appendLeft(fileStartIndex,`${ssrExportNameKey}(${JSON.stringify(name)}, () => { try { return ${local} } catch {} });\n`,);}
History of this line
fix(ssr): hoist export to handle cyclic import better #18983 (merged 2025-03-26) — fix(ssr): hoist export to handle cyclic import better. Hoisted the export registration above the await __vite_ssr_import__(...) calls — the hoist is what creates the TDZ window.
Hits as a confusing error in dev SSR for shadcn-svelte-style barrels over bits-ui:
import{TooltipasTooltipPrimitive}from'bits-ui';constRoot=TooltipPrimitive.Root;// TooltipPrimitive === undefined during cycle
[email protected]/dist/bits/tooltip/index.js is literally export * as Tooltip from "./exports.js"; — the exact pattern this bug targets. Users see:
(ssr) Error when evaluating SSR module .../+layout.svelte: undefined is not an object (evaluating '__vite_ssr_import_0__.Tooltip.Root')
The error shape matches what this issue describes: a namespace import that should be the bits-ui Tooltip namespace is undefined, and the downstream destructure (const Root = TooltipPrimitive.Root in the shadcn barrel) crashes. I haven't proven the exact cycle mechanism in every real user report — the trigger in practice is often moduleGraph.invalidateAll() (e.g., via reloadOnTsconfigChange) combined with whatever re-evaluation ordering the runner picks, which may create transient states that look like cycles from the getter's point of view. The minimal source-level cycle above is the smallest deterministic way I found to surface the spec-divergent silent undefined.
Related prior report (closed as "user config", same stack-trace shape on a shadcn barrel, triggered by tailwind.config.js HMR invalidation rather than tsconfig): huntabyte/shadcn-svelte#1718.
Known workarounds (user-side, don't fix root cause)
optimizeDeps.include: ['bits-ui'] (or equivalent) — forces pre-bundling, SSR transform never runs on it.
Don't destructure the namespace at module top level; read at usage site so init has completed.
Throw a Vite-specific error naming the module/export when local is uninitialized — clearer than raw TDZ.
Replace the hoisted-getter approximation with a pattern that keeps the namespace object stable (not undefined) and TDZ-throws only on individual member reads — closest to ESM namespace semantics.
Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
Make sure this is a Vite issue and not a framework-specific issue. For example, if it's a Vue SFC related bug, it should likely be reported to vuejs/core instead.
Describe the bug
I am using Vite's SSR module loader (both
server.ssrLoadModuleand the Vite 8 environment APIserver.environments.ssr.runner.import) to evaluate modules that useexport * as X from './…'in an import cycle.What I expect — per ES module spec, and as demonstrated by native
import()under both Node and Bun — is that accessing the re-exported namespace during the cycle window throwsReferenceError: Cannot access 'X' before initialization. The namespace object itself is stable; reading a member whose inner binding is in TDZ throws loudly.What actually happens — Vite's SSR transform wraps every exported binding in
try { return ${local} } catch {}(thedefineExporthelper inpackages/vite/src/node/ssr/ssrTransform.ts, hoisted above theawait __vite_ssr_import__(...)lines). When the cycle peer reads the re-export before itsconstis initialized, thecatch {}silently swallows the TDZReferenceErrorand returnsundefined— for the entire namespace binding.Consumer code like
const Root = Namespace.Rootat module top-level then crashes downstream with a confusingTypeError: undefined is not an object (evaluating '…')far from the real cause.The
try/catchis self-flagged as provisional in PR #19959 (merged 2025-05-02, commitfd38d07, first shipped in 6.3.5):This issue is the follow-up — with a deterministic repro and a confirmation that it is not specific to the runtime (both node and bun), not specific to the vite version (reproduces on 7.3.2 stable and 8.0.0), and not specific to the legacy API (reproduces on the new environment API too).
Not submitting a PR — leaving the design call to maintainers (options in the "Steps" notes below).
Reproduction
Minimal repro (4 JS files + driver, ~30 lines of user code total). I can push to a public repo and paste the URL here if needed. Full source included in Steps.
Steps to reproduce
modules/sub.js:modules/cyc-a.js:modules/cyc-b.js:test-cycle.js:node test-cycle.jsExpected output (if behavior matched native ESM): a
ReferenceErrorduring cycle resolution.Actual output:
Native-ESM comparison — same three
modules/*.jsfiles, no Vite:$ node -e "import('./modules/cyc-a.js').then(async () => { const b = await import('./modules/cyc-b.js'); console.log(b.NsKeys, b.maybeA); })"Spec-correct TDZ throw. Vite's SSR loader swallows it.
Coverage matrix (reproduced identically)
server.ssrLoadModuleundefinedserver.ssrLoadModuleundefinedserver.environments.ssr.runner.importundefinedserver.environments.ssr.runner.importundefinedserver.ssrLoadModuleundefinedserver.ssrLoadModuleundefinedNot rolldown-vite-specific. Not runtime-specific.
defineExporthelper unchanged onmainas of 2026-04-21.Production build (
vite build --ssr) — also silently-undefines, different mechanismFor completeness: running the same three files through
vite build --ssrand executing the built artifact under node or bun prints the same fallback strings:The built output shows
var-hoisting ordering the dependency read before the dependency is defined:This is a different mechanism (rolldown
varhoisting vs. SSR transform getter with try/catch) but the same spec-divergent outcome. I'm filing this issue specifically about the SSR transform because that's the one with the self-flaggedtry/catchTODO — but flagging the build-side behavior in case it's relevant to a holistic fix.Transform output from Vite (identical across 7.3.2 and 8.0.0)
The getter for
Nsis hoisted above theconst __vite_ssr_import_1__ = await …. Whencyc-bevaluates inside the cycle and readsNson the partially-initializedcyc-amodule, the getter hits TDZ on__vite_ssr_import_1__, thecatch {}swallows, and the getter returnsundefined.Root cause pointer
packages/vite/src/node/ssr/ssrTransform.ts,defineExporthelper intransformExports(currentmainas of 2026-04-21):History of this line
fix(ssr): hoist export to handle cyclic import better. Hoisted the export registration above theawait __vite_ssr_import__(...)calls — the hoist is what creates the TDZ window.v6.3.3: Cannot access '__vite_ssr_export_default__' before initialization. TDZ regression from fix(ssr): hoist export to handle cyclic import better #18983.fd38d07, first shipped in 6.3.5) —fix(ssr): handle uninitialized export access as undefined. Added thetry { } catch { }as a mitigation for v6.3.3 ReferenceError: Cannot access '__vite_ssr_export_default__' before initialization #19929 with an explicit "revisit" note.Real-world impact
Hits as a confusing error in dev SSR for shadcn-svelte-style barrels over
bits-ui:[email protected]/dist/bits/tooltip/index.jsis literallyexport * as Tooltip from "./exports.js";— the exact pattern this bug targets. Users see:The error shape matches what this issue describes: a namespace import that should be the bits-ui
Tooltipnamespace isundefined, and the downstream destructure (const Root = TooltipPrimitive.Rootin the shadcn barrel) crashes. I haven't proven the exact cycle mechanism in every real user report — the trigger in practice is oftenmoduleGraph.invalidateAll()(e.g., viareloadOnTsconfigChange) combined with whatever re-evaluation ordering the runner picks, which may create transient states that look like cycles from the getter's point of view. The minimal source-level cycle above is the smallest deterministic way I found to surface the spec-divergent silentundefined.Related prior report (closed as "user config", same stack-trace shape on a shadcn barrel, triggered by
tailwind.config.jsHMR invalidation rather than tsconfig): huntabyte/shadcn-svelte#1718.Known workarounds (user-side, don't fix root cause)
optimizeDeps.include: ['bits-ui'](or equivalent) — forces pre-bundling, SSR transform never runs on it.Proposed fix directions (maintainer's call)
try/catch. LetReferenceErrorpropagate; matches native ESM. Likely regresses v6.3.3 ReferenceError: Cannot access '__vite_ssr_export_default__' before initialization #19929 unless combined with (3).localis uninitialized — clearer than raw TDZ.undefined) and TDZ-throws only on individual member reads — closest to ESM namespace semantics.Related issues for hygiene
System Info
System: OS: macOS 15.7.3 CPU: (10) arm64 Apple M4 Memory: 97.02 MB / 24.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 25.6.1 - /opt/homebrew/bin/node npm: 11.9.0 - /opt/homebrew/bin/npm pnpm: 10.32.1 - /opt/homebrew/bin/pnpm bun: 1.3.13 - /opt/homebrew/bin/bun Deno: 2.7.4 - /opt/homebrew/bin/deno Browsers: Safari: 26.2 npmPackages: vite: 8.0.0 => 8.0.0Used Package Manager
bun
Logs
Click to expand
Validations