Skip to content

fix: preview and bundled scripts run where and when the author put them - #5014

Merged
miguel-heygen merged 10 commits into
mainfrom
fix/core-bundler-keeps-local-script-order
Oct 4, 2026
Merged

miguel-heygen merged 10 commits into
mainfrom
fix/core-bundler-keeps-local-script-order

Conversation

@miguel-heygen

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

Copy link
Copy Markdown
Collaborator

What

Scripts in a Studio preview now run where and when the author put them:

  • Local scripts in a bundled composition (Studio preview and the CLI's layout, motion-shot, validate, compare and grade-compare) keep their place, defer, async, nomodule and type. A <script defer src="main.js"> after a deferred gsap from a CDN runs after gsap again, so the composition builds its timeline instead of failing with gsap is not defined. This holds under a page security policy that allows inline scripts only.
  • When the bundler returns nothing, throws, or returns a page without a runtime, the preview still loads the runtime ahead of the composition's scripts, as the bundled page does. Before, the runtime came after them (bundler returned nothing) or was missing (bundler threw), so script-time installs in the runtime (authored opacity and media capture, the force3D install) ran too late or never.
  • An authored script that merely reads window.__hyperframeRuntime (or mentions a runtime file name, or sets up window.__player) is no longer deleted from the preview and the bundle.

Why

The bundler joined every local <script src> file into one inline script at the position of the first one. That:

  • dropped defer and async (an inline script cannot be deferred), so a deferred main.js ran during parsing, before a deferred gsap;
  • moved later files ahead of any CDN script between them (setup.js, gsap CDN, main.js became setup.js + main.js, then gsap).

The step that later merges adjacent inline scripts also folded in nomodule scripts, dropping the attribute, so legacy-only code ran in modern browsers. It only treated three type strings as JavaScript, so a script typed with another JavaScript MIME type the browser runs (application/ecmascript, text/jscript, ...) skipped the web-font gate and ran early.

Runtime stripping matched words in a script's text (__hyperframeRuntime, window.__player =, ...), so it deleted authored scripts that used them.

The preview's no-bundle path placed the runtime before </body>, after the composition's scripts; the bundler-threw path added none.

Related work

Same family as #5007 (Studio's MotionPathPlugin placement after a deferred or query-string gsap); different owner, so a separate change.

How

  • bundleProject (core/src/compiler/htmlBundler.ts): each local classic script is inlined in its own tag, keeping its other attributes (defer, async, nomodule, a non-JavaScript type) and marked data-hf-inlined-src with the file it came from. Module and integrity-pinned scripts are untouched, as before. Every inlined local JavaScript file now goes through the same esbuild comment strip that merged runs always had (main already did this for files merged into a body script): comments, license banners and source-map comments are dropped and fn.toString() text changes, behaviour does not. Inlined text and merged runs go through core's escapeInlineScriptSource, so a file containing </script> or <!-- cannot end or swallow its tag; <!-- is now written \x3C!--, which stays valid inside a unicode regex.
  • inlineScriptRuns / isClassicInline (core/src/compiler/scriptRuns.ts), shared by the bundler and the producer's render compiler: a defer, async or nomodule script is never merged into a run, and every JavaScript MIME type from the HTML spec counts as classic JavaScript.
  • runScriptsAfterFonts (core/src/runtime/afterFonts.ts), which already runs a bundled page's body scripts once fonts are ready, and its fallback for older runtimes: a deferred file (defer with a src, or with the inlined-file mark) runs after the classic scripts, in document order, and each external script finishes loading before the next script runs. That is the browser's order for the authored page, so no data: URL or other source is needed. An authored inline <script defer> keeps running in place, as browsers run it.
  • stripEmbeddedRuntimeScripts (core/src/compiler/htmlDocument.ts): a script is the runtime's own when its start tag carries the bootstrap attribute or its src path ends in a runtime file name, plus the existing whole-script readiness flags; text inside an authored script no longer counts. Every writer of a lasting runtime tag (bundler, Studio preview, sub-composition preview) sets the attribute. The two "is a runtime already here?" text checks that followed the strip (bundler, sub-composition preview) are gone: after the strip none is left, and the text check skipped the runtime when an authored script mentioned its attribute. The render compiler's CDN inliner (inlineExternalScripts) leaves a runtime file link alone, so the render file server still strips it instead of booting a second, unpinned runtime.
  • insertRuntimeTag (core/src/compiler/htmlDocument.ts): the bundler's runtime placement (end of <head>, else after <html> or the doctype), moved out of injectInterceptor. The Studio preview applies its runtime through it on every path: it strips any runtime already in the page and inserts the preview runtime with the bootstrap attribute.

Adjacent plain inline scripts, including inlined local files with nothing between them, are still merged into one by the existing step, as before; a throw in one stops the rest of that merged script. Files separated by any other script stay separate.

Known limits, unchanged or accepted: a deferred local file in <head> is inlined there and still runs during parsing, as before this change, so a page that needs it to run after a deferred CDN script in <head> still fails (scripts in <body> work); a follow-up handles head order. String.raw text containing <!-- in a bundled script reads back as \x3C!--; in a local non-JavaScript file inlined on its own (JSON, a template, text/babel), every <!-- and </script is escaped (\x3C!--, <\/script), which its reader does not undo, so such text in a JSON string stops it parsing (main already turned these files into JavaScript). A deferred CDN script that never answers stops the scripts after it, as in the raw page (the engine's player-ready limit still ends a render). A runtime pasted inline without the bootstrap attribute is no longer recognised and would load twice; nothing in the repo writes one. Studio preview loads deferred external scripts and modules one after another, as it already did for classic ones. The fallback for runtimes older than the font gate does not wait for an inline module before a later deferred file. Studio preview always serves its own runtime, also for a bundle built with HYPERFRAME_RUNTIME_URL.

Render path

Checked with real renders (CLI, Chrome 152) of four shapes on main and on the web-font gate build: deferred gsap + deferred local main.js, local setup.js / CDN gsap / local main.js, deferred gsap + DOMContentLoaded listener, async gsap + DOMContentLoaded listener (3 runs). All rendered the tween correctly at t = 1 s on both builds; a reversed control (main.js before gsap) stayed static. The render compiler inlines CDN scripts in place and keeps their defer, without the inlined-file mark, so renders run them exactly as before this change. It reaches the render path through isClassicInline (nomodule and the JavaScript type list), the stricter runtime strip, and the runtime-link skip above. No template, fixture or skill in the repo uses defer scripts.

Test plan

  • htmlBundler.test.ts (script order): a local file around a CDN script keeps its place; a local defer and a local async file are inlined in their own tag with the attribute; nomodule survives on a local file and on an inline script; a local type="text/babel" file keeps its type; a local file containing </script> and <!-- is escaped, merged and unmerged; a unicode regex matching <!-- still compiles; a run merging a local file that has a legacy <!-- comment with authored scripts and other files stays valid and runs all five in order; a local file with a legacy <!-- comment inlined on its own (in <head>, or deferred in <body>) stays valid; a local non-JavaScript file keeps its text as written; an authored script reading window.__hyperframeRuntime survives; the runtime is still added when an authored script mentions its attribute.
  • entry.afterFonts.test.ts: an inlined deferred file runs after a later classic script, through the runtime and through the old-runtime fallback, while an authored inline defer script runs in place, through both; an inline script after an external one runs once the external one has loaded, with and without defer.
  • htmlDocument.test.ts: the escape keeps the matched case (</SCRIPT> becomes <\/SCRIPT>).
  • scriptRuns.test.ts: four legacy JavaScript types join a run; text/javascript2 does not.
  • subComposition.test.ts: the preview runtime is added when the project head's script names a runtime file. Producer htmlCompiler.test.ts: a CDN runtime link is not inlined.
  • htmlDocument.test.ts: authored scripts that read the runtime global, query the bootstrap attribute, name a runtime file or set up window.__player are kept; a runtime file linked with a query string or uppercase name is stripped.
  • preview.test.ts: when the bundler returns nothing, returns a page without a runtime, or throws, the runtime loads before a composition script that calls gsap.set(); a disk page that already links a runtime gets exactly one; an authored script reading the runtime global survives.
  • bundleScriptPolicy.test.ts (producer integration, real Chrome): a bundled page with script-src 'unsafe-inline' runs its local defer script after the classic script and animates (CSS animation at 1 s after a seek). Fails on the previous data: URL version with a policy violation. A second Chrome test serves a deferred CDN script through request interception and checks that the inlined deferred main.js sees it; a third checks that a deferred local file runs after an inline module before it.
  • Each new test fails without its fix. Mutants: late rule back to src-only (runtime, fallback and Chrome tests red), merging defer/async, the old <\!-- escape, no runtime on the bundled path, text-based stripping; each turns its own test red.
  • Core runtime and compiler (2282), studio-server routes and helpers (953), producer Chrome and render-order tests (15) and producer compiler and file server (194, bun), each 3 runs; CLI bundle users (layout, validate, localized fonts); producer file server (54, bun), sdk (588), engine services; core, producer and studio-server typecheck; lint, format, comment ratchet, test classification, test reachability and Fallow audit clean.

Before

Studio preview, Chrome 152: deferred gsap from a CDN, then <script defer src="main.js"> that builds the timeline. The bundled main.js runs before gsap: Uncaught ReferenceError: gsap is not defined, no timeline, the dot never moves.

Studio preview error dialog: gsap is not defined

After

Same project with this change, seeked to 1 s through the runtime: main.js is inlined with defer kept and runs after gsap, the main timeline exists and the dot is half-way across. 3 of 3 runs.

Studio preview with the dot half-way across at 1 s

@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Edit accuracy: accurate 1562 (base branch 1562), smooth 1305 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)

The bundler joined every local <script src> into one inline script at the first one's position. That dropped defer and async (a deferred main.js ran before a deferred CDN gsap), moved later files ahead of CDN scripts between them, and let one file's error stop the rest. Each local script is now inlined where it stands with its attributes; a deferred or async one keeps its tag with its file as a data: URL. Inline-script merging also no longer folds in nomodule scripts, which dropped the attribute.
…d inlined files are escaped

- When the bundler returns nothing or throws, the preview places the runtime
  where the bundled page does (end of head) instead of after the composition's
  scripts or not at all; core's placement moves to insertRuntimeTag.
- Every HTML-spec JavaScript MIME type counts as a classic script for merging
  and the web-font gate.
- Inlined local files and merged runs are escaped with escapeInlineScriptSource.
@miguel-heygen
miguel-heygen force-pushed the fix/core-bundler-keeps-local-script-order branch from bc80685 to 77c38f3 Compare October 4, 2026 15:58
@miguel-heygen miguel-heygen changed the title fix(core): bundled local scripts keep their own place, defer and async fix: preview and bundled scripts run where and when the author put them Oct 4, 2026
… runtime reads survive

- Local defer/async scripts are inlined in their own tag with the attribute
  kept; the font-gate runner (and its old-runtime fallback) runs an inline
  classic defer script after the classic scripts, in document order, so a page
  policy that allows inline scripts but not data: still runs them.
- stripEmbeddedRuntimeScripts matches the bootstrap attribute and runtime file
  names on the tag, not runtime words in an authored script's text.
- The preview applies its runtime on every path, so an adapter page without
  one still gets it; the inline escaper writes <!-- as \x3C!--, valid in a
  unicode regex.
@miguel-heygen
miguel-heygen force-pushed the fix/core-bundler-keeps-local-script-order branch from 96a12f0 to 46369b1 Compare October 4, 2026 17:48
@miguel-heygen
miguel-heygen marked this pull request as ready for review October 4, 2026 19:16

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at b8516ff9.

Correctness, checked against the code:

  • Bundler: each local file now becomes its own inline tag, marked data-hf-inlined-src. Body scripts still go through deferScriptsUntilFonts, so an inlined defer file waits for the runtime gate. DEFERRED_FILE then runs it after the classic scripts.
  • runScriptsAfterFonts: it now runs the late scripts in the same loop as the others and waits for each external script to load. That is what makes an inline deferred file run after a deferred CDN gsap. An inline script inserted later would otherwise run straight away, not in queue order. loaded() also resolves on error, so a CDN that fails to load can't stall the queue.
  • isClassicInline: with nomodule excluded, a legacy-only body script is no longer given the after-fonts type, so modern browsers skip it again.
  • Escaping merged runs: applying escapeInlineScriptSource twice changes nothing. Escaped text no longer contains </script or <!--, so inlining a file and then merging it is safe. \x3C!-- means the same thing inside strings, template literals and regexes, character classes included.
  • Runtime strip: every writer of a lasting runtime tag sets the bootstrap attribute. That covers the bundler's three modes, the preview route and the sub-composition preview. A placeholder src="" bundle is therefore stripped, and the preview runtime goes in its place.
  • Render path: inlineExternalScripts skipping a runtime file link keeps the file server's strip in charge. Render CDN inlining keeps defer without the inlined-file mark, so it doesn't match DEFERRED_FILE and runs as before.

Reuse:

  • insertRuntimeTag takes the bundler's placement code and gives it to the preview route.
  • The two "is a runtime already here?" text checks are deleted rather than copied.
  • escapeInlineScriptSource and isRuntimeFileUrl move from private helpers to exports.
  • The bundler and producer share inlineScriptRuns and isClassicInline.

I found nothing else this should have been built on.

Simplicity: the bundler's local-JS step gets shorter, since a three-line inline-in-place replaces the anchor and join step. I don't see a smaller fix for both the order and the attributes.

Verified locally:

  • These suites pass:
    • core's htmlBundler, htmlDocument, scriptRuns and entry.afterFonts: 221 tests
    • studio-server's subComposition and preview: 108 tests
    • producer's htmlCompiler, under bun: 140 tests
    • the producer test-classification check
  • I made six mutations, and each one turned tests red:
    • defer scripts merging into runs again: 1 fails
    • nomodule counting as classic: 2 fail
    • DEFERRED_FILE without the inlined mark: 3 fail
    • the late scripts never running: 2 fail
    • inlined files left unescaped: 1 fails
    • the __hyperframeRuntime text-marker strip restored: 2 fail

Non-blocking:

  1. A runtime pasted inline with no bootstrap attribute now survives the strip. That's a copy of the runtime body inside a plain <script>. The old text markers caught it, so Studio preview and serialize({ stripRuntime: true }) would now load a second runtime for such a page. I found no writer or fixture in the repo that produces one; the producer's attribute-less inline runtime is only added when the page is served. Worth a line in the known limits.
  2. Late module scripts with a src now load one after another instead of together. Order and results are unchanged, so it only costs time on pages with several external modules.

— Rames

@somanshreddy somanshreddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at b8516ff9 (full PR).

This is a comment, not an approval. Rames already covered the deferred-after-CDN order, nomodule, double escaping, the attribute-based strip, the inline runtime pasted without the attribute, and late external modules loading one at a time. I don't repeat those here.

Order matrix (real Chrome 152, raw page vs the head's bundle + runtime). 15 pages: local and CDN scripts, classic, defer, async, async defer, inline and src modules, nomodule, text/jscript, head and body, interleaved, plus 404, throwing, CSP-blocked and hanging scripts. 6 mismatches:

  • a local defer file in <head> runs during parsing: known limit.
  • a body inline script runs after a <head> CDN defer script: this comes from the #4980 gate and is the same on the base.
  • a local async and a local async defer file run at a different moment: allowed, since async has no order guarantee.
  • under an inline-only CSP, the inlined local defer file runs and the raw page blocks it: intended.
  • an inline module followed by a local defer file: bug, finding 2.

On the base bundle, P1 put 4 of 10 scripts in the wrong place, and P4 ran setup, main, tail, gsap. Both match the raw page at this head. The pre-#4980 runtime's fallback gives the same order as the head runtime.

1. (medium) A local file that uses <!-- as a legacy comment now breaks every script merged with it, including the scripts before it. htmlBundler.ts:1231 escapes the file to \x3C!-- before the merge at :741 runs esbuild (stripJsCommentsParserSafe, :793-799). esbuild can't parse the escaped text, so the catch returns the source unchanged and the whole merged script is one SyntaxError. In the probe, esc.js, legacy.js, two authored inline scripts and plain.js sit next to each other. At this head none of the five ran ("Invalid or unexpected token"). On the base all five ran. An authored inline script with the same comment still works, because esbuild strips the comment before the escape. Known limits says only the legacy file stops parsing. Fix: run the comment strip before the escape at :1231, or escape once, after the merge.

2. (low) An inline module followed by a deferred local file runs in the wrong order (afterFonts.ts:116-118). The inline module has no src, so nothing waits for it. The inlined file is a classic inline script, so it runs as soon as it is inserted, before the module. Raw page: m1, then d with sawModule=true. Bundled: d with sawModule=false, then m1. Same result through the fallback. This is no worse than the base, which ran the file even earlier, but it doesn't match the "document order" claim. One fix is a barrier after an inline module when another deferred script follows. Codex flagged this first; I confirmed it in Chrome.

3. (low) The escape changes some values. String.raw\<!--`becomes\x3C!--andString.raw`</script>`becomes</script>. "</SCRIPT >"becomes"</script >", because escapeCaseInsensitiveToken (htmlDocument.ts:170-190) writes the replacement in lower case. The base kept all three. The merge path (:741`) applies this to authored inline scripts too. Keeping the matched case fixes the last one; the raw-string cases belong in Known limits. Codex found the same thing on its own.

Liveness. Each wait resolves on load or error. A 404, a script that throws, and a CSP-blocked script all let the chain go on. A CDN defer script that never answers stops it for good: the runtime never boots and __renderReady stays unset. That is also what the raw page does (DOMContentLoaded never fires) and what the base does. There is no timeout, so a render stops only at the engine's 45 s player-ready limit. A hanging async script doesn't block. Worth a line in Known limits.

#4980 and #5007. My #4980 finding 1 is still there: with the pre-#4980 runtime, a deferred main.js that runs after a slow deferred gsap sees __playerReady and __renderReady already true. With the head runtime both are false. Findings 2 and 3 are in files this PR doesn't change. #5007 works with this PR: its plugin tag copies gsap's type and defer, so both go through the gate as deferred and the plugin runs after gsap. The async gsap case is still open.

Codex leads I did not keep. First, async defer treated as deferred (scriptRuns.ts:71). async allows any position, so this is not a bug. Second, the sub-composition runtime moving to after the head scripts. The base also stripped the runtime and added it again at the end (subComposition.ts:350-352), so its position is unchanged.

Render path. The repo's registry, examples, skills, producer tests and CLI use no defer, async or nomodule scripts, and no script type that the new MIME list changes. The scripts the render server adds to <head> contain none of the old text markers. The runtime delta adds no src, data: or blob:.

Mutations: 7 of 7 caught. Dropping defer/async from the merge guard, cutting the MIME list, making only modules late in the runtime, the same in the fallback, not waiting for late src scripts, putting the preview runtime before </body>, and letting the producer inline the runtime link.

CI: 84 pass, 8 skipped, 0 failed; 11 of 11 required checks pass.

What I ran: the Chrome order and escape probe against the head, the base, and the pre-#4980 runtime. The changed core tests, subComposition, bundleScriptPolicy (Chrome) and the producer htmlCompiler tests all pass, run under vitest or bun. In preview.test.ts I ran only the 9 runtime tests, under bun, because vitest won't load the file here (the node: issue). One static Codex pass finished within its time box, and I checked each of its leads at source.

— Somu

@miguel-heygen
miguel-heygen disabled auto-merge October 4, 2026 20:25

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing my approval to request changes. The comment review posted at this same head found a regression that I missed, and I reproduced it.

Blocker: a local script that uses <!-- as a legacy comment now stops every script merged with it from running.

  • htmlBundler.ts:1231 writes each inlined local file through escapeInlineScriptSource, which turns <!-- into \x3C!--.
  • Then :741 joins the run and passes it to stripJsCommentsParserSafe. As code, \x3C!-- is invalid, so esbuild throws, the catch returns the source unchanged, and the browser gets a block that won't parse. None of the scripts in that run execute, including the ones before the legacy file.
  • Probe at this head: window.first = 1; merged with a file containing <!-- hide\nwindow.legacy = 2;\n// -->. The output fails new vm.Script with "Invalid or unexpected token". Joining the raw sources, then stripping and escaping once, parses fine, and so does the raw source (it's an Annex B HTML comment).

The smallest fix I see: strip comments at :1231 before escaping, i.e. escapeInlineScriptSource(stripJsCommentsParserSafe(js)). After esbuild, any <!-- left over is inside a string, regex or template, where \x3C is valid, so the strip at :741 parses it and the escape there has nothing left to change. Scripts that run separately (deferred, async, head) stay escaped exactly as they are now. Please add a bundler test with a legacy-comment file next to another local script, asserting that both run.

The other reviewer's low-severity notes (an inline module before a deferred local file, the String.raw value change, a CDN defer that never responds) don't block from my side, and I haven't verified them. Everything else in my earlier approval still holds, and I'll re-approve once the fix lands.

— Rames

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 6a7f72cf (one commit on top of b8516ff9). Approving. The blocker from my last review is fixed.

The legacy-comment blocker is fixed. Local files now stay raw when they're inlined (htmlBundler.ts:1231). A merged run is stripped by esbuild and then escaped once (:741). Inlined files that were left unmerged get one escape pass after coalesceHeadStylesAndBodyScripts (:1265). So every tag is escaped exactly once, and esbuild only ever sees raw source. Nothing between :1231 and :1265 serializes or re-parses the script text. The new bundler test runs the five-script case in order and checks that the merged text has no </script or <!--.

The other two changes check out:

  • afterFonts.ts:119 now waits for the module queue after each inline module, not once at the end. A later deferred file therefore sees the module's globals, and the new Chrome test covers that. A module whose import never resolves already held up the end of the chain before this change, so this adds no new stall.
  • The escape keeps the case it matched (</SCRIPT becomes <\/SCRIPT).

Evidence:

  • core htmlBundler, htmlDocument, scriptRuns and runtime suites: 72 files, 2044 tests, all pass.
  • producer bundleScriptPolicy.test.ts in Chrome: 3 of 3 pass.
  • I made four mutations, and a test failed for each one:
    • escaping again at :1231
    • removing the final escape pass
    • lowercasing the escape again
    • moving the module wait back to the end of the chain (fails the new Chrome test)

Reuse / simplicity: the fix reuses the existing escape helper and the existing post-merge step. It's smaller than the code it replaces, and I don't see a simpler version.

Non-blocking: Known limits says a file that's inlined on its own (defer, async, nomodule) and uses a legacy <!-- comment stops parsing. On main, such a file was merged, so its comment was stripped, which makes this a narrow regression. If you want to close it, the final pass could run stripJsCommentsParserSafe before escaping, limited to JavaScript types so that text/babel and other non-JS types are untouched. It's fine as a follow-up.

— Rames

Comment thread packages/core/src/compiler/htmlDocument.ts Dismissed
Comment thread packages/core/src/compiler/htmlDocument.ts Dismissed
Comment thread packages/core/src/compiler/htmlDocument.ts Dismissed

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 53506c9c, which adds two commits on top of 6a7f72cf, the head I approved. Approving.

What changed: the last escape pass now strips comments before escaping, but only on JavaScript types, through the new isJavaScriptType helper (htmlBundler.ts:1266-1270). That closes the non-blocking case from my last review: a file inlined on its own (in <head>, or defer in <body>) with a legacy <!-- comment. Non-JavaScript types such as JSON and text/babel keep their text as written. The type check is the one isClassicInline already uses, now factored out, so there's no second list of types to keep in sync.

Evidence:

  • The core compiler and runtime suites pass (84 files, 2337 tests), and the producer bundleScriptPolicy Chrome tests pass 3 of 3.
  • Removing the strip fails both new it.each cases.
  • Stripping every type fails the new JSON test, because esbuild rewrites [1,2].

Reuse / simplicity: the fix reuses the existing strip helper and the existing type set, and it's four lines. I don't see a smaller fix.

— Rames

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit 16871a1 Oct 4, 2026
169 of 170 checks passed
@miguel-heygen
miguel-heygen deleted the fix/core-bundler-keeps-local-script-order branch October 4, 2026 22:30
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.

4 participants