/** * Shared sub-composition inlining logic. * * Both the core bundler (preview) and the producer compiler (render) need to * inline sub-composition HTML referenced via `data-composition-src`. This * module is the single source of truth for that transformation, eliminating * divergence that previously caused bugs (e.g. producer not setting * `data-composition-file`). */ import { rewriteAssetPaths, rewriteCssAssetUrls, rewriteInlineStyleAssetUrls, } from "./rewriteSubCompPaths"; import { scopeCssToComposition, wrapInlineScriptWithErrorBoundary, wrapScopedCompositionScript, } from "./compositionScoping"; // --------------------------------------------------------------------------- // Public interface // --------------------------------------------------------------------------- export interface InlineSubCompositionsOptions { /** * Resolve the HTML content for a sub-composition given its `data-composition-src` value. * Return `null` when the file cannot be found. */ resolveHtml: (srcPath: string) => string | null; /** * Parse an HTML string into a Document. The returned object must expose * standard DOM APIs (querySelector, querySelectorAll, body, head, etc.). * Both linkedom's `parseHTML(...).document` and the core bundler's * `parseHTMLContent(...)` satisfy this contract. */ parseHtml: (html: string) => Document; /** * Identity map produced by `assignBundledRuntimeCompositionIds`. * When provided, authoredCompositionId and runtimeCompositionId are read * from this map instead of from the host element's attributes directly. * The bundler uses this; the producer can omit it. */ hostIdentityMap?: Map< Element, { authoredCompositionId: string | null; runtimeCompositionId: string | null } >; /** * When true, rewrite `url(...)` references in inline `style` attributes * on sub-composition elements. The bundler enables this; the producer * can skip it. */ rewriteInlineStyles?: boolean; /** * Prepare the inner root element before injecting it into the host. * The bundler's `prepareFlattenedInnerRoot` clones the element, strips * timing attributes, and adds `data-hf-inner-root`. When omitted, the * inner root's outerHTML is injected as-is. */ flattenInnerRoot?: (innerRoot: Element) => Element; /** * When true, CSS selectors targeting the authored root use a compound * selector (`[scope][root]`) instead of a descendant (`[scope] [root]`). * Enable this in the producer path where the inner root merges onto * the host element via innerHTML — both attributes end up on the same * element and a descendant selector won't match. */ compoundAuthoredRoot?: boolean; /** * Read declared variable defaults from a sub-composition's `` element. * The bundler passes `readDeclaredDefaults`; the producer can omit this. */ readVariableDefaults?: (docElement: Element) => Record; /** * Parse host-level variable overrides from `data-variable-values`. * The bundler passes `parseHostVariableValues`; the producer can omit this. */ parseHostVariables?: (host: Element) => Record; /** * Build a CSS attribute selector for scoping, e.g. * `[data-composition-id="my-comp"]`. Defaults to a simple implementation * when not provided. The bundler passes `cssAttributeSelector` which * handles escaping. */ buildScopeSelector?: (compId: string) => string; /** * Error label prefix used in wrapped composition scripts. * Defaults to `"[HyperFrames] composition script error:"`. */ scriptErrorLabel?: string; /** * Log a warning when a composition file cannot be resolved. * Defaults to `console.warn`. */ onMissingComposition?: (srcPath: string) => void; } 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>; } // --------------------------------------------------------------------------- // Default helpers // --------------------------------------------------------------------------- function defaultBuildScopeSelector(compId: string): string { const escaped = compId.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); return `[data-composition-id="${escaped}"]`; } // --------------------------------------------------------------------------- // Core implementation // --------------------------------------------------------------------------- /** * 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 `