Skip to content

SSR module runner exposes incomplete export * facade namespace in cold circular imports #22491

@schiller-manuel

Description

@schiller-manuel

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions