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);
}