diff --git a/packages/core/src/compiler/htmlBundler.test.ts b/packages/core/src/compiler/htmlBundler.test.ts
index 195d133e0..8981c1ea5 100644
--- a/packages/core/src/compiler/htmlBundler.test.ts
+++ b/packages/core/src/compiler/htmlBundler.test.ts
@@ -267,24 +267,60 @@ describe("bundleToSingleHtml", () => {
"compositions/scene.html": `
`,
- "shared/dist/vendor/powerglitch.min.js": `window.PowerGlitch = { glitch(){ return { startGlitch(){}, stopGlitch(){} }; } };`,
- "shared/dist/index.js": `window.__HF_SHARED_TEST__ = "shared-runtime-loaded";`,
+ "vendor/effect-plugin.js": `window.PowerGlitch = { glitch(){ return { startGlitch(){}, stopGlitch(){} }; } };`,
+ "assets/scene-runtime.js": `window.__HF_SHARED_TEST__ = "shared-runtime-loaded";`,
});
const bundled = await bundleToSingleHtml(dir);
expect(bundled).toContain('__HF_SHARED_TEST__ = "shared-runtime-loaded"');
expect(bundled).toContain("window.PowerGlitch = { glitch()");
- expect(bundled).not.toContain('src="shared/dist/index.js"');
- expect(bundled).not.toContain('src="shared/dist/vendor/powerglitch.min.js"');
+ expect(bundled).not.toContain('src="assets/scene-runtime.js"');
+ expect(bundled).not.toContain('src="vendor/effect-plugin.js"');
+ });
+
+ it("preserves local sub-composition script order before inline scene scripts", async () => {
+ const dir = makeTempProject({
+ "index.html": `
+
+
+
+
+
+`,
+ "compositions/scene.html": `
+
+
+
+
+`,
+ "assets/component-runtime.js": `window.__HF_COMPONENT_DEF__ = true; window.Component = { mount(){ window.__HF_COMPONENT_MOUNTED__ = true; } };`,
+ });
+
+ const bundled = await bundleToSingleHtml(dir);
+ const componentIndex = bundled.indexOf("__HF_COMPONENT_DEF__");
+ const sceneIndex = bundled.indexOf("__HF_COMPONENT_CALL__");
+
+ expect(componentIndex).toBeGreaterThan(-1);
+ expect(sceneIndex).toBeGreaterThan(-1);
+ expect(componentIndex).toBeLessThan(sceneIndex);
});
it("does not duplicate CDN scripts already present in the main document", async () => {
diff --git a/packages/core/src/compiler/htmlBundler.ts b/packages/core/src/compiler/htmlBundler.ts
index 69debaca2..f4442a955 100644
--- a/packages/core/src/compiler/htmlBundler.ts
+++ b/packages/core/src/compiler/htmlBundler.ts
@@ -718,12 +718,34 @@ export async function bundleToSingleHtml(
},
});
const compStyleChunks: string[] = [...subCompResult.styles];
- const compScriptChunks: string[] = [...subCompResult.scripts];
- const compExternalScriptSrcs: string[] = [...subCompResult.externalScriptSrcs];
+ const compScriptChunks: string[] = [];
const compExternalLinks = [...subCompResult.externalLinks];
const compVariablesByComp: Record> = {
...subCompResult.variablesByComp,
};
+ const seenCompScriptSrcs = new Set();
+ for (const scriptItem of subCompResult.scriptItems) {
+ if (scriptItem.kind === "inline") {
+ compScriptChunks.push(scriptItem.content);
+ continue;
+ }
+ const extSrc = scriptItem.src;
+ if (seenCompScriptSrcs.has(extSrc)) continue;
+ seenCompScriptSrcs.add(extSrc);
+ if (isRelativeUrl(extSrc)) {
+ const jsPath = safePath(projectDir, extSrc);
+ const js = jsPath ? safeReadFile(jsPath) : null;
+ if (js != null) {
+ compScriptChunks.push(js);
+ continue;
+ }
+ }
+ if (!document.querySelector(`script[src="${extSrc}"]`)) {
+ const extScript = document.createElement("script");
+ extScript.setAttribute("src", extSrc);
+ document.body.appendChild(extScript);
+ }
+ }
// Inline template compositions: inject content into
// matching empty host elements with data-composition-id="X" (no data-composition-src)
@@ -773,8 +795,23 @@ export async function bundleToSingleHtml(
for (const scriptEl of [...innerRoot.querySelectorAll("script")]) {
const externalSrc = (scriptEl.getAttribute("src") || "").trim();
if (externalSrc) {
- if (!compExternalScriptSrcs.includes(externalSrc)) {
- compExternalScriptSrcs.push(externalSrc);
+ if (!seenCompScriptSrcs.has(externalSrc)) {
+ seenCompScriptSrcs.add(externalSrc);
+ if (isRelativeUrl(externalSrc)) {
+ const jsPath = safePath(projectDir, externalSrc);
+ const js = jsPath ? safeReadFile(jsPath) : null;
+ if (js != null) {
+ compScriptChunks.push(js);
+ } else if (!document.querySelector(`script[src="${externalSrc}"]`)) {
+ const extScript = document.createElement("script");
+ extScript.setAttribute("src", externalSrc);
+ document.body.appendChild(extScript);
+ }
+ } else if (!document.querySelector(`script[src="${externalSrc}"]`)) {
+ const extScript = document.createElement("script");
+ extScript.setAttribute("src", externalSrc);
+ document.body.appendChild(extScript);
+ }
}
} else {
compScriptChunks.push(
@@ -810,8 +847,23 @@ export async function bundleToSingleHtml(
for (const scriptEl of [...innerDoc.querySelectorAll("script")]) {
const externalSrc = (scriptEl.getAttribute("src") || "").trim();
if (externalSrc) {
- if (!compExternalScriptSrcs.includes(externalSrc)) {
- compExternalScriptSrcs.push(externalSrc);
+ if (!seenCompScriptSrcs.has(externalSrc)) {
+ seenCompScriptSrcs.add(externalSrc);
+ if (isRelativeUrl(externalSrc)) {
+ const jsPath = safePath(projectDir, externalSrc);
+ const js = jsPath ? safeReadFile(jsPath) : null;
+ if (js != null) {
+ compScriptChunks.push(js);
+ } else if (!document.querySelector(`script[src="${externalSrc}"]`)) {
+ const extScript = document.createElement("script");
+ extScript.setAttribute("src", externalSrc);
+ document.body.appendChild(extScript);
+ }
+ } else if (!document.querySelector(`script[src="${externalSrc}"]`)) {
+ const extScript = document.createElement("script");
+ extScript.setAttribute("src", externalSrc);
+ document.body.appendChild(extScript);
+ }
}
} else {
compScriptChunks.push(
@@ -839,22 +891,6 @@ export async function bundleToSingleHtml(
// Inject external scripts from sub-compositions (e.g., Lottie CDN)
// that aren't already present in the main document.
- for (const extSrc of compExternalScriptSrcs) {
- if (isRelativeUrl(extSrc)) {
- const jsPath = safePath(projectDir, extSrc);
- const js = jsPath ? safeReadFile(jsPath) : null;
- if (js != null) {
- compScriptChunks.push(js);
- continue;
- }
- }
- if (!document.querySelector(`script[src="${extSrc}"]`)) {
- const extScript = document.createElement("script");
- extScript.setAttribute("src", extSrc);
- document.body.appendChild(extScript);
- }
- }
-
for (const link of compExternalLinks) {
const escapedHref = link.href.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
if (!document.querySelector(`link[href="${escapedHref}"]`)) {
diff --git a/packages/core/src/compiler/inlineSubCompositions.ts b/packages/core/src/compiler/inlineSubCompositions.ts
index 77d2dcd25..647e51988 100644
--- a/packages/core/src/compiler/inlineSubCompositions.ts
+++ b/packages/core/src/compiler/inlineSubCompositions.ts
@@ -106,6 +106,7 @@ export interface InlineSubCompositionsResult {
styles: string[];
scripts: string[];
externalScriptSrcs: string[];
+ scriptItems: Array<{ kind: "inline"; content: string } | { kind: "external"; src: string }>;
externalLinks: { href: string; rel: string; crossorigin?: string }[];
variablesByComp: Record>;
}
@@ -160,6 +161,7 @@ export function inlineSubCompositions(
const styles: string[] = [];
const scripts: string[] = [];
const externalScriptSrcs: string[] = [];
+ const scriptItems: InlineSubCompositionsResult["scriptItems"] = [];
const externalLinks: { href: string; rel: string; crossorigin?: string }[] = [];
const seenLinkHrefs = new Set();
const variablesByComp: Record> = {};
@@ -232,8 +234,11 @@ export function inlineSubCompositions(
}
for (const s of [...compDoc.head.querySelectorAll("script")]) {
const externalSrc = (s.getAttribute("src") || "").trim();
- if (externalSrc && !externalScriptSrcs.includes(externalSrc)) {
- externalScriptSrcs.push(externalSrc);
+ if (externalSrc) {
+ if (!externalScriptSrcs.includes(externalSrc)) {
+ externalScriptSrcs.push(externalSrc);
+ }
+ scriptItems.push({ kind: "external", src: externalSrc });
}
}
for (const link of [
@@ -271,19 +276,20 @@ export function inlineSubCompositions(
if (!externalScriptSrcs.includes(externalSrc)) {
externalScriptSrcs.push(externalSrc);
}
+ scriptItems.push({ kind: "external", src: externalSrc });
} else {
- scripts.push(
- scopeCompId
- ? wrapScopedCompositionScript(
- s.textContent || "",
- scopeCompId,
- scriptErrorLabel,
- runtimeScope || undefined,
- runtimeCompId || scopeCompId,
- authoredRootId,
- )
- : `(function(){ try { ${s.textContent || ""} } catch (_err) { console.error(${JSON.stringify(scriptErrorLabel)}, _err); } })();`,
- );
+ const wrappedScript = scopeCompId
+ ? wrapScopedCompositionScript(
+ s.textContent || "",
+ scopeCompId,
+ scriptErrorLabel,
+ runtimeScope || undefined,
+ runtimeCompId || scopeCompId,
+ authoredRootId,
+ )
+ : `(function(){ try { ${s.textContent || ""} } catch (_err) { console.error(${JSON.stringify(scriptErrorLabel)}, _err); } })();`;
+ scripts.push(wrappedScript);
+ scriptItems.push({ kind: "inline", content: wrappedScript });
}
s.remove();
}
@@ -359,5 +365,5 @@ export function inlineSubCompositions(
hostEl.removeAttribute("data-composition-src");
}
- return { styles, scripts, externalScriptSrcs, externalLinks, variablesByComp };
+ return { styles, scripts, externalScriptSrcs, scriptItems, externalLinks, variablesByComp };
}