From 0618fc0dc38ce9abbd4ff6af2c35146c7490b166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 23 Sep 2026 12:49:04 -0400 Subject: [PATCH] fix(parsers): render bare fragment sub-compositions (#4335) --- .../core/src/compiler/compositionAssembly.ts | 11 ++++-- .../compiler/inlineSubCompositions.test.ts | 20 +++++++++++ .../src/compiler/inlineSubCompositions.ts | 27 ++++++++------ .../src/runtime/compositionLoader.test.ts | 22 ++++++++++++ .../core/src/runtime/compositionLoader.ts | 8 +++-- .../src/subCompositionValidity.test.ts | 18 ++++++++++ .../parsers/src/subCompositionValidity.ts | 25 ++++++++----- .../src/services/htmlCompiler.test.ts | 21 +++++++++++ .../producer/src/services/htmlCompiler.ts | 2 +- packages/producer/tests/shard-schedule.json | 1 + .../tests/subcomp-bare-fragment/meta.json | 13 +++++++ .../output/compiled.html | 3 ++ .../subcomp-bare-fragment/output/output.mp4 | 3 ++ .../src/compositions/scene.css | 3 ++ .../src/compositions/scene.html | 19 ++++++++++ .../subcomp-bare-fragment/src/index.html | 35 +++++++++++++++++++ 16 files changed, 207 insertions(+), 24 deletions(-) create mode 100644 packages/producer/tests/subcomp-bare-fragment/meta.json create mode 100644 packages/producer/tests/subcomp-bare-fragment/output/compiled.html create mode 100644 packages/producer/tests/subcomp-bare-fragment/output/output.mp4 create mode 100644 packages/producer/tests/subcomp-bare-fragment/src/compositions/scene.css create mode 100644 packages/producer/tests/subcomp-bare-fragment/src/compositions/scene.html create mode 100644 packages/producer/tests/subcomp-bare-fragment/src/index.html diff --git a/packages/core/src/compiler/compositionAssembly.ts b/packages/core/src/compiler/compositionAssembly.ts index 7564c30200..256ae161c4 100644 --- a/packages/core/src/compiler/compositionAssembly.ts +++ b/packages/core/src/compiler/compositionAssembly.ts @@ -29,11 +29,13 @@ const STYLE_SELECTOR = "style"; const SCRIPT_SELECTOR = "script"; /** - * `` links both paths hoist into the host document. Stylesheets carry + * Links both paths hoist into the host document. Stylesheets carry * webfonts a composition's CSS depends on; preconnects are their latency hint. */ const HOISTED_LINK_SELECTOR = 'link[rel="stylesheet"], link[rel="preconnect"]'; +export const EXTRACTED_COMPOSITION_ASSET_SELECTOR = `${STYLE_SELECTOR}, ${SCRIPT_SELECTOR}, ${HOISTED_LINK_SELECTOR}`; + /** * The compiler's nesting cap, enforced against the ancestry chain rather than a * counter so a wide tree is not penalised for a deep sibling. @@ -131,7 +133,7 @@ export interface CompositionAssemblyPlan { */ scriptSources: TElement[]; - /** `` links to hoist into the host document. */ + /** Head and content links to hoist into the host document. */ linkSources: TElement[]; /** @@ -189,7 +191,10 @@ export function planCompositionAssembly( ...toArray(assetHead?.querySelectorAll(SCRIPT_SELECTOR)), ...toArray(contentNode.querySelectorAll(SCRIPT_SELECTOR)), ], - linkSources: toArray(head?.querySelectorAll(HOISTED_LINK_SELECTOR)), + linkSources: [ + ...toArray(head?.querySelectorAll(HOISTED_LINK_SELECTOR)), + ...toArray(contentNode.querySelectorAll(HOISTED_LINK_SELECTOR)), + ], variableDefaultCarriers: [documentElement, innerRoot].filter( (carrier): carrier is TElement => carrier != null, ), diff --git a/packages/core/src/compiler/inlineSubCompositions.test.ts b/packages/core/src/compiler/inlineSubCompositions.test.ts index a05e993fab..e89d0dee88 100644 --- a/packages/core/src/compiler/inlineSubCompositions.test.ts +++ b/packages/core/src/compiler/inlineSubCompositions.test.ts @@ -68,6 +68,26 @@ describe("inlineSubCompositions – #ID selector scoping divergence", () => { expect(result.scripts).toHaveLength(0); }); + it("hoists stylesheet and preconnect links from a bare fragment", () => { + const document = makeHostDocument("intro"); + const host = document.querySelector('[data-composition-src="intro.html"]')!; + const result = inlineSubCompositions(document, [host], { + resolveHtml: () => ` + + +
Hello
`, + parseHtml: (html) => parseHTML(html).document, + }); + + expect(result.externalLinks).toEqual([ + { href: "theme.css", rel: "stylesheet", crossorigin: undefined }, + { href: "https://fonts.example", rel: "preconnect", crossorigin: "" }, + { href: "nested.css", rel: "stylesheet", crossorigin: undefined }, + ]); + expect(host.querySelectorAll('link[rel="stylesheet"]')).toHaveLength(0); + expect(host.querySelectorAll('link[rel="icon"]')).toHaveLength(1); + }); + it("passes the failure reason through to onMissingComposition", () => { const document = makeHostDocument("intro"); const host = document.querySelector('[data-composition-src="intro.html"]')!; diff --git a/packages/core/src/compiler/inlineSubCompositions.ts b/packages/core/src/compiler/inlineSubCompositions.ts index 57d42ec5d9..c0fcadb64d 100644 --- a/packages/core/src/compiler/inlineSubCompositions.ts +++ b/packages/core/src/compiler/inlineSubCompositions.ts @@ -22,8 +22,15 @@ import { wrapInlineScriptWithErrorBoundary, wrapScopedCompositionScript, } from "./compositionScoping"; -import { checkSubCompositionUsability } from "@hyperframes/parsers/sub-composition-validity"; -import { enumerateNestedCompositionHosts, planCompositionAssembly } from "./compositionAssembly"; +import { + checkSubCompositionUsability, + resolveSubCompositionContent, +} from "@hyperframes/parsers/sub-composition-validity"; +import { + enumerateNestedCompositionHosts, + planCompositionAssembly, + EXTRACTED_COMPOSITION_ASSET_SELECTOR, +} from "./compositionAssembly"; // --------------------------------------------------------------------------- // Public interface @@ -154,7 +161,7 @@ function defaultBuildScopeSelector(compId: string): string { * Inline sub-compositions into a document. For each host element in `hosts`: * * 1. Resolve the sub-composition HTML via `options.resolveHtml` - * 2. Parse it, find `