Skip to content

Nitro-in-Vite: intermediate chunks re-transformed, duplicate class registration crashes SSR with "Cannot redefine property: classId" #2894

Description

@ryantbrown

Description

In the Nitro-in-Vite pipeline (e.g. TanStack Start with nitro/vite), the workflow SWC transform runs a second time over Nitro's own intermediate chunks. Any serialization class that was correctly registered in the first pass gets a second inlined registration IIFE appended in the final server bundle. Since the IIFE defines classId as non-configurable (#1480), the second Object.defineProperty on the same class throws at module load, and every SSR request 500s:

TypeError: Cannot redefine property: classId
    at Object.defineProperty (<anonymous>)
    at file:///var/task/_ssr/dist-D8JevQNu.mjs:15277:9
    at ModuleJob.run (node:internal/modules/esm/module_job:439:25)
    ...
    at async loadEntries (file:///var/task/_ssr/ssr.mjs:13810:52)

The final chunk contains both registrations for the same class object — first the correct package-derived id, then a chunk-path-derived one (on workflow@5.0.0-beta.31 we see the path-derived registration up to 3×):

// pass 1 — correct
})(GatewayEmbeddingModel, "class//@ai-sdk/gateway@4.0.16//GatewayEmbeddingModel");

// pass 2 — re-transform of Nitro's intermediate chunk, crashes at load
})(GatewayEmbeddingModel, "class//./node_modules/.nitro/vite/services/ssr/assets/dist-Bhk9b09n//GatewayEmbeddingModel");

Root cause

packages/nitro/src/index.ts unshifts workflowTransformPlugin in rollup:before with only the pre-built workflow bundles excluded:

nitro.hooks.hook('rollup:before', (_nitro, config) => {
  config.plugins.unshift(
    workflowTransformPlugin({
      // Exclude pre-built workflow bundles from re-transformation
      exclude: [workflowBuildDir],
    })
  );
});

Nitro's second build pass consumes the Vite environment build's output (node_modules/.nitro/vite/services/ssr/assets/*). Those chunks still contain serde markers (static [WORKFLOW_SERIALIZE], Symbol.for("workflow-serialize")) from bundled dependencies, so shouldTransformFile matches and the chunk is transformed again. The same gap exists for the Vite-level transform plugin, which also runs in the Nitro environment.

Reproduction conditions

  1. TanStack Start (or any Nitro-in-Vite app) + workflow/vite
  2. ssr.noExternal: true (bundle dependencies into SSR chunks — needed e.g. under bun's isolated installs)
  3. Any dependency with WDK serde-marked classes in the SSR graph — @ai-sdk/gateway (GatewayLanguageModel et al.) or @vercel/sandbox

The workbench apps don't hit this because they externalize SSR deps, so serde-marked code never sits inside an intermediate chunk.

Reproduced on: workflow@5.0.0-beta.30 and 5.0.0-beta.31, nitro@3.0.260610-beta, vite@8.1.4, @tanstack/react-start@1.168.27, bun 1.3.12, nitro({ preset: "vercel" }).

Suggested fix

Exclude Nitro's intermediate build assets from re-transformation (mirroring the existing workflowBuildDir exclusion), or make the transform idempotent for already-registered classes (e.g. skip emitting a registration when the module already contains the registry IIFE for that class, or guard the inlined defineProperty).

Workaround

Wrapping every workflow:transform plugin instance (the Vite-level one and the one unshifted in rollup:before) to return null for ids containing /.nitro/ restores a correct bundle: each class registered exactly once under its package-derived id, server boots cleanly.

How did you test your changes?

n/a — bug report. Verified the mechanism by importing the built chunk directly (node -e "import('./_ssr/dist-….mjs')"), which reproduces the crash, and confirming the double registration in the emitted code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions