Skip to content

App Router prod build: circular chunk init crashes hydration on every route (RestorableClientStateController is not a constructor) — beta.0 & beta.1 #2620

Description

@reynard93

Summary

Production client bundles crash during hydration on every route with TypeError: <Class> is not a constructor. The failing class is vinext's own RestorableClientStateController, instantiated by AppBrowserHistoryController at the top level of the browser entry. vinext dev is fine; only vinext build output is broken. Reproduces on 1.0.0-beta.0 and 1.0.0-beta.1. Last working version for us is 0.0.46.

Because the crash happens at module-evaluation time in the client bootstrap, server-rendered HTML still appears but the page never hydrates: static pages look alive, while any client component (ours: an /internal route) freezes on its loading state, and client-side navigation is dead everywhere.

Environment

  • vinext: 1.0.0-beta.0 and 1.0.0-beta.1 (both reproduce)
  • Bundler: vinext-bundled vite = npm:@voidzero-dev/vite-plus-core (0.2.1 in beta.0, 0.2.2 in beta.1) — i.e. Rolldown
  • Peers: vite@8.0.10, @vitejs/plugin-rsc@0.5.26, @vitejs/plugin-react@6, react/react-dom@19.2.6
  • Target: Cloudflare Workers (@cloudflare/vite-plugin), App Router
  • Node: 24/25, macOS arm64

Error

Minified (beta.1):

TypeError: I is not a constructor
    at <client-entry chunk>

With build: { minify: false } the names resolve (beta.0 dist):

TypeError: RestorableClientStateController is not a constructor
    at AppBrowserHistoryController (index-*.js)   // this.#restorableClientState = new RestorableClientStateController({...})
    at <top-level of client entry> (index-*.js)   // const historyController = new AppBrowserHistoryController({...})

Root cause — circular chunk dependency

dist/server/app-browser-entry.js runs a top-level side effect:

const historyController = new AppBrowserHistoryController({
  initialHistoryState: window.history.state,
  maxHistoryStateSnapshots: 50,
  ...
});

and AppBrowserHistoryController's constructor does this.#restorableClientState = new RestorableClientStateController({ initialHistoryState, maxHistoryStateSnapshots }).

In the production build these two land in separate chunks with a direct cycle:

  • the entry/index chunk contains AppBrowserHistoryController + the top-level new AppBrowserHistoryController(...), and imports RestorableClientStateController from the navigation chunk;
  • the navigation chunk defines RestorableClientStateController and imports back from the index chunk.

Chunk execution enters navigation first; navigation imports index, which runs its top-level new AppBrowserHistoryController(...) before navigation has reached the RestorableClientStateController class definition. So the imported binding is still undefined → "is not a constructor".

0.0.46 (plain Vite/Rollup, no bundled vite-plus) handled this cycle correctly. dev works because native ESM evaluates the dependency before the importer's body. Only the Rolldown production chunking reorders it unsafely.

What I ruled out

  • Not minification — reproduces identically with build.rolldownOptions/build.minify:false.
  • Not treeshake side-effects — patching the client build's getClientTreeshakeConfig() from { moduleSideEffects: "no-external" } to { moduleSideEffects: true } had no effect.
  • Not app-fixable — vinext owns output.codeSplitting, so app-level build.rolldownOptions.output.manualChunks / advancedChunks are ignored ("advancedChunks/manualChunks option is ignored because the codeSplitting option is specified").

Reproduce

  1. App Router app on vinext@1.0.0-beta.1, Cloudflare target, Vite 8.
  2. vinext build then vinext start (or deploy).
  3. Load any route in a browser → console throws TypeError: … is not a constructor; page never hydrates. vinext dev is unaffected.
  4. Add build: { minify: false } to see the real names (RestorableClientStateController / AppBrowserHistoryController).

I haven't reduced this to a from-scratch minimal repo yet (it depends on the app's module graph producing the indexnavigation chunk split), but the failing code is entirely within vinext's own client runtime. Happy to provide a minimal reproduction repo if that helps triage.

Suggested direction

Either keep the interdependent runtime modules (app-browser-entry / app-browser-history-controller / navigation RestorableClientStateController) in one chunk, or avoid the top-level new AppBrowserHistoryController(...) side effect during client-entry evaluation (lazy/first-use init), so a cross-chunk cycle can't observe the class before it's defined.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions