fix(producer): a mounted composition's local script runs after the inline script before it (#4425)

This commit is contained in:
Miguel Ángel
2026-09-24 13:25:39 -04:00
committed by GitHub
parent 8dabc8af63
commit ce2b338710
2 changed files with 82 additions and 12 deletions
@@ -2,13 +2,14 @@
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { compileForRender } from "./htmlCompiler.js";
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
vi.unstubAllGlobals();
});
function project(files: Record<string, string>): string {
@@ -44,4 +45,58 @@ describe("compileForRender script order", () => {
expect(before).toBeLessThan(lib);
expect(lib).toBeLessThan(after);
});
it("keeps a mounted composition's local src script between the inline scripts around it", async () => {
const dir = project({
"index.html": `<!doctype html>
<html><body>
<div data-composition-id="root" data-width="320" data-height="180">
<div id="block" data-composition-id="block" data-composition-src="compositions/block/block.html" data-width="320" data-height="180"></div>
</div>
</body></html>`,
"compositions/block/block.html": `<!doctype html>
<html><body>
<div data-composition-id="block" data-width="320" data-height="180"></div>
<script>window.MARK_BEFORE = 1;</script>
<script src="assets/needs-before.js"></script>
<script>window.MARK_AFTER = 1;</script>
</body></html>`,
"compositions/block/assets/needs-before.js": "void 0;",
});
const { html } = await compileForRender(dir, join(dir, "index.html"), join(dir, ".downloads"), {
allowSystemFontCapture: false,
});
const before = html.indexOf("MARK_BEFORE");
const lib = html.indexOf("compositions/block/assets/needs-before.js");
const after = html.indexOf("MARK_AFTER");
expect(before).toBeGreaterThan(-1);
expect(lib).toBeGreaterThan(-1);
expect(before).toBeLessThan(lib);
expect(lib).toBeLessThan(after);
});
it("still runs a mounted composition's CDN script before its inline scripts", async () => {
vi.stubGlobal("fetch", async () => new Response("window.CDN_LIB = 1;"));
const dir = project({
"index.html": `<!doctype html>
<html><body>
<div data-composition-id="root" data-width="320" data-height="180">
<div id="block" data-composition-id="block" data-composition-src="compositions/block/block.html" data-width="320" data-height="180"></div>
</div>
</body></html>`,
"compositions/block/block.html": `<!doctype html>
<html><body>
<div data-composition-id="block" data-width="320" data-height="180"></div>
<script>window.MARK_BEFORE = window.CDN_LIB;</script>
<script src="https://cdn.example/lib.js"></script>
</body></html>`,
});
const { html } = await compileForRender(dir, join(dir, "index.html"), join(dir, ".downloads"), {
allowSystemFontCapture: false,
});
const lib = html.indexOf("window.CDN_LIB = 1");
const before = html.indexOf("MARK_BEFORE");
expect(lib).toBeGreaterThan(-1);
expect(lib).toBeLessThan(before);
});
});
+26 -11
View File
@@ -1056,12 +1056,16 @@ function inlineSubCompositions(
head.appendChild(styleEl);
}
// Inject external CDN scripts before inline scripts so plugins (e.g.
// TextPlugin, ScrollTrigger) are registered before composition code runs.
// Deduplicate against scripts already present in the document.
// CDN and integrity-pinned scripts go first so plugins (e.g. TextPlugin,
// ScrollTrigger) register before composition code, as in htmlBundler. A local
// src script keeps its authored place among the inline scripts (see below).
const isHoisted = (item: { src: string; integrity?: string }) =>
Boolean(item.integrity?.trim()) || isNonRelativeUrl(item.src);
if (body) {
for (const item of result.scriptItems) {
if (item.kind === "external") ensureExternalScriptTag(document, item.src, item);
if (item.kind === "external" && isHoisted(item)) {
ensureExternalScriptTag(document, item.src, item);
}
}
}
@@ -1074,13 +1078,24 @@ function inlineSubCompositions(
// text (issue #2064). Same shared builder as the bundler so they stay in
// lockstep.
const variablesByCompScript = buildVariablesByCompScript(result.variablesByComp);
const inlineScripts = variablesByCompScript
? [variablesByCompScript, ...result.scripts]
: result.scripts;
if (inlineScripts.length && body) {
const scriptEl = document.createElement("script");
scriptEl.textContent = inlineScripts.join("\n;\n");
body.appendChild(scriptEl);
if (body) {
let pending = variablesByCompScript ? [variablesByCompScript] : [];
const flushInline = () => {
if (!pending.length) return;
const scriptEl = document.createElement("script");
scriptEl.textContent = pending.join("\n;\n");
body.appendChild(scriptEl);
pending = [];
};
for (const item of result.scriptItems) {
if (item.kind === "inline") {
pending.push(item.content);
} else if (!isHoisted(item)) {
flushInline();
ensureExternalScriptTag(document, item.src, item);
}
}
flushInline();
}
// Compile-time CSS custom properties (mirrors the preview bundler): root