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
- TanStack Start (or any Nitro-in-Vite app) +
workflow/vite
ssr.noExternal: true (bundle dependencies into SSR chunks — needed e.g. under bun's isolated installs)
- 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.
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 definesclassIdas non-configurable (#1480), the secondObject.definePropertyon the same class throws at module load, and every SSR request 500s: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.31we see the path-derived registration up to 3×):Root cause
packages/nitro/src/index.tsunshiftsworkflowTransformPlugininrollup:beforewith only the pre-built workflow bundles excluded: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, soshouldTransformFilematches 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
workflow/vitessr.noExternal: true(bundle dependencies into SSR chunks — needed e.g. under bun's isolated installs)@ai-sdk/gateway(GatewayLanguageModelet al.) or@vercel/sandboxThe 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.30and5.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
workflowBuildDirexclusion), 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 inlineddefineProperty).Workaround
Wrapping every
workflow:transformplugin instance (the Vite-level one and the one unshifted inrollup:before) to returnnullfor 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.