You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Dynamic tag referencing a Marko.Template imported from a separate module crashes client hydration in production builds — reproduces with @marko/run, not with plain @marko/vite #266
this issue and the reproduction repo were written by Claude, but I did personally verify that it's real.
Reproduction repo: https://github.com/svallory/marko-run-dynamic-tag-repro — the crashing @marko/run variant at the root, the non-crashing plain @marko/vite counterpart under plain/, README has the one-line failing/working switch.
Environment
marko: 6.3.34 and 6.3.35 (latest at time of writing) — both confirmed
@marko/run: 0.11.8 and 0.11.9 (latest) — both confirmed (crashing variant; re-verified after bun update --latest)
@marko/vite: 6.1.9 (non-crashing plain variant, same trigger shape)
vite: 8.2.1
Node: 22.15.1
bun: 1.3.11
OS: macOS (Darwin), also unrelated to platform as far as I can tell
Summary
Rendering a component through a dynamic tag (<${Component}/>) throws a TypeError during client hydration, but only in a production build (marko-run build) served statically — the Vite dev server never reproduces it.
The trigger is narrow: the Component reference must come from an object literal (a Record<string, Marko.Template>-shaped manifest) that is declared in a separate module and imported, then indexed by a runtime-determined string key (in my repro, a route param). The identical lookup against the identical object, when the object is instead declared as an inline <const> literal directly in the .marko file doing the rendering, does not crash.
I built two repros with the identical trigger shape — same manifest pattern, same nested-layout structure, same dynamic tag — one routed through @marko/run, one using plain @marko/vite with a hand-rolled Node HTTP server and no router. The @marko/run variant crashes. The plain @marko/vite variant does not (verified with real, non-scripted clicks incrementing a hydrated counter 0 → 1 → 2 → 3, zero console errors). That's why this is filed here rather than at marko-js/marko: the evidence points at something in @marko/run's generated route/client entries, not the marko compiler or @marko/vite's bundling in isolation. (I have not ruled out that marko-js/marko is still the right owner if the root cause turns out to be a compiler edge case that @marko/run's route codegen happens to trigger and @marko/vite's linked-mode codegen happens not to — filing here first since it's the narrower, better-evidenced claim, and I'm happy to move it or cross-link if a maintainer determines otherwise.)
Two layered layouts (src/routes/+layout.marko and src/routes/docs/+layout.marko) each thread content through with their own <${input.content}/> dynamic tag, matching the route nesting depth of the app this was bisected from.
Steps:
bun install
bun run build (i.e. marko-run build)
bun run start (i.e. marko-run preview) and open /docs/components/button
Open the browser console
Observed: on page load,
TypeError: e[(t++)] is not a function
(minified). With an unminified/sourcemapped build the same throw unminifies to effects[(i++)] is not a function, originating in runEffects, called from runResumeEffects → init in Marko's compiled DOM runtime. The SSR HTML itself is correct (view-source shows the rendered button), but no client-side event listeners attach — the counter button is dead after the crash.
Expected: the page hydrates cleanly, the same as when the dev server serves it, and the same as the plain-@marko/vite variant below.
Non-crashing counterpart — plain @marko/vite, no @marko/run
Same manifest, same demo component, same nested-dynamic-tag-content layout structure, composed by hand instead of by a router:
src/page.marko (SSR entry, composes the equivalent of the two layouts above plus the component-page lookup — see the attached repro for full
file contents):
ComponentPage (component-page.marko) does the identical DEMOS[input.name] lookup against the identical tiny-manifest.ts, rendered via the identical <${Selected}/>.
Built with two sequential vite build() calls (SSR then client — the same sequence @marko/run's own CLI uses internally, see node_modules/@marko/run/dist/cli/index.mjs), served by a ~40-line plain Node http server calling template.render(input).
Observed: no console error. template.render()'s output hydrates correctly; clicking the button (real mouse clicks, not scripted) increments the counter 0 → 1 → 2 → 3 reliably across repeated clicks.
What changes the outcome (within the @marko/run variant)
Replacing only the manifest's declaration site — nothing else in the file changes — removes the crash:
This was bisected from a larger app (a ~76-component docs site) where the same crash appeared on every /docs/components/<name> page, in two independent locations: once with the dynamic tag written directly in a route file, and once with it one level removed inside a custom tag that the route used. In both cases the common factor was the same: a dynamic tag whose component reference was resolved from a runtime lookup (a route param, or a manifest keyed by string) against an object imported from a separate module, as opposed to a statically-known tag reference or an inline object literal.
During minimization, several things were ruled out as necessary conditions: manifest size (one entry crashes the same as hundreds), a <for> loop or multiple call sites on the page (a single call site crashes), a custom-tag boundary around the dynamic tag (crashes with it written directly in the route file), and any interactive chrome (<lifecycle onMount>, <id>-generated DOM ids, tabs) around the dynamic tag (a bare <div><${Selected}/></div> is enough). The one variable that consistently flipped the outcome across every trial (within the @marko/run variant) was the manifest object's declaration site: imported from another module vs. an inline <const> literal in the same file. The plain-@marko/vite variant above adds a second axis: the same "imported from another module" shape, routed without @marko/run, does not crash at all — so whatever @marko/run generates for its route/client entries appears to be a necessary ingredient too, on top of the import-vs-inline distinction.
Suspected cause
Not confirmed, but the shape of the bug (works in dev, breaks only in a minified/bundled production build, only for a very specific rendering pattern, and only through @marko/run's route codegen) strongly suggests a scope-id allocation mismatch between what the compiler assigns to the dynamic-tag branch at compile time and what the SSR-emitted resume payload actually encodes for the client to walk during hydration — i.e. the compiler and the SSR runtime disagree about how many scope slots this branch occupies, and that disagreement only surfaces once the effects array gets walked by the compiled runEffects walker. Given the plain @marko/vite variant doesn't reproduce it, whatever @marko/run's route-file compilation or client-entry generation does differently from @marko/vite's own linked-mode entry generation is likely the other half of the trigger — I did not dig into that diff, since it's @marko/run's own codegen and better understood by that project's maintainers.
Additional notes
Confirmed present on both marko 6.3.34 and 6.3.35 (latest).
Both repros (crashing @marko/run variant and non-crashing plain @marko/vite variant) live in https://github.com/svallory/marko-run-dynamic-tag-repro, structured to make the diff between them as small as possible (same manifest file content, same demo component, same nested dynamic-tag-content layout shape) specifically so the only real variable is the routing layer.
Note
this issue and the reproduction repo were written by Claude, but I did personally verify that it's real.
Reproduction repo: https://github.com/svallory/marko-run-dynamic-tag-repro — the crashing
@marko/runvariant at the root, the non-crashing plain@marko/vitecounterpart underplain/, README has the one-line failing/working switch.Environment
bun update --latest)Summary
Rendering a component through a dynamic tag (
<${Component}/>) throws aTypeErrorduring client hydration, but only in a production build (marko-run build) served statically — the Vite dev server never reproduces it.The trigger is narrow: the
Componentreference must come from an object literal (aRecord<string, Marko.Template>-shaped manifest) that is declared in a separate module and imported, then indexed by a runtime-determined string key (in my repro, a route param). The identical lookup against the identical object, when the object is instead declared as an inline<const>literal directly in the.markofile doing the rendering, does not crash.I built two repros with the identical trigger shape — same manifest pattern, same nested-layout structure, same dynamic tag — one routed through
@marko/run, one using plain@marko/vitewith a hand-rolled Node HTTP server and no router. The@marko/runvariant crashes. The plain@marko/vitevariant does not (verified with real, non-scripted clicks incrementing a hydrated counter 0 → 1 → 2 → 3, zero console errors). That's why this is filed here rather than at marko-js/marko: the evidence points at something in@marko/run's generated route/client entries, not themarkocompiler or@marko/vite's bundling in isolation. (I have not ruled out that marko-js/marko is still the right owner if the root cause turns out to be a compiler edge case that@marko/run's route codegen happens to trigger and@marko/vite's linked-mode codegen happens not to — filing here first since it's the narrower, better-evidenced claim, and I'm happy to move it or cross-link if a maintainer determines otherwise.)Minimal reproduction — crashing (@marko/run)
src/demos/tiny-manifest.ts:src/demos/button/button-demo.marko:src/routes/docs/components/$name/+page.marko(a@marko/runroute file,param
$name):Two layered layouts (
src/routes/+layout.markoandsrc/routes/docs/+layout.marko) each thread content through with their own<${input.content}/>dynamic tag, matching the route nesting depth of the app this was bisected from.Steps:
bun installbun run build(i.e.marko-run build)bun run start(i.e.marko-run preview) and open/docs/components/buttonObserved: on page load,
(minified). With an unminified/sourcemapped build the same throw unminifies to
effects[(i++)] is not a function, originating inrunEffects, called fromrunResumeEffects→initin Marko's compiled DOM runtime. The SSR HTML itself is correct (view-source shows the rendered button), but no client-side event listeners attach — the counter button is dead after the crash.Expected: the page hydrates cleanly, the same as when the dev server serves it, and the same as the plain-
@marko/vitevariant below.Non-crashing counterpart — plain @marko/vite, no @marko/run
Same manifest, same demo component, same nested-dynamic-tag-content layout structure, composed by hand instead of by a router:
src/page.marko(SSR entry, composes the equivalent of the two layouts above plus the component-page lookup — see the attached repro for fullfile contents):
ComponentPage(component-page.marko) does the identicalDEMOS[input.name]lookup against the identicaltiny-manifest.ts, rendered via the identical<${Selected}/>.Built with two sequential
vite build()calls (SSR then client — the same sequence@marko/run's own CLI uses internally, seenode_modules/@marko/run/dist/cli/index.mjs), served by a ~40-line plain Nodehttpserver callingtemplate.render(input).Observed: no console error.
template.render()'s output hydrates correctly; clicking the button (real mouse clicks, not scripted) increments the counter 0 → 1 → 2 → 3 reliably across repeated clicks.What changes the outcome (within the @marko/run variant)
Replacing only the manifest's declaration site — nothing else in the file changes — removes the crash:
This was bisected from a larger app (a ~76-component docs site) where the same crash appeared on every
/docs/components/<name>page, in two independent locations: once with the dynamic tag written directly in a route file, and once with it one level removed inside a custom tag that the route used. In both cases the common factor was the same: a dynamic tag whose component reference was resolved from a runtime lookup (a route param, or a manifest keyed by string) against an object imported from a separate module, as opposed to a statically-known tag reference or an inline object literal.During minimization, several things were ruled out as necessary conditions: manifest size (one entry crashes the same as hundreds), a
<for>loop or multiple call sites on the page (a single call site crashes), a custom-tag boundary around the dynamic tag (crashes with it written directly in the route file), and any interactive chrome (<lifecycle onMount>,<id>-generated DOM ids, tabs) around the dynamic tag (a bare<div><${Selected}/></div>is enough). The one variable that consistently flipped the outcome across every trial (within the@marko/runvariant) was the manifest object's declaration site: imported from another module vs. an inline<const>literal in the same file. The plain-@marko/vitevariant above adds a second axis: the same "imported from another module" shape, routed without@marko/run, does not crash at all — so whatever@marko/rungenerates for its route/client entries appears to be a necessary ingredient too, on top of the import-vs-inline distinction.Suspected cause
Not confirmed, but the shape of the bug (works in dev, breaks only in a minified/bundled production build, only for a very specific rendering pattern, and only through
@marko/run's route codegen) strongly suggests a scope-id allocation mismatch between what the compiler assigns to the dynamic-tag branch at compile time and what the SSR-emitted resume payload actually encodes for the client to walk during hydration — i.e. the compiler and the SSR runtime disagree about how many scope slots this branch occupies, and that disagreement only surfaces once the effects array gets walked by the compiledrunEffectswalker. Given the plain@marko/vitevariant doesn't reproduce it, whatever @marko/run's route-file compilation or client-entry generation does differently from @marko/vite's own linked-mode entry generation is likely the other half of the trigger — I did not dig into that diff, since it's @marko/run's own codegen and better understood by that project's maintainers.Additional notes
@marko/runvariant and non-crashing plain@marko/vitevariant) live inhttps://github.com/svallory/marko-run-dynamic-tag-repro, structured to make the diff between them as small as possible (same manifest file content, same demo component, same nested dynamic-tag-content layout shape) specifically so the only real variable is the routing layer.