(
projectDir: string,
htmlPath: string,
downloadDir: string,
options: CompileForRenderOptions = {},
)
| 1499 | */ |
| 1500 | // fallow-ignore-next-line complexity |
| 1501 | export async function compileForRender( |
| 1502 | projectDir: string, |
| 1503 | htmlPath: string, |
| 1504 | downloadDir: string, |
| 1505 | options: CompileForRenderOptions = {}, |
| 1506 | ): Promise<CompiledComposition> { |
| 1507 | const rawHtml = rewriteUnresolvableGsapToCdn(readFileSync(htmlPath, "utf-8"), projectDir); |
| 1508 | |
| 1509 | // Pre-flight: every data-composition-src reference must resolve to a |
| 1510 | // usable file before we spend any time compiling, launching a browser, or |
| 1511 | // waiting out a capture timeout. See EmptyCompositionError for why this is |
| 1512 | // unconditional (not gated behind --strict like lint warnings) — a render |
| 1513 | // that silently drops a scene is strictly worse than one that refuses to |
| 1514 | // start. |
| 1515 | assertSubCompositionsUsable(rawHtml, projectDir); |
| 1516 | |
| 1517 | const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile( |
| 1518 | rawHtml, |
| 1519 | projectDir, |
| 1520 | downloadDir, |
| 1521 | options.log, |
| 1522 | ); |
| 1523 | |
| 1524 | // Parse sub-compositions first (extracts media + compiled HTML for each) |
| 1525 | const { |
| 1526 | videos: subVideos, |
| 1527 | audios: subAudios, |
| 1528 | images: subImages, |
| 1529 | subCompositions, |
| 1530 | } = await parseSubCompositions(compiledHtml, projectDir, downloadDir); |
| 1531 | |
| 1532 | // Ensure the HTML is a full document before inlining sub-compositions. |
| 1533 | // When index.html is a fragment (no <html>/<head>/<body>), linkedom.parseHTML() |
| 1534 | // returns a document with null head/body, which causes inlineSubCompositions to |
| 1535 | // silently discard all collected composition styles and scripts. |
| 1536 | const fullHtml = ensureFullDocument(compiledHtml); |
| 1537 | |
| 1538 | // Inline sub-compositions into the main HTML so the runtime takes the same |
| 1539 | // synchronous code path as the bundled preview (no async fetch of |
| 1540 | // data-composition-src). This mirrors what htmlBundler.ts does for preview. |
| 1541 | const inlinedHtml = inlineSubCompositions(fullHtml, subCompositions, projectDir); |
| 1542 | |
| 1543 | // Strip preload="none" from media elements — the renderer needs to load all |
| 1544 | // media upfront for frame capture. Users add this to reduce browser memory in |
| 1545 | // preview, but it causes the headless renderer to never load the media, leading |
| 1546 | // to 45s timeout failures. |
| 1547 | const sanitizedHtml = inlinedHtml.replace( |
| 1548 | /(<(?:video|audio)\b[^>]*?)\s+preload\s*=\s*["']none["']/gi, |
| 1549 | "$1", |
| 1550 | ); |
| 1551 | const renderModeHints = detectRenderModeHints(sanitizedHtml); |
| 1552 | const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml); |
| 1553 | |
| 1554 | const normalizedFontHtml = normalizeSystemFontPrimaryFamilies( |
| 1555 | injectTextRenderingRule( |
| 1556 | coalesceHeadStylesAndBodyScripts(promoteCssImportsToLinkTags(sanitizedHtml)), |
| 1557 | ), |
| 1558 | ); |
no test coverage detected