Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions packages/core/src/compiler/htmlBundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,15 +266,15 @@ 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, '\\"')}"]`;
}

function uniqueCompositionId(baseId: string, index: number): string {
return `${baseId}__hf${index}`;
}

type BundledHostCompositionIdentity = {
export type BundledHostCompositionIdentity = {
authoredCompositionId: string | null;
runtimeCompositionId: string | null;
};
Expand Down Expand Up @@ -320,7 +320,8 @@ function countBundledAuthoredCompositionIds(hosts: Element[]): Map<string, numbe
return counts;
}

function assignBundledRuntimeCompositionIds(
// fallow-ignore-next-line complexity
export function assignBundledRuntimeCompositionIds(
hosts: Element[],
counts: Map<string, number> = countBundledAuthoredCompositionIds(hosts),
): Map<Element, BundledHostCompositionIdentity> {
Expand Down Expand Up @@ -366,7 +367,7 @@ function assignBundledRuntimeCompositionIds(
return identities;
}

function parseHostVariableValues(host: Element): Record<string, unknown> {
export function parseHostVariableValues(host: Element): Record<string, unknown> {
const raw = host.getAttribute("data-variable-values");
if (!raw) return {};
let parsed: unknown;
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/compiler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
60 changes: 60 additions & 0 deletions packages/producer/src/services/htmlCompiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
`<!DOCTYPE html>
<html>
<body>
<div id="root" data-composition-id="root" data-width="640" data-height="360" data-duration="4">
<div
id="card-a"
data-composition-id="card"
data-composition-src="compositions/card.html"
data-variable-values='{"title":"Pro"}'></div>
<div
id="card-b"
data-composition-id="card"
data-composition-src="compositions/card.html"
data-variable-values='{"title":"Enterprise"}'></div>
</div>
</body>
</html>`,
);
writeFileSync(
join(compositionsDir, "card.html"),
`<!DOCTYPE html>
<html data-composition-variables='[
{"id":"title","type":"string","label":"Title","default":"Default Title"},
{"id":"theme","type":"string","label":"Theme","default":"light"}
]'>
<body>
<div id="card-root" data-composition-id="card" data-width="640" data-height="360" data-duration="4">
<script>
window.__captured = window.__captured || [];
window.__captured.push(__hyperframes.getVariables());
</script>
</div>
</body>
</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");
Expand Down
25 changes: 22 additions & 3 deletions packages/producer/src/services/htmlCompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<string, string>,
Expand All @@ -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,
Expand All @@ -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),
},
);

Expand Down Expand Up @@ -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 <body>
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);
}

Expand Down