Describe the bug
Description
Vite module runner can expose an incomplete facade namespace during cold SSR evaluation when a facade imports a module, later re-exports * from the same module, and an intervening dependency cycles back to the facade.
This can cause valid native ESM to fail in SSR with runtime errors like:
TypeError: createThing is not a function
Reduced Module Graph
facade.js
|
| import { createStart } from './core.js'
v
core.js
facade.js
|
| export { client } from './client.js'
v
client.js
|
v
start.js
|
v
middleware.js
|
| import { createThing } from './facade.js'
v
facade.js
|
| returns partial in-flight namespace
v
missing createThing from later export * from './core.js'
Reduced Source Shape
// facade.js
import { createStart } from './core.js'
export { client } from './client.js'
export * from './core.js'
export { createStart }
// core.js
export var createThing = (value) => value
export var createStart = () => {
return { started: true }
}
// client.js
import { start } from './start.js'
export const client = start()
// start.js
import { createStart } from './facade.js'
import { middleware } from './middleware.js'
export function start() {
return { ...createStart(), middleware }
}
// middleware.js
import { createThing } from './facade.js'
export const middleware = createThing('middleware')
Expected Behavior
This works in native ESM:
node --input-type=module -e "import('./facade.js').then((m) => console.log(m.client))"
Expected output:
{ started: true, middleware: 'middleware' }
The circular import observes createThing through the facade because facade.js already imported ./core.js before evaluating the client re-export.
Actual Behavior
In Vite SSR/module-runner, the circular import can observe the facade before the later transformed export * from './core.js' has registered its runtime __vite_ssr_exportAll__() getters.
The facade namespace is returned as partial exports, but it does not contain the wildcard export yet.
Result:
TypeError: createThing is not a function
Real-World Case
This appears in TanStack Start cold SSR startup (see TanStack/router#7459).
The public @tanstack/react-start facade imports @tanstack/react-start-client, and that client path eventually imports #tanstack-start-entry, which points back to the app's src/start.ts.
At the same time, app code imports createStart / createMiddleware from @tanstack/react-start.
Simplified graph:
src/start.ts
|
| import { createStart } from '@tanstack/react-start'
v
@tanstack/react-start
|
| import { Hydrate } from '@tanstack/react-start-client'
v
@tanstack/react-start-client
|
| export { hydrateStart }
v
@tanstack/start-client-core/client/hydrateStart
|
| import { startInstance } from '#tanstack-start-entry'
v
src/start.ts
|
| imports middleware
v
middleware.ts
|
| import { createMiddleware } from '@tanstack/react-start'
v
@tanstack/react-start
|
| partial in-flight facade namespace
v
missing createMiddleware from export * from '@tanstack/start-client-core'
The package facade has this emitted shape:
import { createServerFn } from '@tanstack/start-client-core'
import { Hydrate } from '@tanstack/react-start-client'
export * from '@tanstack/start-client-core'
export { Hydrate, createServerFn, useServerFn }
Since createMiddleware is only available through export *, Vite's partial namespace does not include it when the cycle returns.
Reproduction
see above
Steps to reproduce
No response
System Info
reproduced with vite 8.0.14
Used Package Manager
pnpm
Logs
No response
Validations
Describe the bug
Description
Vite module runner can expose an incomplete facade namespace during cold SSR evaluation when a facade imports a module, later re-exports
*from the same module, and an intervening dependency cycles back to the facade.This can cause valid native ESM to fail in SSR with runtime errors like:
Reduced Module Graph
Reduced Source Shape
Expected Behavior
This works in native ESM:
node --input-type=module -e "import('./facade.js').then((m) => console.log(m.client))"Expected output:
{ started: true, middleware: 'middleware' }The circular import observes
createThingthrough the facade becausefacade.jsalready imported./core.jsbefore evaluating the client re-export.Actual Behavior
In Vite SSR/module-runner, the circular import can observe the facade before the later transformed
export * from './core.js'has registered its runtime__vite_ssr_exportAll__()getters.The facade namespace is returned as partial exports, but it does not contain the wildcard export yet.
Result:
Real-World Case
This appears in TanStack Start cold SSR startup (see TanStack/router#7459).
The public
@tanstack/react-startfacade imports@tanstack/react-start-client, and that client path eventually imports#tanstack-start-entry, which points back to the app'ssrc/start.ts.At the same time, app code imports
createStart/createMiddlewarefrom@tanstack/react-start.Simplified graph:
The package facade has this emitted shape:
Since
createMiddlewareis only available throughexport *, Vite's partial namespace does not include it when the cycle returns.Reproduction
see above
Steps to reproduce
No response
System Info
Used Package Manager
pnpm
Logs
No response
Validations