Skip to content
Merged
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
30 changes: 30 additions & 0 deletions packages/core/src/compiler/htmlBundler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3441,6 +3441,36 @@ ${["b", "h"].map((id, i) => ` <div data-composition-id="${id}" data-compositi
]);
});

it("lets a scene wait when its script only marks a node in its own root or reads an attribute off <html>", async () => {
const scene = (id: string, script: string) =>
`<template id="${id}-template"><div data-composition-id="${id}" id="r-${id}"><p>${id}</p>
<script>${script}</script></div></template>`;
const claim = (id: string) =>
`const root = (function(){var a=document.querySelectorAll('[id="r-${id}"]');for(var i=0;i<a.length;i++){if(!a[i].__hfClaimed){a[i].__hfClaimed=1;return a[i];}}return document.getElementById('r-${id}');})();`;
const dir = makeTempProject({
"index.html": `<!doctype html><html><head></head><body>
<div data-composition-id="main" data-width="1920" data-height="1080" data-duration="8">
${["g", "h", "i", "j"].map((id, i) => ` <div data-composition-id="${id}" data-composition-src="compositions/${id}.html" data-start="${i * 2}" data-duration="2"></div>`).join("\n")}
</div></body></html>`,
"compositions/g.html": scene("g", claim("g")),
"compositions/h.html": scene(
"h",
'JSON.parse(document.documentElement.getAttribute("data-composition-variables") || "[]");',
),
"compositions/i.html": scene("i", "document.documentElement.classList.add('dark');"),
"compositions/j.html": scene("j", "window.__hfClaimed = true;"),
});
const doc = parseHTML(await bundleToSingleHtml(dir, { sceneParts: true })).document;
const eager = (id: string) =>
doc.querySelector(`div[data-hf-scene="${id}"]`)?.getAttribute("data-hf-scene-eager");
expect(["g", "h", "i", "j"].map(eager)).toEqual([
null,
null,
"its script uses documentElement",
"its script uses window.__hfClaimed",
]);
});

it("marks a scene whose own script leaves work running as not swappable, and only that scene", async () => {
const dir = makeTempProject({
"index.html": `<!doctype html><html><head>
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/compiler/inlineSubCompositions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ function sceneScriptSwapRefusal(script: string): string | null {
return match ? `its script uses ${match[0].trim()}` : null;
}

// What ties a scene script to boot order: reaching outside its root, or leaving state on window for
// other scripts (the timeline registry aside).
// What ties a scene script to boot order: reaching outside its root (reading <html>'s attributes and
// marking nodes its scoped `document` found stay inside), or leaving state on window for others.
const REACHES_OUTSIDE_RE =
/\b(documentElement|getElementsBy\w+|__hf[A-Z]\w*)\b|document\s*\.\s*(head|body)\b|querySelector(All)?\(\s*["'](head|body)["']|defineProperty\s*\(\s*(window|globalThis|self|document)\b|\b(window|globalThis|self)\s*(\.\s*(?!__timelines\b)[A-Za-z_$][\w$]*|\[[^\]]*\])\s*=(?!=)/;
/\bgetElementsBy\w+\b|\bdocumentElement\b(?!\s*\.\s*(?:get|has)Attribute\s*\()|(?<!\.\s*)\b__hf[A-Z]\w*\b|\b(?:window|globalThis|self)\s*\.\s*__hf[A-Z]\w*|document\s*\.\s*(head|body)\b|querySelector(All)?\(\s*["'](head|body)["']|defineProperty\s*\(\s*(window|globalThis|self|document)\b|\b(window|globalThis|self)\s*(\.\s*(?!__timelines\b)[A-Za-z_$][\w$]*|\[[^\]]*\])\s*=(?!=)/;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep shared-object __hf* properties eager

The dotted-property exception also admits document.__hfSharedTone, although the scoped document is a Proxy over the real Document, with only the three query getters overridden and no private set trap (compositionScoping.ts:395–405). Two authored data-composition-src scenes can therefore share this property. Put a preparer first in source order at start=60/duration=5 that assigns document.__hfSharedTone = 'ready', followed by a reader at start=30/duration=5 that snapshots String(document.__hfSharedTone || 'unset') into its scoped .out. Each can register only its own timeline, with a 70-second root and no cross-scene registry/selector dependencies.

BASE marks both eager and initial mounting runs preparer then reader, producing ready. HEAD marks neither eager: the reader mounts at t=20 and captures unset; the preparer only mounts at t=50. takeHeldScenesNear removes each entry once, so later preparation does not repair the reader's visible text. The same overly broad receiver assumption admits aliases such as const page = window; page.__hfReady = 'ready', despite the window proxy forwarding that write globally. Preserve shared-receiver eagerness or establish that the receiver is actually a scene-local node. This is a source-derived ordering witness, not a claimed executed reproduction.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve ordering for mutable shared <html> attribute reads

documentElement falls through the scoped document proxy to the real outer <html>. A getter writes nothing, but can still depend on its position relative to an eager writer. For example, start with <html data-shared-tone='warm'>; put a reader scene first in source order at start=30/duration=5 that snapshots document.documentElement.getAttribute('data-shared-tone') into its scoped .out, then a writer at start=60/duration=5 that calls document.documentElement.setAttribute('data-shared-tone', 'cool'). Use a 70-second root and only own-key timeline registrations.

BASE keeps both eager, so the reader captures warm before the writer changes the attribute. HEAD defers the reader but keeps the writer eager: the writer runs at initial mounting, and the reader captures cool at t=20 instead. Neither registry dependency closure nor the unchanged no-swap marker preserves this ordering; held-scene scheduling does not consult no-swap. Restrict the exemption to guaranteed-immutable reads or preserve their ordering dependencies, rather than allowing arbitrary getAttribute/hasAttribute calls. This is a source-derived BASE/HEAD control, not a claimed browser test.


/** Why an authored scene script must run at boot rather than when the playhead nears, or null. */
function sceneScriptEagerReason(script: string): string | null {
Expand Down
Loading