fix(core): a mounted composition's import map and module scripts run as such (#4446)

* feat(core): a composition's script can load its own files with __hyperframes.assetUrl

* test(core): a template composition inside a mounted file resolves assets from that file

* docs: assetUrl is for paths beside the composition, not project-root paths

* docs: say assetUrl misses a project-root path

* fix(core): a mounted composition's import map and module scripts run as such

* refactor(core): scope a mounted module script under the same condition as a classic one

* fix(core): a mounted module script loaded by src stays a module

* fix(core): a mounted import map keeps the trailing slash of a prefix address

* chore(core): keep the module-script helpers' comment share at the base
This commit is contained in:
Miguel Ángel
2026-09-24 23:44:04 -04:00
committed by GitHub
parent 0e34d69641
commit 278bc712d7
13 changed files with 414 additions and 19 deletions
@@ -4,6 +4,7 @@ import {
buildVariablesByCompScript,
scopeCssToComposition,
wrapInlineScriptWithErrorBoundary,
scopedModulePrelude,
wrapScopedCompositionScript,
} from "./compositionScoping";
@@ -165,6 +166,26 @@ body { margin: 0; }
expect(fakeWindow.__captured).toEqual({ title: "Pro", price: "$29" });
});
it("gives a mounted module script its composition's own __hyperframes", () => {
const { document } = parseHTML(`<div></div>`);
Object.defineProperty(document, "baseURI", { value: "https://p.test/preview/" });
const fakeWindow = {
document,
__hfVariablesByComp: { blk: { title: "Hi" }, other: { title: "No" } },
__hyperframes: { assetUrl: () => "TOP-LEVEL", getVariables: () => ({}), fitTextFontSize: 1 },
};
const scoped = new Function(
"window",
`${scopedModulePrelude("blk", "compositions/blk/blk.html")}return __hyperframes;`,
)(fakeWindow);
expect(scoped.assetUrl("assets/env.hdr")).toBe(
"https://p.test/preview/compositions/blk/assets/env.hdr",
);
expect(scoped.getVariables()).toEqual({ title: "Hi" });
expect(scoped.fitTextFontSize).toBe(1);
});
it("resolves __hyperframes.assetUrl against the mounted composition's own file", () => {
const run = (compositionSrc?: string) => {
const { document } = parseHTML(`<div data-composition-id="blk"></div>`);
@@ -276,6 +276,30 @@ function jsonScriptLiteral(value: unknown): string {
return JSON.stringify(value).replace(/</g, "\\u003c");
}
const SCOPED_HYPERFRAMES_EXPRESSION = `!__hfBaseHyperframes
? __hfBaseHyperframes
: Object.assign({}, __hfBaseHyperframes, {
assetUrl: function(path) {
var page = window.document.baseURI;
return new URL(path, __hfCompositionSrc ? new URL(__hfCompositionSrc, page) : page).href;
},
getVariables: function() {
var byComp = window.__hfVariablesByComp;
var scoped = byComp && __hfTimelineCompId ? byComp[__hfTimelineCompId] : null;
return scoped ? Object.assign({}, scoped) : {};
},
})`;
export function scopedModulePrelude(
timelineCompositionId: string,
compositionSrc?: string | null,
): string {
return `const __hyperframes = (function(__hfBaseHyperframes, __hfTimelineCompId, __hfCompositionSrc) {
return ${SCOPED_HYPERFRAMES_EXPRESSION};
})(window.__hyperframes, ${jsonScriptLiteral(timelineCompositionId)}, ${jsonScriptLiteral(compositionSrc?.trim() || null)});
`;
}
export function wrapScopedCompositionScript(
source: string,
compositionId: string,
@@ -611,19 +635,7 @@ export function wrapScopedCompositionScript(
},
});
var __hfBaseHyperframes = window.__hyperframes;
var __hfScopedHyperframes = !__hfBaseHyperframes
? __hfBaseHyperframes
: Object.assign({}, __hfBaseHyperframes, {
assetUrl: function(path) {
var page = window.document.baseURI;
return new URL(path, __hfCompositionSrc ? new URL(__hfCompositionSrc, page) : page).href;
},
getVariables: function() {
var byComp = window.__hfVariablesByComp;
var scoped = byComp && __hfTimelineCompId ? byComp[__hfTimelineCompId] : null;
return scoped ? Object.assign({}, scoped) : {};
},
});
var __hfScopedHyperframes = ${SCOPED_HYPERFRAMES_EXPRESSION};
var __hfRun = function() {
try {
(function(document, gsap, window, __hyperframes) {
@@ -1,12 +1,14 @@
export interface ExternalScriptAttributes {
integrity?: string;
crossorigin?: string;
type?: "module";
}
export function readExternalScriptAttributes(el: Element): ExternalScriptAttributes {
const attributes: ExternalScriptAttributes = {};
if (el.hasAttribute("integrity")) attributes.integrity = el.getAttribute("integrity") || "";
if (el.hasAttribute("crossorigin")) attributes.crossorigin = el.getAttribute("crossorigin") || "";
if ((el.getAttribute("type") || "").trim().toLowerCase() === "module") attributes.type = "module";
return attributes;
}
@@ -40,6 +42,7 @@ export function ensureExternalScriptTag(
if (integrity) el.setAttribute("integrity", integrity);
if (attributes.crossorigin !== undefined)
el.setAttribute("crossorigin", attributes.crossorigin);
if (attributes.type) el.setAttribute("type", attributes.type);
}
if (!existing.length) {
const el = elements[0]!;
@@ -198,6 +198,55 @@ describe("bundleToSingleHtml", () => {
expect(bundled).toContain('var __hfCompositionSrc = "compositions/blk/blk.html";');
});
it("keeps a mounted file's import map and module script as such, bound to that file", async () => {
const dir = makeTempProject({
"index.html": `<!doctype html>
<html><head></head><body>
<div id="root" data-composition-id="main" data-width="1920" data-height="1080">
<div id="blk-host" data-composition-id="blk" data-composition-src="compositions/blk/blk.html"
data-start="0" data-duration="5"></div>
</div>
</body></html>`,
"compositions/blk/assets/three.js": "export const REVISION = 1;",
"compositions/blk/assets/addons/env.js": "export const ENV = 1;",
"compositions/blk/assets/scene.js": 'import * as THREE from "three"; export const SCENE = 1;',
"compositions/blk/blk.html": `<div data-composition-id="blk" data-width="1920" data-height="1080">
<script type="module" src="./assets/scene.js"></script>
<script type="module" src="https://cdn.test/mod.js"></script>
<script type="importmap">{ "imports": { "three": "./assets/three.js", "three/addons/": "./assets/addons/", "cdn": "https://cdn.test/x.js" } }</script>
<script type="module">import * as THREE from "three"; window.__url = __hyperframes.assetUrl("assets/leaf.webp");</script>
<script>window.__classic = 1;</script>
</div>`,
});
const { document } = parseHTML(await bundleToSingleHtml(dir));
const importMaps = [...document.querySelectorAll('script[type="importmap"]')];
const modules = [...document.querySelectorAll('script[type="module"]')];
const classic = [...document.querySelectorAll("script:not([type])")].map((s) => s.textContent);
expect(importMaps).toHaveLength(1);
expect(JSON.parse(importMaps[0]!.textContent || "")).toEqual({
imports: {
three: "./compositions/blk/assets/three.js",
"three/addons/": "./compositions/blk/assets/addons/",
cdn: "https://cdn.test/x.js",
},
});
expect(modules.map((m) => m.getAttribute("src")).filter(Boolean)).toEqual([
"compositions/blk/assets/scene.js",
"https://cdn.test/mod.js",
]);
const inline = modules.filter((m) => !m.hasAttribute("src"));
expect(inline).toHaveLength(1);
expect(inline[0]!.textContent).toMatch(/^const __hyperframes = /);
expect(inline[0]!.textContent).toContain('"compositions/blk/blk.html"');
expect(inline[0]!.textContent).toContain('import * as THREE from "three";');
expect(classic.join("")).not.toContain("SCENE");
expect(classic.join("")).toContain(".__classic = 1;");
expect(classic.join("")).not.toContain('"imports"');
expect(classic.join("")).not.toContain("import * as THREE");
});
it("binds a <template> composition authored in a mounted file to that file", async () => {
const dir = makeTempProject({
"index.html": `<!doctype html>
+4 -2
View File
@@ -4,6 +4,7 @@ import {
readExternalScriptAttributes,
type ExternalScriptAttributes,
} from "./externalScripts";
import { emitMountedModuleScripts } from "./importMaps";
import { markFlattenedInnerRoot } from "../runtime/flattenedRoot";
export { FLATTENED_INNER_ROOT_STRIP_ATTRS } from "../runtime/flattenedRoot";
import { parseHostVariableValues, warnUnknownEnumValues } from "../runtime/getVariables";
@@ -846,7 +847,7 @@ function hoistExternalScript(
}
if (seenSrcs.has(src)) return;
seenSrcs.add(src);
if (!isNonRelativeUrl(src) && !isAbsolute(src)) {
if (!isNonRelativeUrl(src) && !isAbsolute(src) && attributes.type !== "module") {
const jsPath = resolveWithinProject(projectDir, src);
const js = jsPath ? safeReadFile(jsPath) : null;
if (js != null) {
@@ -1023,7 +1024,7 @@ export async function bundleToSingleHtml(
}
if (seenCompScriptSrcs.has(extSrc)) continue;
seenCompScriptSrcs.add(extSrc);
if (isRelativeUrl(extSrc)) {
if (isRelativeUrl(extSrc) && scriptItem.type !== "module") {
const jsPath = resolveEntryPath(extSrc);
const js = jsPath ? safeReadFile(jsPath) : null;
if (js != null) {
@@ -1214,6 +1215,7 @@ export async function bundleToSingleHtml(
);
document.body.appendChild(compScript);
}
emitMountedModuleScripts(document, subCompResult.importMaps, subCompResult.moduleScripts);
emitRootCompositionVariableStyles(document, compVariablesByComp);
@@ -0,0 +1,55 @@
import { parseHTML } from "linkedom";
import { afterEach, describe, expect, it, vi } from "vitest";
import { mergeImportMapsIntoDocument, parseImportMap } from "./importMaps";
afterEach(() => vi.restoreAllMocks());
const pageWith = (head: string) =>
parseHTML(`<!doctype html><html><head>${head}</head><body></body></html>`).document;
const mapOf = (doc: Document) =>
JSON.parse(doc.querySelector('script[type="importmap"]')?.textContent || "null");
describe("parseImportMap", () => {
it("rebases imports and scopes, and rejects what the browser would reject", () => {
const rebase = (url: string) => `R(${url})`;
expect(
parseImportMap(`{"imports":{"a":"./a.js"},"scopes":{"./s/":{"b":"./b.js"}}}`, rebase),
).toEqual({ imports: { a: "R(./a.js)" }, scopes: { "R(./s/)": { b: "R(./b.js)" } } });
expect(parseImportMap("{ not json", rebase)).toBeNull();
expect(parseImportMap("null", rebase)).toBeNull();
});
});
describe("mergeImportMapsIntoDocument", () => {
it("creates the page's one import map ahead of every script when it has none", () => {
const doc = pageWith(`<script type="module" src="x.js"></script>`);
mergeImportMapsIntoDocument(doc, [{ imports: { three: "./t.js" } }]);
expect(doc.head.firstElementChild?.getAttribute("type")).toBe("importmap");
expect(mapOf(doc)).toEqual({ imports: { three: "./t.js" } });
});
it("keeps the first mapping of a specifier and warns about a conflicting one", () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const doc = pageWith(
`<script type="importmap">{"imports":{"three":"./page.js"},"integrity":{"./page.js":"sha384-x"}}</script>`,
);
mergeImportMapsIntoDocument(doc, [
{ imports: { three: "./mounted.js", gsap: "./g.js" }, scopes: { "./s/": { a: "./a.js" } } },
{ imports: { gsap: "./g.js" }, scopes: { "./s/": { a: "./other.js" } } },
]);
expect(doc.querySelectorAll('script[type="importmap"]')).toHaveLength(1);
expect(mapOf(doc)).toEqual({
imports: { three: "./page.js", gsap: "./g.js" },
scopes: { "./s/": { a: "./a.js" } },
integrity: { "./page.js": "sha384-x" },
});
expect(warn).toHaveBeenCalledTimes(2);
expect(String(warn.mock.calls[0]?.[0])).toContain('"three"');
});
it("leaves the page alone when no mounted file declares a map", () => {
const doc = pageWith("");
mergeImportMapsIntoDocument(doc, []);
expect(doc.querySelector('script[type="importmap"]')).toBeNull();
});
});
+102
View File
@@ -0,0 +1,102 @@
/** The subset of an import map a composition can declare. */
export interface ImportMap {
imports?: Record<string, string>;
scopes?: Record<string, Record<string, string>>;
}
const rebaseEntries = (
entries: Record<string, string> | undefined,
rebase: (url: string) => string,
): Record<string, string> | undefined =>
entries &&
Object.fromEntries(Object.entries(entries).map(([specifier, url]) => [specifier, rebase(url)]));
/**
* Parses a mounted composition's import map, rebasing each address the way its `src`/`href`
* attributes are rebased. Returns null for a map the browser would reject too.
*/
export function parseImportMap(json: string, rebase: (url: string) => string): ImportMap | null {
let parsed: unknown;
try {
parsed = JSON.parse(json);
} catch {
return null;
}
if (!parsed || typeof parsed !== "object") return null;
const { imports, scopes } = parsed as ImportMap;
return {
imports: rebaseEntries(imports, rebase),
scopes:
scopes &&
Object.fromEntries(
Object.entries(scopes).map(([prefix, entries]) => [
rebase(prefix),
rebaseEntries(entries, rebase) ?? {},
]),
),
};
}
function mergeEntries(
into: Record<string, string>,
from: Record<string, string> | undefined,
where: string,
): void {
for (const [specifier, url] of Object.entries(from ?? {})) {
if (!(specifier in into)) into[specifier] = url;
else if (into[specifier] !== url) {
console.warn(
`[HyperFrames] import map conflict for "${specifier}"${where}: keeping ${into[specifier]}, ignoring ${url}.`,
);
}
}
}
/**
* A page has one import map, and it must precede every module script. Mounted compositions'
* maps merge into the page's own (created at the top of <head> if absent); the first mapping
* of a specifier wins and a conflicting later one is dropped with a warning.
*/
export function mergeImportMapsIntoDocument(doc: Document, maps: ImportMap[]): void {
if (maps.length === 0) return;
let el = doc.querySelector('script[type="importmap"]');
let page: ImportMap & Record<string, unknown> = {};
try {
page = el ? JSON.parse(el.textContent || "{}") : {};
} catch {
console.warn("[HyperFrames] the page's import map is not valid JSON; replacing it.");
}
const imports = { ...page.imports };
const scopes = { ...page.scopes };
for (const map of maps) {
mergeEntries(imports, map.imports, "");
for (const [prefix, entries] of Object.entries(map.scopes ?? {})) {
mergeEntries((scopes[prefix] ??= {}), entries, ` in scope ${prefix}`);
}
}
if (!el) {
el = doc.createElement("script");
el.setAttribute("type", "importmap");
doc.head.prepend(el);
}
el.textContent = JSON.stringify({
...page,
imports,
...(Object.keys(scopes).length ? { scopes } : {}),
}).replace(/</g, "\\u003c");
}
/** Emits what inlineSubCompositions collected from mounted files' module and import-map scripts. */
export function emitMountedModuleScripts(
doc: Document,
importMaps: ImportMap[],
moduleScripts: string[],
): void {
mergeImportMapsIntoDocument(doc, importMaps);
for (const content of moduleScripts) {
const el = doc.createElement("script");
el.setAttribute("type", "module");
el.textContent = content;
doc.body.appendChild(el);
}
}
+1
View File
@@ -100,3 +100,4 @@ export {
} from "./mediaRenderIds";
export { ensureExternalScriptTag } from "./externalScripts";
export { emitMountedModuleScripts } from "./importMaps";
@@ -1,4 +1,5 @@
import { readExternalScriptAttributes, type ExternalScriptAttributes } from "./externalScripts";
import { parseImportMap, type ImportMap } from "./importMaps";
/**
* Shared sub-composition inlining logic.
*
@@ -20,6 +21,7 @@ import { warnUnknownEnumValues } from "../runtime/getVariables";
import {
scopeCssToComposition,
wrapInlineScriptWithErrorBoundary,
scopedModulePrelude,
wrapScopedCompositionScript,
} from "./compositionScoping";
import {
@@ -142,6 +144,10 @@ export interface InlineSubCompositionsResult {
>;
externalLinks: { href: string; rel: string; crossorigin?: string }[];
variablesByComp: Record<string, Record<string, unknown>>;
/** Mounted files' import maps, addresses rebased; emit with `emitMountedModuleScripts`. */
importMaps: ImportMap[];
/** Mounted files' inline module scripts, each already carrying its scoped `__hyperframes`. */
moduleScripts: string[];
}
// ---------------------------------------------------------------------------
@@ -197,6 +203,8 @@ export function inlineSubCompositions(
const scripts: string[] = [];
const externalScriptSrcs: string[] = [];
const scriptItems: InlineSubCompositionsResult["scriptItems"] = [];
const importMaps: ImportMap[] = [];
const moduleScripts: string[] = [];
const externalLinks: { href: string; rel: string; crossorigin?: string }[] = [];
const seenLinkHrefs = new Set<string>();
const variablesByComp: Record<string, Record<string, unknown>> = {};
@@ -340,7 +348,26 @@ export function inlineSubCompositions(
// discarded on render while the mount path executed it.
for (const scriptEl of plan.scriptSources) {
const externalSrc = resolveSubAssetPath(scriptEl.getAttribute("src"));
if (externalSrc) {
const type = (scriptEl.getAttribute("type") || "").trim().toLowerCase();
if (type === "importmap") {
const map = parseImportMap(scriptEl.textContent || "", (url) => {
// The rebase drops a leading "./" and a trailing "/"; an import map address needs both.
const authored = url.trim();
const rebased = resolveSubAssetPath(authored);
if (rebased === authored) return rebased;
const urlLike = /^(\/|\.\.?\/|[a-z][a-z\d+.-]*:)/i.test(rebased)
? rebased
: `./${rebased}`;
return authored.endsWith("/") && !urlLike.endsWith("/") ? `${urlLike}/` : urlLike;
});
if (map) importMaps.push(map);
else console.warn(`[HyperFrames] ${src}: import map is not valid JSON, so it is skipped.`);
} else if (type === "module" && !externalSrc) {
const prelude = scriptCompositionId
? scopedModulePrelude(runtimeCompId || scopeCompId || scriptCompositionId, src)
: "";
moduleScripts.push(prelude + (scriptEl.textContent || ""));
} else if (externalSrc) {
if (!externalScriptSrcs.includes(externalSrc)) {
externalScriptSrcs.push(externalSrc);
}
@@ -461,5 +488,14 @@ export function inlineSubCompositions(
}
}
return { styles, scripts, externalScriptSrcs, scriptItems, externalLinks, variablesByComp };
return {
styles,
scripts,
externalScriptSrcs,
scriptItems,
externalLinks,
variablesByComp,
importMaps,
moduleScripts,
};
}
@@ -54,6 +54,30 @@ describe("loadExternalCompositions", () => {
);
});
it("injects a fetched composition's import map and module script bound to its own URL", async () => {
const host = document.createElement("div");
host.setAttribute("data-composition-src", "https://example.com/blocks/blk/blk.html");
host.setAttribute("data-composition-id", "blk");
document.body.appendChild(host);
const compositionHtml =
`<div data-composition-id="blk">` +
`<script type="importmap">{ "imports": { "three": "./lib/three.js" } }</scr` +
`ipt><script type="module">import "three"; window.__url = __hyperframes.assetUrl("a.png");</scr` +
`ipt></div>`;
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(compositionHtml, { status: 200 }));
const injectedScripts: HTMLScriptElement[] = [];
await loadExternalCompositions({ ...defaultParams, injectedScripts });
const importMap = injectedScripts.find((script) => script.type === "importmap");
const moduleScript = injectedScripts.find((script) => script.type === "module");
expect(JSON.parse(importMap?.textContent || "")).toEqual({
imports: { three: "https://example.com/blocks/blk/lib/three.js" },
});
expect(moduleScript?.textContent).toMatch(/^const __hyperframes = /);
expect(moduleScript?.textContent).toContain('"https://example.com/blocks/blk/blk.html"');
});
it("fetches and mounts external composition HTML", async () => {
const host = document.createElement("div");
host.setAttribute("data-composition-src", "https://example.com/comp.html");
+18 -2
View File
@@ -2,7 +2,12 @@ import {
planCompositionAssembly,
EXTRACTED_COMPOSITION_ASSET_SELECTOR,
} from "../compiler/compositionAssembly";
import { scopeCssToComposition, wrapScopedCompositionScript } from "../compiler/compositionScoping";
import {
scopeCssToComposition,
scopedModulePrelude,
wrapScopedCompositionScript,
} from "../compiler/compositionScoping";
import { parseImportMap } from "../compiler/importMaps";
import { markFlattenedInnerRoot } from "./flattenedRoot";
import {
applyCssVariables,
@@ -556,8 +561,19 @@ async function mountCompositionContent(params: {
injectedScript.async = false;
if (scriptPayload.kind === "external") {
injectedScript.src = scriptPayload.src;
} else if (scriptPayload.type.toLowerCase() === "importmap") {
const map = parseImportMap(scriptPayload.content, (url) =>
resolveScriptSourceUrl(url, params.compositionUrl),
);
injectedScript.textContent = map ? JSON.stringify(map) : scriptPayload.content;
} else if (scriptPayload.type.toLowerCase() === "module") {
injectedScript.textContent = scriptPayload.content;
const prelude = scriptPayload.scopeCompositionId
? scopedModulePrelude(
runtimeScopeCompositionId || scriptPayload.scopeCompositionId,
params.compositionUrl?.href,
)
: "";
injectedScript.textContent = prelude + scriptPayload.content;
} else if (scriptPayload.scopeCompositionId) {
injectedScript.textContent = wrapScopedCompositionScript(
scriptPayload.content,
@@ -0,0 +1,66 @@
// @vitest-environment node
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { parseHTML } from "linkedom";
import { afterEach, describe, expect, it } from "vitest";
import { compileForRender } from "./htmlCompiler.js";
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
});
function project(files: Record<string, string>): string {
const dir = mkdtempSync(join(tmpdir(), "hf-module-scripts-"));
tempDirs.push(dir);
for (const [relative, content] of Object.entries(files)) {
const path = join(dir, relative);
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, content);
}
return dir;
}
describe("compileForRender mounted module scripts", () => {
it("keeps a mounted file's import map and module script as such, bound to that file", async () => {
const dir = project({
"index.html": `<!doctype html>
<html><body>
<div data-composition-id="root" data-width="320" data-height="180">
<div data-composition-id="blk" data-composition-src="compositions/blk/blk.html"
data-start="0" data-duration="2"></div>
</div>
</body></html>`,
"compositions/blk/assets/three.js": "export const REVISION = 1;",
"compositions/blk/assets/scene.js": 'import * as THREE from "three"; export const SCENE = 1;',
"compositions/blk/blk.html": `<div data-composition-id="blk" data-width="320" data-height="180">
<script type="module" src="./assets/scene.js"></script>
<script type="importmap">{ "imports": { "three": "./assets/three.js" } }</script>
<script type="module">import * as THREE from "three"; window.__url = __hyperframes.assetUrl("assets/leaf.webp");</script>
</div>`,
});
const { html } = await compileForRender(dir, join(dir, "index.html"), join(dir, ".downloads"), {
allowSystemFontCapture: false,
});
const { document } = parseHTML(html);
const importMap = document.querySelector('script[type="importmap"]');
const modules = [...document.querySelectorAll('script[type="module"]')];
const classic = [...document.querySelectorAll("script:not([type])")].map((s) => s.textContent);
expect(JSON.parse(importMap?.textContent || "null")).toEqual({
imports: { three: "./compositions/blk/assets/three.js" },
});
expect(modules.map((m) => m.getAttribute("src")).filter(Boolean)).toEqual([
"compositions/blk/assets/scene.js",
]);
const inline = modules.filter((m) => !m.hasAttribute("src"));
expect(inline).toHaveLength(1);
expect(inline[0]!.textContent).toMatch(/^const __hyperframes = /);
expect(inline[0]!.textContent).toContain('"compositions/blk/blk.html"');
expect(classic.join("")).not.toContain("SCENE");
expect(classic.join("")).not.toContain('"imports"');
expect(classic.join("")).not.toContain("import * as THREE");
});
});
@@ -41,6 +41,7 @@ import {
buildVariablesByCompScript,
inlineSubCompositions as inlineSubCompositionsShared,
ensureExternalScriptTag,
emitMountedModuleScripts,
prepareFlattenedInnerRoot,
emitRootCompositionVariableStyles,
readDeclaredDefaults,
@@ -1097,6 +1098,13 @@ function inlineSubCompositions(
}
flushInline();
}
if (body) {
emitMountedModuleScripts(
document as unknown as Document,
result.importMaps,
result.moduleScripts,
);
}
// Compile-time CSS custom properties (mirrors the preview bundler): root
// declarers plus one scoped rule per sub-composition host, so var(--slug)