Skip to content

fix(core): a data script in a sub-composition no longer stops every timeline - #5205

Draft
miguel-heygen wants to merge 4 commits into
mainfrom
fix/core-json-data-scripts
Draft

miguel-heygen wants to merge 4 commits into
mainfrom
fix/core-json-data-scripts

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

What changes for the user

A sub-composition that carries a data block, such as <script type="application/json" id="meta">{"title": "x"}</script>, no longer freezes the film. Before, every scene's timeline stayed static (including scenes without the data block) and hyperframes check reported page_error: Failed to execute 'replaceWith' on 'Element': Unexpected token ':'. Now the data block stays in the DOM as written and readable with document.getElementById("meta"), and every timeline plays.

Root cause

planCompositionAssembly (the one place both the compiler and the runtime loader decide a composition's scripts) listed every <script> regardless of type. The compiler then wrapped the JSON as a JavaScript IIFE, and the bundler merges all composition scripts (root and every sub-composition) into one <script>, so that single parse error stopped all of them.

Fix

  • compositionAssembly.ts: only scripts the browser runs (the HTML spec's JavaScript MIME types, module, importmap) are extracted. extractedCompositionAssets() replaces the selector at the three places that strip extracted assets from the mounted copy, so a data block stays in the content. The JavaScript MIME list moves here because this module must import nothing (it ships in the runtime bundle); scriptRuns.ts re-exports it.
  • htmlBundler.ts: a script run whose joined text does not parse is no longer merged. Each script stays its own <script>, so a broken script fails alone, the way separate authored scripts do in the browser. Chrome reports an inline script's SyntaxError without throwing from replaceWith, so the runtime's after-fonts loop keeps running the rest.
  • The render path (producer/src/services/htmlCompiler.ts) had its own copy of that merge with the same gap. Both now use one check, parsesAsScript, exported from the core compiler.
  • Escaping: a run left unmerged, and every scene-part script in sceneParts preview, now goes through the same comment strip and </script> escaping as merged runs. Scene-part scripts were never escaped on main, so a sub-composition script holding "</script>" in a string ended the script early in Studio preview.

No authoring format changes. Lint has nothing to flag: the data block is valid.

Proof

  • New tests: six in htmlBundler.test.ts (data block kept and readable, every runnable script parses, all three timelines' scripts survive; a sub-composition script that does not parse stays apart, with and without sceneParts; a script it does not merge is still escaped, including scene parts), one in compositionLoader.test.ts (runtime mount keeps the data block and does not inject it) and two in the producer's htmlCompiler.scriptOrder.test.ts (the same two cases on the render path). Each guard was undone on its own (per-run split, merge guard, producer guard, each escape) and only its own tests went red.
  • Touched test files pass three runs in a row: core htmlBundler 157, compositionAssembly 17, inlineSubCompositions 39, scriptRuns 12, htmlParityContract 2, compositionLoader 67, init.swapScenes 140, entry.afterFonts 11; producer htmlCompiler.scriptOrder 6, parity 13, moduleScripts 1, mediaType 3, fontEmbed 5, htmlCompiler.test.ts 140 (bun). tsc clean in core and producer.
  • Two repro projects that differ by that one line: check on the data-block project goes from ok=false to ok=true. Snapshots at 1s, 2.5s and 3.5s are now pixel-identical to the project without it, and both scenes move.

Before

before: every scene static at x=0

After

after: intro and scene animate

…imeline

A sub-composition's <script type="application/json"> was wrapped and run as
JavaScript, and because the bundle merges every composition script into one,
its parse error stopped every scene's timeline. Data scripts now stay in the
DOM as authored, and a script run that does not parse is left unmerged so a
bad script fails alone.
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Edit accuracy: accurate 2059 (base branch 2059), smooth 1552 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (1)

  • crop-none-pct-r0-nested-z50: tracking 0.05, pressJump 0, drop 0.07, reload 40.07, render 40.02, renderKey -, undo false, teleport true / tracking 0.05, pressJump 0, drop 0.07, reload 0.07, render 0.02, renderKey -, undo true, teleport true / tracking 0.05, pressJump 0, drop 0.07, reload 0.07, render 0.02, renderKey -, undo true, teleport true

…topping the rest

The render path's script merge had the same gap as the bundler: one script
that does not parse stopped every timeline. Both now share one parse check.
A run left unmerged, and every scene-part script, is still escaped, so a
string holding "</script>" can no longer end the script early in preview.
A script check now matches the browser (esbuild accepted export and top-level
await), so such a script can no longer stop every timeline. Inline template
compositions keep data scripts and run module and import map scripts as such.
A data script beside the composition root is mounted next to it, a nomodule
script never runs, and the render path merges scripts on a page with no head.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant