import { markFlattenedInnerRoot } from "../runtime/flattenedRoot";
export { FLATTENED_INNER_ROOT_STRIP_ATTRS } from "../runtime/flattenedRoot";
import { parseHostVariableValues } from "../runtime/getVariables";
import { cssVariableName } from "../tokenSlug";
import { readFileSync, existsSync } from "fs";
import { resolve, relative, dirname, isAbsolute, sep } from "path";
import { CSS_URL_RE, isNonRelativeUrl } from "./assetPaths.js";
import { transformSync } from "esbuild";
import { compileHtml, type MediaDurationProber } from "./htmlCompiler";
import {
RUNTIME_BOOTSTRAP_ATTR,
parseHTMLContent,
stripEmbeddedRuntimeScripts,
} from "./htmlDocument";
// rewriteSubCompPaths functions are used by inlineSubCompositions (shared module)
import {
buildVariablesByCompScript,
scopeCssToComposition,
wrapInlineScriptWithErrorBoundary,
wrapScopedCompositionScript,
} from "./compositionScoping";
import { validateHyperframeHtmlContract } from "./staticGuard";
import { getHyperframeRuntimeScript } from "../generated/runtime-inline";
import { readDeclaredDefaults } from "../runtime/getVariables";
import { inlineSubCompositions } from "./inlineSubCompositions";
import { queryByAttr } from "../utils/cssSelector";
import { isSafePath, resolveWithinProject } from "../safePath.js";
import { HF_COLOR_GRADING_ATTR } from "../colorGrading";
const DEFAULT_RUNTIME_SCRIPT_URL = "";
function getRuntimeScriptUrl(): string {
const configured = (process.env.HYPERFRAME_RUNTIME_URL || "").trim();
return configured || DEFAULT_RUNTIME_SCRIPT_URL;
}
function injectInterceptor(html: string, runtimeMode: "inline" | "placeholder" = "inline"): string {
const sanitized = stripEmbeddedRuntimeScripts(html);
if (sanitized.includes(RUNTIME_BOOTSTRAP_ATTR)) return sanitized;
// Three modes for the runtime `;
} else if (runtimeMode === "placeholder") {
tag = ``;
} else {
const inlinedRuntime = getHyperframeRuntimeScript();
tag = ``;
}
if (sanitized.includes("")) {
// Use a function replacer so `String.prototype.replace`'s substitution
// patterns (`$&`, `$$`, `$'`, `` $` ``, `$1`–`$99`) inside the inlined
// runtime IIFE are passed through verbatim. The minified runtime
// contains the literal sequence `$&` as part of legitimate JS, and
// the older `(pattern, string)` form would expand it to the matched
// ``, silently corrupting the runtime and breaking every
// timeline in the bundle with a parse-time SyntaxError.
return sanitized.replace("", () => `${tag}\n`);
}
const htmlOpenMatch = sanitized.match(/]*>/i);
if (htmlOpenMatch?.index != null) {
const insertPos = htmlOpenMatch.index + htmlOpenMatch[0].length;
return `${sanitized.slice(0, insertPos)}
${tag}${sanitized.slice(insertPos)}`;
}
const doctypeIdx = sanitized.toLowerCase().indexOf("= 0) {
const insertPos = sanitized.indexOf(">", doctypeIdx) + 1;
return sanitized.slice(0, insertPos) + tag + sanitized.slice(insertPos);
}
return tag + sanitized;
}
function isRelativeUrl(url: string): boolean {
return !isNonRelativeUrl(url) && !isAbsolute(url);
}
function safeReadFile(filePath: string): string | null {
if (!existsSync(filePath)) return null;
try {
return readFileSync(filePath, "utf-8");
} catch {
return null;
}
}
const CSS_IMPORT_RE =
/@import\s+(?:url\(\s*(["']?)([^)"']+)\1\s*\)|(["'])([^"']+)\3)\s*([^;]*);\s*/g;
const CSS_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
function withCommentsStripped(
css: string,
fn: (stripped: string) => T,
): { result: T; restore: (s: string) => string } {
const comments: string[] = [];
const stripped = css.replace(CSS_COMMENT_RE, (m) => {
const idx = comments.length;
comments.push(m);
return `/*__hf_c${idx}__*/`;
});
const result = fn(stripped);
const restore = (s: string) => {
let out = s;
for (let i = 0; i < comments.length; i++) {
out = out.replace(`/*__hf_c${i}__*/`, comments[i]!);
}
return out;
};
return { result, restore };
}
function rebaseCssUrls(css: string, cssFileDir: string, projectDir: string): string {
const resolvedRoot = resolve(projectDir);
const resolvedDir = resolve(cssFileDir);
if (resolvedDir === resolvedRoot) return css;
return css.replace(CSS_URL_RE, (full, quote: string, urlValue: string) => {
if (!urlValue || !isRelativeUrl(urlValue)) return full;
const { basePath, suffix } = splitUrlSuffix(urlValue.trim());
if (!basePath) return full;
const absolutePath = resolve(resolvedDir, basePath);
const rebased = relative(resolvedRoot, absolutePath).split(sep).join("/");
if (rebased === basePath) return full;
return `url(${quote || ""}${rebased}${suffix}${quote || ""})`;
});
}
function rebaseRelativePath(urlValue: string, fromDir: string, toDir: string): string {
const { basePath, suffix } = splitUrlSuffix(urlValue.trim());
if (!basePath) return urlValue;
const absolutePath = resolve(fromDir, basePath);
const rebased = relative(resolve(toDir), absolutePath).split(sep).join("/");
return appendSuffixToUrl(rebased, suffix);
}
function rebaseSrcsetPaths(srcsetValue: string, fromDir: string, toDir: string): string {
if (!srcsetValue) return srcsetValue;
return srcsetValue
.split(",")
.map((rawCandidate) => {
const candidate = rawCandidate.trim();
if (!candidate) return candidate;
const parts = candidate.split(/\s+/);
const first = parts[0] ?? "";
if (parts.length === 0 || !isRelativeUrl(first)) return candidate;
parts[0] = rebaseRelativePath(first, fromDir, toDir);
return parts.join(" ");
})
.join(", ");
}
function rebaseColorGradingLutPath(value: string, fromDir: string, toDir: string): string {
if (!value.trim().startsWith("{")) return value;
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
return value;
}
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return value;
const lut = Reflect.get(parsed, "lut");
if (typeof lut === "string") {
if (!isRelativeUrl(lut)) return value;
Reflect.set(parsed, "lut", rebaseRelativePath(lut, fromDir, toDir));
return JSON.stringify(parsed);
}
if (typeof lut !== "object" || lut === null || Array.isArray(lut)) return value;
const lutSrc = Reflect.get(lut, "src");
if (typeof lutSrc !== "string" || !isRelativeUrl(lutSrc)) return value;
Reflect.set(lut, "src", rebaseRelativePath(lutSrc, fromDir, toDir));
return JSON.stringify(parsed);
}
function rebaseEntryAuthoredAssetPaths(
document: Document,
sourceDir: string,
projectDir: string,
): void {
for (const styleEl of [...document.querySelectorAll("style")]) {
styleEl.textContent = rebaseCssUrls(styleEl.textContent || "", sourceDir, projectDir);
}
for (const el of [...document.querySelectorAll("[style]")]) {
const styleAttr = el.getAttribute("style");
if (styleAttr) el.setAttribute("style", rebaseCssUrls(styleAttr, sourceDir, projectDir));
}
for (const el of [...document.querySelectorAll("[src], [href], [poster], [xlink\\:href]")]) {
if (el.tagName === "LINK" && (el.getAttribute("rel") || "").toLowerCase() === "stylesheet")
continue;
if (el.tagName === "SCRIPT" && el.hasAttribute("src")) continue;
for (const attr of ["src", "href", "poster", "xlink:href"] as const) {
const value = el.getAttribute(attr);
if (!value || !isRelativeUrl(value)) continue;
el.setAttribute(attr, rebaseRelativePath(value, sourceDir, projectDir));
}
}
for (const el of [...document.querySelectorAll("[srcset]")]) {
const srcset = el.getAttribute("srcset");
if (srcset) el.setAttribute("srcset", rebaseSrcsetPaths(srcset, sourceDir, projectDir));
}
for (const el of [...document.querySelectorAll(`[${HF_COLOR_GRADING_ATTR}]`)]) {
const value = el.getAttribute(HF_COLOR_GRADING_ATTR);
if (value)
el.setAttribute(
HF_COLOR_GRADING_ATTR,
rebaseColorGradingLutPath(value, sourceDir, projectDir),
);
}
}
function inlineCssFile(
css: string,
cssFileDir: string,
projectDir: string,
visited: Set = new Set(),
): string {
const { result: strippedCss, restore: restoreComments } = withCommentsStripped(css, (s) => s);
const importPlaceholders: string[] = [];
const withPlaceholders = strippedCss.replace(
CSS_IMPORT_RE,
(full, _q1, urlPath, _q2, barePath, mediaQuery) => {
const importPath = urlPath ?? barePath;
if (!importPath || !isRelativeUrl(importPath)) return full;
const resolved = resolve(cssFileDir, importPath);
// @import is resolved relative to the CSS file, but must stay within the
// project root; isSafePath also blocks symlink escapes (content is inlined).
if (!isSafePath(projectDir, resolved)) return full;
if (visited.has(resolved)) return "";
const content = safeReadFile(resolved);
if (content == null) return full;
visited.add(resolved);
const inlined = inlineCssFile(content, dirname(resolved), projectDir, visited);
const trimmedMedia = (mediaQuery || "").trim();
const block = trimmedMedia ? `@media ${trimmedMedia} {\n${inlined}\n}\n` : inlined + "\n";
const idx = importPlaceholders.length;
importPlaceholders.push(block);
return `/*__hf_import_${idx}__*/`;
},
);
let rebased = rebaseCssUrls(withPlaceholders, cssFileDir, projectDir);
rebased = restoreComments(rebased);
for (let i = 0; i < importPlaceholders.length; i++) {
rebased = rebased.replace(`/*__hf_import_${i}__*/`, importPlaceholders[i]!);
}
return rebased;
}
function safeReadFileBuffer(filePath: string): Buffer | null {
if (!existsSync(filePath)) return null;
try {
return readFileSync(filePath);
} catch {
return null;
}
}
function splitUrlSuffix(urlValue: string): { basePath: string; suffix: string } {
const queryIdx = urlValue.indexOf("?");
const hashIdx = urlValue.indexOf("#");
if (queryIdx < 0 && hashIdx < 0) return { basePath: urlValue, suffix: "" };
const cutIdx = queryIdx < 0 ? hashIdx : hashIdx < 0 ? queryIdx : Math.min(queryIdx, hashIdx);
return { basePath: urlValue.slice(0, cutIdx), suffix: urlValue.slice(cutIdx) };
}
function appendSuffixToUrl(baseUrl: string, suffix: string): string {
if (!suffix) return baseUrl;
if (suffix.startsWith("#")) return `${baseUrl}${suffix}`;
if (suffix.startsWith("?")) {
const queryWithOptionalHash = suffix.slice(1);
if (!queryWithOptionalHash) return baseUrl;
const hashIdx = queryWithOptionalHash.indexOf("#");
const queryPart =
hashIdx >= 0 ? queryWithOptionalHash.slice(0, hashIdx) : queryWithOptionalHash;
const hashPart = hashIdx >= 0 ? queryWithOptionalHash.slice(hashIdx) : "";
if (!queryPart) return `${baseUrl}${hashPart}`;
const joiner = baseUrl.includes("?") ? "&" : "?";
return `${baseUrl}${joiner}${queryPart}${hashPart}`;
}
return baseUrl;
}
const INLINE_MIME: Record = {
".svg": "image/svg+xml",
".json": "application/json",
".txt": "text/plain",
".cube": "text/plain",
".xml": "application/xml",
};
function maybeInlineRelativeAssetUrl(urlValue: string, projectDir: string): string | null {
if (!urlValue || !isRelativeUrl(urlValue)) return null;
const { basePath, suffix } = splitUrlSuffix(urlValue.trim());
if (!basePath) return null;
const filePath = resolveWithinProject(projectDir, basePath);
if (!filePath) return null;
const ext = filePath.toLowerCase().match(/\.[^.]+$/)?.[0] ?? "";
const mimeType = INLINE_MIME[ext];
if (!mimeType) return null;
const content = safeReadFileBuffer(filePath);
if (content == null) return null;
const dataUrl = `data:${mimeType};base64,${content.toString("base64")}`;
return appendSuffixToUrl(dataUrl, suffix);
}
function warnColorGradingLutNotInlined(lutSrc: string): void {
const trimmed = lutSrc.trim();
if (!isRelativeUrl(trimmed)) return;
console.warn(
`[HyperFrames] Could not inline color grading LUT "${trimmed}". The rendered bundle may not be self-contained.`,
);
}
// fallow-ignore-next-line complexity
function rewriteColorGradingLutWithInlinedAssets(value: string, projectDir: string): string {
if (!value.trim().startsWith("{")) return value;
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
return value;
}
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return value;
const lut = Reflect.get(parsed, "lut");
if (typeof lut === "string") {
const inlined = maybeInlineRelativeAssetUrl(lut, projectDir);
if (!inlined) {
warnColorGradingLutNotInlined(lut);
return value;
}
Reflect.set(parsed, "lut", inlined);
return JSON.stringify(parsed);
}
if (typeof lut !== "object" || lut === null || Array.isArray(lut)) return value;
const lutSrc = Reflect.get(lut, "src");
if (typeof lutSrc !== "string") return value;
const inlined = maybeInlineRelativeAssetUrl(lutSrc, projectDir);
if (!inlined) {
warnColorGradingLutNotInlined(lutSrc);
return value;
}
Reflect.set(lut, "src", inlined);
return JSON.stringify(parsed);
}
function rewriteSrcsetWithInlinedAssets(srcsetValue: string, projectDir: string): string {
if (!srcsetValue) return srcsetValue;
return srcsetValue
.split(",")
.map((rawCandidate) => {
const candidate = rawCandidate.trim();
if (!candidate) return candidate;
const parts = candidate.split(/\s+/);
if (parts.length === 0) return candidate;
const maybeInlined = maybeInlineRelativeAssetUrl(parts[0] ?? "", projectDir);
if (maybeInlined) parts[0] = maybeInlined;
return parts.join(" ");
})
.join(", ");
}
function rewriteCssUrlsWithInlinedAssets(cssText: string, projectDir: string): string {
if (!cssText) return cssText;
return cssText.replace(
/\burl\(\s*(["']?)([^)"']+)\1\s*\)/g,
(_full, quote: string, rawUrl: string) => {
const maybeInlined = maybeInlineRelativeAssetUrl((rawUrl || "").trim(), projectDir);
if (!maybeInlined) return _full;
return `url(${quote || ""}${maybeInlined}${quote || ""})`;
},
);
}
function cssAttributeSelector(attr: string, value: string): string {
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
return `[${attr}="${escaped}"]`;
}
function uniqueCompositionId(baseId: string, index: number): string {
return `${baseId}__hf${index}`;
}
export type BundledHostCompositionIdentity = {
authoredCompositionId: string | null;
runtimeCompositionId: string | null;
};
function getBundledHostCompositionIdentity(host: Element): BundledHostCompositionIdentity {
const currentCompositionId = (host.getAttribute("data-composition-id") || "").trim() || null;
const authoredCompositionId =
(host.getAttribute("data-hf-original-composition-id") || currentCompositionId || "").trim() ||
null;
return {
authoredCompositionId,
runtimeCompositionId: currentCompositionId,
};
}
function getBundledTrackedCompositionHosts(document: Document): Element[] {
const hosts = Array.from(
document.querySelectorAll("[data-composition-src], [data-composition-id]"),
);
return hosts.filter((host) => {
if (host.hasAttribute("data-composition-src")) return true;
const authoredCompositionId = getBundledHostCompositionIdentity(host).authoredCompositionId;
if (!authoredCompositionId) return false;
return !!document.getElementById(`${authoredCompositionId}-template`);
});
}
function shouldAssignBundledRuntimeCompositionId(host: Element, document: Document): boolean {
if (host.hasAttribute("data-composition-src")) return true;
const authoredCompositionId = getBundledHostCompositionIdentity(host).authoredCompositionId;
if (!authoredCompositionId) return false;
if (!document.getElementById(`${authoredCompositionId}-template`)) return false;
return host.children.length === 0;
}
function countBundledAuthoredCompositionIds(hosts: Element[]): Map {
const counts = new Map();
for (const host of hosts) {
const authoredCompositionId = getBundledHostCompositionIdentity(host).authoredCompositionId;
if (!authoredCompositionId) continue;
counts.set(authoredCompositionId, (counts.get(authoredCompositionId) || 0) + 1);
}
return counts;
}
// fallow-ignore-next-line complexity
export function assignBundledRuntimeCompositionIds(
hosts: Element[],
counts: Map = countBundledAuthoredCompositionIds(hosts),
): Map {
const instanceByCompositionId = new Map();
const identities = new Map();
for (const host of hosts) {
const { authoredCompositionId, runtimeCompositionId: previousRuntimeCompositionId } =
getBundledHostCompositionIdentity(host);
const shouldAssign = shouldAssignBundledRuntimeCompositionId(host, host.ownerDocument);
if (!authoredCompositionId) {
identities.set(host, {
authoredCompositionId: null,
runtimeCompositionId: previousRuntimeCompositionId,
});
continue;
}
const duplicateInstance = (counts.get(authoredCompositionId) || 0) > 1;
let runtimeCompositionId = previousRuntimeCompositionId || authoredCompositionId;
if (shouldAssign) {
const instanceIndex = duplicateInstance
? (instanceByCompositionId.get(authoredCompositionId) || 0) + 1
: 0;
if (duplicateInstance) {
instanceByCompositionId.set(authoredCompositionId, instanceIndex);
host.setAttribute("data-hf-original-composition-id", authoredCompositionId);
} else {
host.removeAttribute("data-hf-original-composition-id");
}
runtimeCompositionId = duplicateInstance
? uniqueCompositionId(authoredCompositionId, instanceIndex)
: authoredCompositionId;
host.setAttribute("data-composition-id", runtimeCompositionId);
}
identities.set(host, {
authoredCompositionId,
runtimeCompositionId,
});
}
return identities;
}
export function prepareFlattenedInnerRoot(innerRoot: Element): Element {
const prepared = innerRoot.cloneNode(true) as Element;
markFlattenedInnerRoot(prepared);
const w = prepared.getAttribute("data-width");
const h = prepared.getAttribute("data-height");
const widthVal = w ? `${w}px` : "100%";
const heightVal = h ? `${h}px` : "100%";
const existingStyle = (prepared.getAttribute("style") || "").trim();
const fill = `width:${widthVal};height:${heightVal}`;
prepared.setAttribute("style", existingStyle ? `${existingStyle};${fill}` : fill);
return prepared;
}
function enforceCompositionPixelSizing(document: Document): void {
const compositionEls = [
...document.querySelectorAll("[data-composition-id][data-width][data-height]"),
];
if (compositionEls.length === 0) return;
const sizeMap = new Map();
for (const el of compositionEls) {
const compId = el.getAttribute("data-composition-id");
const w = Number(el.getAttribute("data-width"));
const h = Number(el.getAttribute("data-height"));
if (compId && Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
sizeMap.set(compId, { w, h });
}
}
if (sizeMap.size === 0) return;
for (const styleEl of document.querySelectorAll("style")) {
let css = styleEl.textContent || "";
let modified = false;
for (const [compId, { w, h }] of sizeMap) {
const escaped = compId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const blockRe = new RegExp(
`(\\[data-composition-id=["']${escaped}["']\\]\\s*\\{)([^}]*)(})`,
"g",
);
css = css.replace(blockRe, (_, open, body, close) => {
const newBody = body
.replace(/(\bwidth\s*:\s*)100%/g, `$1${w}px`)
.replace(/(\bheight\s*:\s*)100%/g, `$1${h}px`);
if (newBody !== body) modified = true;
return open + newBody + close;
});
}
if (modified) styleEl.textContent = css;
}
}
function autoHealMissingCompositionIds(document: Document): void {
const compositionIdRe = /data-composition-id=["']([^"']+)["']/gi;
const referencedIds = new Set();
for (const el of document.querySelectorAll("style, script")) {
const text = (el.textContent || "").trim();
if (!text) continue;
let match: RegExpExecArray | null;
while ((match = compositionIdRe.exec(text)) !== null) {
const compId = (match[1] || "").trim();
if (compId) referencedIds.add(compId);
}
}
if (referencedIds.size === 0) return;
const existingIds = new Set();
for (const el of document.querySelectorAll("[data-composition-id]")) {
const id = (el.getAttribute("data-composition-id") || "").trim();
if (id) existingIds.add(id);
}
for (const compId of referencedIds) {
if (compId === "root" || existingIds.has(compId)) continue;
const candidates = [`${compId}-layer`, `${compId}-comp`, compId];
for (const targetId of candidates) {
const found = document.getElementById(targetId);
if (found && !found.getAttribute("data-composition-id")) {
found.setAttribute("data-composition-id", compId);
break;
}
}
}
}
function coalesceHeadStylesAndBodyScripts(document: Document): void {
const headStyleEls = [...document.querySelectorAll("head style")];
if (headStyleEls.length > 1) {
const imports: string[] = [];
const cssParts: string[] = [];
const seenImports = new Set();
for (const el of headStyleEls) {
const raw = (el.textContent || "").trim();
if (!raw) continue;
const nonImportCss = raw.replace(CSS_IMPORT_RE, (match) => {
const cleaned = match.trim();
if (!seenImports.has(cleaned)) {
seenImports.add(cleaned);
imports.push(cleaned);
}
return "";
});
const trimmed = nonImportCss.trim();
if (trimmed) cssParts.push(trimmed);
}
const merged = [...imports, ...cssParts].join("\n\n").trim();
if (merged) {
headStyleEls[0]!.textContent = merged;
for (let i = 1; i < headStyleEls.length; i++) headStyleEls[i]!.remove();
}
}
const bodyInlineScripts = [...document.querySelectorAll("body script")].filter((el) => {
if (el.hasAttribute(RUNTIME_BOOTSTRAP_ATTR) || el.hasAttribute("src")) return false;
const type = (el.getAttribute("type") || "").trim().toLowerCase();
return !type || type === "text/javascript" || type === "application/javascript";
});
if (bodyInlineScripts.length > 0) {
const mergedJs = joinJsChunks(bodyInlineScripts.map((el) => el.textContent || ""));
for (const el of bodyInlineScripts) el.remove();
if (mergedJs) {
const stripped = stripJsCommentsParserSafe(mergedJs);
const inlineScript = document.createElement("script");
inlineScript.textContent = stripped;
document.body.appendChild(inlineScript);
}
}
}
/**
* Force subpixel glyph positioning so headless rendering paths
* (chrome-headless-shell with BeginFrame) lay text out identically to full
* Chrome. `text-rendering: auto` resolves to `optimizeSpeed` (integer glyph
* advances) in headless-shell but `geometricPrecision` in full Chrome, which
* shifts line-wrap points and any animation that reads measured text width.
* Mirrors the producer's `injectTextRenderingRule` so bundled previews and
* compiled renders stay byte-aligned. `*` has zero specificity, so authored
* class/id rules still override.
*/
function injectTextRenderingRule(document: Document): void {
const head = document.head;
if (!head) return;
if (document.querySelector("style[data-hyperframes-text-rendering]")) return;
const styleEl = document.createElement("style");
styleEl.setAttribute("data-hyperframes-text-rendering", "true");
styleEl.textContent = "html,body,*{text-rendering:geometricPrecision}";
head.insertBefore(styleEl, head.firstChild);
}
/**
* Concatenate JS chunks safely. Goals:
* - Each chunk's last statement is terminated, so joining can't introduce ASI
* surprises (e.g. `a()` followed by `(b)()` — the second chunk would parse
* as a call on the first's return value).
* - In the common case (chunk already ends with `;` — typical of esbuild
* output and IIFE-wrapped composition scripts ending in `})();`), the join
* produces clean output: chunks separated by `\n` with no stray bare
* semicolon lines.
* - Defensive against trailing line comments. If a chunk ends with `// ...`
* and we appended `;` on the same line, the appended semicolon would be
* swallowed by the comment, leaving the next chunk's first statement
* attached to the previous chunk's last expression — exactly the ASI
* hazard this helper exists to prevent. So when a chunk doesn't already
* end in `;`, we append `\n;` instead — the newline closes any line
* comment, and the standalone `;` becomes the statement separator.
*/
function joinJsChunks(chunks: string[]): string {
return chunks
.map((chunk) => chunk.trim())
.filter((chunk) => chunk.length > 0)
.map((chunk) => (chunk.endsWith(";") ? chunk : chunk + "\n;"))
.join("\n");
}
function stripJsCommentsParserSafe(source: string): string {
if (!source) return source;
try {
const result = transformSync(source, { loader: "js", minify: false, legalComments: "none" });
return result.code.trim();
} catch {
return source;
}
}
export interface BundleOptions {
/** Project-relative HTML entry to bundle. Defaults to `index.html`. */
entryFile?: string;
/** Optional media duration prober (e.g., ffprobe). If omitted, media durations are not resolved. */
probeMediaDuration?: MediaDurationProber;
/**
* How to handle the HyperFrames runtime ` so the caller can
* substitute it with a real URL via string replace. Used by the dev studio
* server and vite preview to point at a local runtime endpoint, which keeps
* the runtime cacheable across hot-reloads instead of re-inlining ~150 KB
* on every change.
*
* The `HYPERFRAME_RUNTIME_URL` env var, when set, takes precedence over both
* modes and emits `