From e22bbc2e837efaacdabb35302ced9732d28373a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 28 Sep 2026 18:33:53 -0400 Subject: [PATCH] fix(core): a film that loads GSAP from its own folder can swap edited scenes in place The preview's scan of root scripts read a vendored gsap.min.js as film code, so the library's own strings ("*" among them) refused every scene. A root script that is GSAP's own build (its banner, then one minified line) is now read as the library, as the CDN copy already is; code after the build is still scanned. --- .../core/src/compiler/htmlBundler.test.ts | 23 ++++++++++++++++++- .../src/compiler/inlineSubCompositions.ts | 20 ++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/packages/core/src/compiler/htmlBundler.test.ts b/packages/core/src/compiler/htmlBundler.test.ts index 6eb1f059d2..60ee444334 100644 --- a/packages/core/src/compiler/htmlBundler.test.ts +++ b/packages/core/src/compiler/htmlBundler.test.ts @@ -1,5 +1,5 @@ // @vitest-environment node -import { mkdtempSync, writeFileSync, mkdirSync, symlinkSync, rmSync } from "node:fs"; +import { mkdtempSync, readFileSync, writeFileSync, mkdirSync, symlinkSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { parseHTML } from "linkedom"; @@ -2465,6 +2465,27 @@ describe("bundleToSingleHtml sceneParts", () => { expect(await swapMarks(rootProject(root))).toEqual([null, null]); }); + it("reads a vendored GSAP build as the library, and film code after it as film code", async () => { + const gsap = readFileSync( + join( + __dirname, + "../../../../skills/music-to-video/references/motion-primitives/assets/gsap.min.js", + ), + "utf8", + ); + const root = ``; + expect(await swapMarks(rootProject(root, { "assets/gsap.min.js": gsap }))).toEqual([ + null, + null, + ]); + const bundle = `${gsap}\ndocument.querySelector(".go").onclick = () => {};`; + // Read whole, so the library's own strings refuse as they did before. + expect(await swapMarks(rootProject(root, { "assets/gsap.min.js": bundle }))).toEqual([ + selects("*"), + selects("*"), + ]); + }); + it("runs a scene's local script file in source order with its inline scripts, as a render does", async () => { const dir = makeTempProject({ "index.html": ` diff --git a/packages/core/src/compiler/inlineSubCompositions.ts b/packages/core/src/compiler/inlineSubCompositions.ts index 3551c324db..5657b5316c 100644 --- a/packages/core/src/compiler/inlineSubCompositions.ts +++ b/packages/core/src/compiler/inlineSubCompositions.ts @@ -61,6 +61,14 @@ const isNotASelector = (script: string, at: number, literal: string) => CREATES_ELEMENT_RE.test(script.slice(Math.max(0, at - 60), at)) || (literal.trim() === "*" && /,\s*$/.test(script.slice(Math.max(0, at - 20), at))); +// GSAP's own build as vendored beside a film: its banner, then one minified line. Film code has no such banner. +const isGsapBuild = (script: string) => + /^\s*\/\*!\s*\*\s*GSAP \d+\.\d+\.\d+\s/.test(script) && + script + .slice(script.indexOf("*/") + 2) + .split("\n") + .filter((line) => line.trim() && !line.startsWith("//# sourceMappingURL=")).length === 1; + /** Marks each scene whose nodes a script outside it names by selector, id or class: a swap would strand it. */ export function refuseSwapsReachedByRootScripts(document: Document, rootScripts: string[]): void { const hosts = [...document.querySelectorAll(`[${SCENE_PART_ATTR}]`)]; @@ -91,11 +99,13 @@ export function refuseSwapsReachedByRootScripts(document: Document, rootScripts: ), ); const literals = new Set( - rootScripts.flatMap((s) => - [...s.matchAll(STRING_LITERAL_RE)] - .filter((m) => !isNotASelector(s, m.index, m[2] ?? "")) - .map((m) => m[2] ?? ""), - ), + rootScripts + .filter((s) => !isGsapBuild(s)) + .flatMap((s) => + [...s.matchAll(STRING_LITERAL_RE)] + .filter((m) => !isNotASelector(s, m.index, m[2] ?? "")) + .map((m) => m[2] ?? ""), + ), ); for (const literal of literals) { const open = hosts.filter((host) => !host.hasAttribute(SCENE_NO_SWAP_ATTR));