diff --git a/packages/core/src/compiler/htmlBundler.ts b/packages/core/src/compiler/htmlBundler.ts index 121b89336..7be2a0bf8 100644 --- a/packages/core/src/compiler/htmlBundler.ts +++ b/packages/core/src/compiler/htmlBundler.ts @@ -266,7 +266,7 @@ function rewriteCssUrlsWithInlinedAssets(cssText: string, projectDir: string): s ); } -function cssAttributeSelector(attr: string, value: string): string { +export function cssAttributeSelector(attr: string, value: string): string { return `[${attr}="${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"]`; } @@ -274,7 +274,7 @@ function uniqueCompositionId(baseId: string, index: number): string { return `${baseId}__hf${index}`; } -type BundledHostCompositionIdentity = { +export type BundledHostCompositionIdentity = { authoredCompositionId: string | null; runtimeCompositionId: string | null; }; @@ -320,7 +320,8 @@ function countBundledAuthoredCompositionIds(hosts: Element[]): Map = countBundledAuthoredCompositionIds(hosts), ): Map { @@ -366,7 +367,7 @@ function assignBundledRuntimeCompositionIds( return identities; } -function parseHostVariableValues(host: Element): Record { +export function parseHostVariableValues(host: Element): Record { const raw = host.getAttribute("data-variable-values"); if (!raw) return {}; let parsed: unknown; diff --git a/packages/core/src/compiler/index.ts b/packages/core/src/compiler/index.ts index d18803c92..38e5d666e 100644 --- a/packages/core/src/compiler/index.ts +++ b/packages/core/src/compiler/index.ts @@ -16,11 +16,16 @@ export { compileHtml, type MediaDurationProber } from "./htmlCompiler"; // HTML bundler (Node.js — requires fs, linkedom, esbuild) export { + assignBundledRuntimeCompositionIds, bundleToSingleHtml, + cssAttributeSelector, type BundleOptions, + type BundledHostCompositionIdentity, prepareFlattenedInnerRoot, FLATTENED_INNER_ROOT_STRIP_ATTRS, + parseHostVariableValues, } from "./htmlBundler"; +export { readDeclaredDefaults } from "../runtime/getVariables"; export { RUNTIME_BOOTSTRAP_ATTR, diff --git a/packages/producer/src/services/htmlCompiler.test.ts b/packages/producer/src/services/htmlCompiler.test.ts index cb8c9bda8..3e5873162 100644 --- a/packages/producer/src/services/htmlCompiler.test.ts +++ b/packages/producer/src/services/htmlCompiler.test.ts @@ -714,6 +714,66 @@ describe("template-wrapped sub-composition media offsets", () => { expect(compiled.html).toContain("__hfNormalizeSelector"); }); + it("emits per-instance scoped variables for external sub-composition hosts", async () => { + const projectDir = mkdtempSync(join(tmpdir(), "hf-render-vars-")); + const compositionsDir = join(projectDir, "compositions"); + mkdirSync(compositionsDir, { recursive: true }); + const indexPath = join(projectDir, "index.html"); + + writeFileSync( + indexPath, + ` + + +
+
+
+
+ +`, + ); + writeFileSync( + join(compositionsDir, "card.html"), + ` + + +
+ +
+ +`, + ); + + const compiled = await compileForRender(projectDir, indexPath, projectDir); + const { document } = parseHTML(compiled.html); + const cardA = document.querySelector("#card-a"); + const cardB = document.querySelector("#card-b"); + + expect(cardA?.getAttribute("data-composition-id")).toBe("card__hf1"); + expect(cardB?.getAttribute("data-composition-id")).toBe("card__hf2"); + expect(cardA?.getAttribute("data-hf-original-composition-id")).toBe("card"); + expect(cardB?.getAttribute("data-hf-original-composition-id")).toBe("card"); + expect(compiled.html).toContain("window.__hfVariablesByComp"); + expect(compiled.html).toContain('"card__hf1":{"title":"Pro","theme":"light"}'); + expect(compiled.html).toContain('"card__hf2":{"title":"Enterprise","theme":"light"}'); + expect(compiled.html).toContain('var __hfTimelineCompId = "card__hf1"'); + expect(compiled.html).toContain('var __hfTimelineCompId = "card__hf2"'); + }); + it("preserves the inferred composition boundary when the host has no composition id", async () => { const projectDir = mkdtempSync(join(tmpdir(), "hf-anonymous-host-")); const compositionsDir = join(projectDir, "compositions"); diff --git a/packages/producer/src/services/htmlCompiler.ts b/packages/producer/src/services/htmlCompiler.ts index 26309ff34..4864eab3a 100644 --- a/packages/producer/src/services/htmlCompiler.ts +++ b/packages/producer/src/services/htmlCompiler.ts @@ -23,7 +23,13 @@ import { type ResolvedDuration, type UnresolvedElement, } from "@hyperframes/core"; -import { inlineSubCompositions as inlineSubCompositionsShared } from "@hyperframes/core/compiler"; +import { + assignBundledRuntimeCompositionIds, + cssAttributeSelector, + inlineSubCompositions as inlineSubCompositionsShared, + parseHostVariableValues, + readDeclaredDefaults, +} from "@hyperframes/core/compiler"; import { extractMediaMetadata, extractAudioMetadata } from "../utils/ffprobe.js"; import { isPathInside, toExternalAssetKey } from "../utils/paths.js"; import { @@ -573,6 +579,7 @@ function coalesceHeadStylesAndBodyScripts(html: string): string { * compositions from the pre-compiled map or disk, and setting explicit * pixel dimensions on host elements for headless rendering. */ +// fallow-ignore-next-line complexity function inlineSubCompositions( html: string, subCompositions: Map, @@ -584,6 +591,7 @@ function inlineSubCompositions( const hosts = Array.from(document.querySelectorAll("[data-composition-src]")); if (!hosts.length) return html; + const hostIdentityByElement = assignBundledRuntimeCompositionIds(hosts); const result = inlineSubCompositionsShared( document as unknown as Document, @@ -600,8 +608,12 @@ function inlineSubCompositions( return compHtml; }, parseHtml: (htmlStr: string) => parseHTML(htmlStr).document as unknown as Document, + hostIdentityMap: hostIdentityByElement, scriptErrorLabel: "[Compiler] Composition script failed", compoundAuthoredRoot: true, + readVariableDefaults: readDeclaredDefaults, + parseHostVariables: parseHostVariableValues, + buildScopeSelector: (compId: string) => cssAttributeSelector("data-composition-id", compId), }, ); @@ -677,10 +689,17 @@ function inlineSubCompositions( } } + const inlineScripts = [...result.scripts]; + if (Object.keys(result.variablesByComp).length > 0) { + inlineScripts.unshift( + `window.__hfVariablesByComp = Object.assign({}, window.__hfVariablesByComp || {}, ${JSON.stringify(result.variablesByComp)});`, + ); + } + // Append collected inline scripts to - if (result.scripts.length && body) { + if (inlineScripts.length && body) { const scriptEl = document.createElement("script"); - scriptEl.textContent = result.scripts.join("\n;\n"); + scriptEl.textContent = inlineScripts.join("\n;\n"); body.appendChild(scriptEl); }