From 278bc712d7b40d03cd3bfa6a705cf64e2f709c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 24 Sep 2026 23:44:04 -0400 Subject: [PATCH] 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 --- .../src/compiler/compositionScoping.test.ts | 21 ++++ .../core/src/compiler/compositionScoping.ts | 38 ++++--- packages/core/src/compiler/externalScripts.ts | 3 + .../core/src/compiler/htmlBundler.test.ts | 49 +++++++++ packages/core/src/compiler/htmlBundler.ts | 6 +- packages/core/src/compiler/importMaps.test.ts | 55 ++++++++++ packages/core/src/compiler/importMaps.ts | 102 ++++++++++++++++++ packages/core/src/compiler/index.ts | 1 + .../src/compiler/inlineSubCompositions.ts | 40 ++++++- .../src/runtime/compositionLoader.test.ts | 24 +++++ .../core/src/runtime/compositionLoader.ts | 20 +++- .../htmlCompiler.moduleScripts.test.ts | 66 ++++++++++++ .../producer/src/services/htmlCompiler.ts | 8 ++ 13 files changed, 414 insertions(+), 19 deletions(-) create mode 100644 packages/core/src/compiler/importMaps.test.ts create mode 100644 packages/core/src/compiler/importMaps.ts create mode 100644 packages/producer/src/services/htmlCompiler.moduleScripts.test.ts diff --git a/packages/core/src/compiler/compositionScoping.test.ts b/packages/core/src/compiler/compositionScoping.test.ts index 2449f21349..b5bb34fc25 100644 --- a/packages/core/src/compiler/compositionScoping.test.ts +++ b/packages/core/src/compiler/compositionScoping.test.ts @@ -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(`
`); + 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(`
`); diff --git a/packages/core/src/compiler/compositionScoping.ts b/packages/core/src/compiler/compositionScoping.ts index 133f861e9a..f463fefca0 100644 --- a/packages/core/src/compiler/compositionScoping.ts +++ b/packages/core/src/compiler/compositionScoping.ts @@ -276,6 +276,30 @@ function jsonScriptLiteral(value: unknown): string { return JSON.stringify(value).replace(/ { 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": ` + +
+
+
+`, + "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": `
+ + + + + +
`, + }); + + 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