Skip to content

Commit 89fe898

Browse files
fix(core): an added sub-composition keeps its declared variable defaults when bundled (#4539)
* fix(core): an added sub-composition keeps its declared variable defaults when bundled parseHTMLContent only treated input as a full document when it started with a doctype or <html>. Files installed by `hyperframes add` start with a marker comment, so they were wrapped as a fragment in a new <html> and lost their own <html data-composition-variables>. Leading comments are now skipped first. * fix(studio-server): an installed block's preview keeps its <html> variables isFullHtmlDocument moves to core beside parseHTMLContent and both callers use it, so the standalone preview of a file that `hyperframes add` installed (a marker comment above the doctype) is treated as the full document it is. The leading-comment skip also handles an empty `<!-->` comment, and `<html-card>` style elements no longer count as a document. * fix(core): a doctype written without a space still reads as a full document
1 parent c238abf commit 89fe898

5 files changed

Lines changed: 72 additions & 12 deletions

File tree

‎packages/core/src/compiler/htmlBundler.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,32 @@ describe("bundleToSingleHtml", () => {
962962
expect(externalHost?.querySelector("p")?.textContent).toBe("External scene");
963963
});
964964

965+
it("keeps an installed sub-composition's declared defaults for getVariables", async () => {
966+
// `hyperframes add` writes a marker comment above the doctype.
967+
const dir = makeTempProject({
968+
"index.html": `<!doctype html>
969+
<html><head></head><body>
970+
<div id="root" data-composition-id="main" data-width="1920" data-height="1080">
971+
<div data-composition-id="blk" data-composition-src="compositions/blk.html"></div>
972+
</div>
973+
<script>window.__timelines={};</script>
974+
</body></html>`,
975+
"compositions/blk.html": `<!-- hyperframes-registry-item: blk -->
976+
<!doctype html>
977+
<html data-composition-variables='[{"id":"image1","type":"image","default":"assets/blk/one.jpg"}]'>
978+
<body>
979+
<div id="blk-root" data-composition-id="blk" data-width="1920" data-height="1080">
980+
<script>window.__blkVars = __hyperframes.getVariables();</script>
981+
</div>
982+
</body>
983+
</html>`,
984+
});
985+
986+
const bundled = await bundleToSingleHtml(dir);
987+
988+
expect(bundled).toMatch(/__hfVariablesByComp = Object\.assign\([^;]*assets\/blk\/one\.jpg/);
989+
});
990+
965991
it("emits per-instance scoped variables for bundled sub-compositions", async () => {
966992
const dir = makeTempProject({
967993
"index.html": `<!doctype html>

‎packages/core/src/compiler/htmlDocument.test.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,29 @@ import {
44
injectScriptsIntoHtml,
55
injectTagsAtHeadStart,
66
insertBeforeCloseTag,
7+
isFullHtmlDocument,
78
parseHTMLContent,
89
stripEmbeddedRuntimeScripts,
910
} from "./htmlDocument.js";
1011

1112
describe("htmlDocument helpers", () => {
13+
it("keeps a document's <html> attributes when a comment comes before the doctype", () => {
14+
const doc = parseHTMLContent(
15+
'<!-- hyperframes-registry-item: blk -->\n<!doctype html>\n<html lang="en" data-composition-variables="[]"><body></body></html>',
16+
);
17+
expect(doc.documentElement.getAttribute("lang")).toBe("en");
18+
expect(doc.documentElement.hasAttribute("data-composition-variables")).toBe(true);
19+
});
20+
21+
it("tells a document from a fragment past leading comments", () => {
22+
expect(isFullHtmlDocument("<!-- marker -->\n<!doctype html><html></html>")).toBe(true);
23+
expect(isFullHtmlDocument("<!-- a --><!-- b --><html lang='en'></html>")).toBe(true);
24+
expect(isFullHtmlDocument("<!-- marker --><div data-composition-id='x'></div>")).toBe(false);
25+
expect(isFullHtmlDocument("<!--><div></div><!-- --><html></html>")).toBe(false);
26+
expect(isFullHtmlDocument("<html-card></html-card>")).toBe(false);
27+
expect(isFullHtmlDocument("<!DOCTYPEhtml><html/ lang='en'></html>")).toBe(true);
28+
});
29+
1230
it("wraps fragments before parsing", () => {
1331
const doc = parseHTMLContent("<template><span>hello</span></template>");
1432
expect(doc.body.querySelector("template")?.innerHTML).toContain("<span>hello</span>");

‎packages/core/src/compiler/htmlDocument.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,18 @@ const SIMPLE_RUNTIME_FLAG_ASSIGNMENTS = [
2222
/^window\.__renderReady\s*=\s*(?:true|false)\s*;?$/,
2323
];
2424

25+
const LEADING_COMMENTS = /^(?:\s|<!--(?:>|->|[\s\S]*?-->))*/;
26+
27+
export function isFullHtmlDocument(html: string): boolean {
28+
return /^(?:<!doctype|<html[\s>/])/i.test(html.replace(LEADING_COMMENTS, ""));
29+
}
30+
2531
/**
2632
* Parse a full HTML document or wrap a fragment so linkedom consistently puts
2733
* fragment content under document.body.
2834
*/
2935
export function parseHTMLContent(html: string): Document {
30-
const trimmed = html.trimStart().toLowerCase();
31-
if (trimmed.startsWith("<!doctype") || trimmed.startsWith("<html")) {
36+
if (isFullHtmlDocument(html)) {
3237
return parseHTML(html).document;
3338
}
3439
return parseHTML(`<!DOCTYPE html><html><head></head><body>${html}</body></html>`).document;

‎packages/studio-server/src/helpers/subComposition.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,26 @@ describe("buildSubCompositionHtml", () => {
9090
expect(html).toContain('lang="en"');
9191
});
9292

93+
it("keeps an installed block's <html> variables though a marker comment precedes the doctype", () => {
94+
const dir = makeTempProject({
95+
"index.html": `<!doctype html>
96+
<html><head></head><body></body></html>`,
97+
"compositions/blk.html": `<!-- hyperframes-registry-item: blk -->
98+
<!doctype html>
99+
<html lang="en" data-composition-variables='[{"id":"image1","type":"image","default":"assets/blk/one.jpg"}]'>
100+
<body>
101+
<div id="root" data-composition-id="blk" data-width="1920" data-height="1080"></div>
102+
</body>
103+
</html>`,
104+
});
105+
106+
const html = buildSubCompositionHtml(dir, "compositions/blk.html", "/api/runtime.js", "/p/");
107+
108+
const body = html!.indexOf("<body");
109+
expect(html!.slice(0, body)).toContain("data-composition-variables=");
110+
expect(html!.slice(body)).not.toContain("<html");
111+
});
112+
93113
it("handles raw fragment compositions (no template, no full document)", () => {
94114
const dir = makeTempProject({
95115
"index.html": `<!doctype html>

‎packages/studio-server/src/helpers/subComposition.ts‎

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,7 @@ import {
77
rewriteInlineStyleAssetUrls,
88
} from "@hyperframes/core";
99
import { stripEmbeddedRuntimeScripts } from "@hyperframes/core/compiler";
10-
11-
/**
12-
* Detect whether `html` is a full document (has `<html>`, `<head>`, or
13-
* `<!doctype`), as opposed to a `<template>`-wrapped fragment.
14-
* Anchored to start-of-string (ignoring leading whitespace) so stray
15-
* occurrences inside script/template content don't false-positive.
16-
*/
17-
function isFullHtmlDocument(html: string): boolean {
18-
return /^\s*(?:<!doctype\s|<html[\s>])/i.test(html);
19-
}
10+
import { isFullHtmlDocument } from "@hyperframes/core/compiler/html-document";
2011

2112
/**
2213
* Rewrite relative asset paths in a parsed DOM tree. Shared across all

0 commit comments

Comments
 (0)