fix(core): consolidate external asset and dependency preservation (#2410)

## Summary

- preserve external SVG fragment references during bundling
- preserve external module scripts and serve `.mjs` with a JavaScript MIME type
- retain template-head stylesheets when mounting sub-compositions
- add regression coverage across compiler runtime and file-server paths

Consolidates and replaces #2390, #2297, and #2375.

## Verification

- core compiler/runtime tests: 89 passed
- producer file-server tests: 48 passed
- core, producer, engine, and CLI typechecks passed
- `git diff --check`
This commit is contained in:
Miguel Ángel
2026-07-14 21:55:51 -04:00
committed by GitHub
parent 5d3a7404fa
commit 7382fabab9
12 changed files with 364 additions and 13 deletions
@@ -95,6 +95,34 @@ describe("bundleToSingleHtml", () => {
expect(bundled).not.toContain("./bg.svg");
});
it("preserves external SVG fragment references used by <use>", async () => {
const spriteSvg = `<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="patch-head" viewBox="0 0 10 10"><circle cx="5" cy="5" r="4" /></symbol>
</svg>`;
const dir = makeTempProject({
"index.html": `<!doctype html><html><body>
<div data-composition-id="main" data-width="320" data-height="180" data-start="0" data-duration="1">
<svg>
<use id="href-use" href="assets/patch.svg#patch-head"></use>
<use id="xlink-use" xlink:href="assets/patch.svg#patch-head"></use>
</svg>
</div>
<script>window.__timelines = window.__timelines || {}; window.__timelines.main = {}</script>
</body></html>`,
"assets/patch.svg": spriteSvg,
});
const bundled = await bundleToSingleHtml(dir);
const { document } = parseHTML(bundled);
expect(document.getElementById("href-use")?.getAttribute("href")).toBe(
"assets/patch.svg#patch-head",
);
expect(document.getElementById("xlink-use")?.getAttribute("xlink:href")).toBe(
"assets/patch.svg#patch-head",
);
expect(bundled).not.toContain("data:image/svg+xml;base64");
});
it("does not merge author scripts into the runtime bootstrap placeholder", async () => {
const dir = makeTempProject({
"index.html": `<!doctype html>
@@ -405,6 +433,22 @@ describe("bundleToSingleHtml", () => {
expect(bundled).not.toContain('src="vendor/effect-plugin.js"');
});
it("preserves local module scripts and their import base URL", async () => {
const dir = makeTempProject({
"index.html": `<!doctype html><html><body>
<div data-composition-id="main" data-start="0" data-duration="1"></div>
<script type="module" src="./module.js"></script>
</body></html>`,
"module.js": `import { value } from "./value.js"; window.result = value;`,
"value.js": `export const value = "loaded";`,
});
const bundled = await bundleToSingleHtml(dir);
expect(bundled).toMatch(/<script\b[^>]*\btype="module"[^>]*\bsrc="\.\/module\.js"/);
expect(bundled).not.toContain('import { value } from "./value.js"');
});
it("preserves local sub-composition script order before inline scene scripts", async () => {
const dir = makeTempProject({
"index.html": `<!doctype html>
+19
View File
@@ -310,6 +310,16 @@ function maybeInlineRelativeAssetUrl(urlValue: string, projectDir: string): stri
return appendSuffixToUrl(dataUrl, suffix);
}
function isExternalSvgFragmentUse(el: Element, attr: string, urlValue: string): boolean {
if (el.tagName.toLowerCase() !== "use") return false;
if (attr !== "href" && attr !== "xlink:href") return false;
if (!isRelativeUrl(urlValue)) return false;
const hashIdx = urlValue.indexOf("#");
if (hashIdx <= 0) return false;
const pathBeforeFragment = urlValue.slice(0, hashIdx).split("?", 1)[0] ?? "";
return pathBeforeFragment.toLowerCase().endsWith(".svg");
}
function warnColorGradingLutNotInlined(lutSrc: string): void {
const trimmed = lutSrc.trim();
if (!isRelativeUrl(trimmed)) return;
@@ -843,6 +853,10 @@ export async function bundleToSingleHtml(
for (const el of [...document.querySelectorAll("script[src]")]) {
const src = el.getAttribute("src");
if (!src || !isRelativeUrl(src)) continue;
// Module scripts can contain static imports whose resolution is relative
// to the script URL. Folding their source into a classic inline script
// both drops module semantics and changes the import base URL.
if ((el.getAttribute("type") || "").trim().toLowerCase() === "module") continue;
const jsPath = resolveEntryPath(src);
const js = jsPath ? safeReadFile(jsPath) : null;
if (js == null) continue;
@@ -1070,6 +1084,11 @@ export async function bundleToSingleHtml(
for (const attr of ["src", "href", "poster", "xlink:href"] as const) {
const value = el.getAttribute(attr);
if (!value) continue;
// Chromium requires external SVG <use> fragments to be same-origin with
// the document. Converting the sprite to a data: URL makes it an opaque
// origin and triggers "Unsafe attempt to load URL ... from frame".
// Keep the project-relative URL; render/check servers already expose it.
if (isExternalSvgFragmentUse(el, attr, value)) continue;
const inlined = maybeInlineRelativeAssetUrl(value, projectDir);
if (inlined) el.setAttribute(attr, inlined);
}