mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(preview): rewrite sub-composition asset urls in styles (#174)
## Summary
- rewrite CSS `url(...)` asset paths from sub-compositions before styles are hoisted into bundled/master preview output
- rewrite standalone sub-composition preview HTML so `src`/`href` paths keep resolving correctly under the preview root `<base>`
- add regression tests for bundled CSS asset rewriting and standalone sub-composition preview rewriting
## Root cause
Standalone composition previews reused the project `<head>` with a preview-root `<base href="/api/projects/:id/preview/">`, but the sub-composition body still contained `../...` asset references. Those escaped the preview route and 404ed. Separately, bundled preview already rewrote `<img src="../...">` paths but left hoisted CSS asset references like `@font-face src: url("../font.woff2")` untouched.
## Validation
- `pnpm --filter @hyperframes/core exec vitest run src/compiler/htmlBundler.test.ts src/studio-api/helpers/subComposition.test.ts`
- `pnpm --filter @hyperframes/core exec tsc --noEmit`
- `pnpm --filter @hyperframes/producer exec tsc --noEmit` *(blocked by pre-existing `packages/producer/src/services/deterministicFonts.ts` importing missing generated file `./fontData.generated.js` in the worktree install)*
- browser verification with `agent-browser` against the local preview server using `/Users/miguel07code/dev/test-hyperframes/heygen-promo`
## Browser proof
Verified the fixed preview routes in-browser after seeking to visible frames:
- standalone composition preview
- bundled master preview
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
// @vitest-environment node
|
||||
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildSubCompositionHtml } from "./subComposition";
|
||||
|
||||
function makeTempProject(files: Record<string, string>): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-subcomp-preview-"));
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
const full = join(dir, rel);
|
||||
mkdirSync(join(full, ".."), { recursive: true });
|
||||
writeFileSync(full, content, "utf-8");
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
describe("buildSubCompositionHtml", () => {
|
||||
it("rewrites sub-composition asset paths against the project root preview base", () => {
|
||||
const dir = makeTempProject({
|
||||
"index.html": `<!doctype html>
|
||||
<html><head><title>Test</title></head><body></body></html>`,
|
||||
"compositions/hero.html": `<template id="hero-template">
|
||||
<div data-composition-id="hero" data-width="1920" data-height="1080">
|
||||
<img src="../logo.png" alt="Logo" />
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Brand Sans";
|
||||
src: url("../fonts/brand.woff2") format("woff2");
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
</template>`,
|
||||
});
|
||||
|
||||
const html = buildSubCompositionHtml(
|
||||
dir,
|
||||
"compositions/hero.html",
|
||||
"/api/runtime.js",
|
||||
"/api/projects/demo/preview/",
|
||||
);
|
||||
|
||||
expect(html).toContain('<base href="/api/projects/demo/preview/">');
|
||||
expect(html).toContain('src="logo.png"');
|
||||
expect(html).toContain('url("fonts/brand.woff2")');
|
||||
expect(html).not.toContain('src="../logo.png"');
|
||||
expect(html).not.toContain('url("../fonts/brand.woff2")');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import * as cheerio from "cheerio";
|
||||
import { rewriteAssetPaths, rewriteCssAssetUrls } from "../../compiler/rewriteSubCompPaths.js";
|
||||
|
||||
/**
|
||||
* Build a standalone HTML page for a sub-composition.
|
||||
@@ -22,6 +24,21 @@ export function buildSubCompositionHtml(
|
||||
// Extract content from <template> wrapper (compositions are always templates)
|
||||
const templateMatch = rawComp.match(/<template[^>]*>([\s\S]*)<\/template>/i);
|
||||
const content = templateMatch?.[1] ?? rawComp;
|
||||
const $content = cheerio.load(content, {}, false);
|
||||
|
||||
rewriteAssetPaths(
|
||||
$content("[src], [href]").toArray(),
|
||||
compPath,
|
||||
(el, attr) => $content(el).attr(attr),
|
||||
(el, attr, value) => {
|
||||
$content(el).attr(attr, value);
|
||||
},
|
||||
);
|
||||
$content("style").each((_, styleEl) => {
|
||||
$content(styleEl).html(rewriteCssAssetUrls($content(styleEl).html() || "", compPath));
|
||||
});
|
||||
|
||||
const rewrittenContent = $content.root().html() || content;
|
||||
|
||||
// Use the project's index.html <head> to preserve all dependencies
|
||||
const indexPath = join(projectDir, "index.html");
|
||||
@@ -58,7 +75,7 @@ ${headContent}
|
||||
</head>
|
||||
<body>
|
||||
<script>window.__timelines=window.__timelines||{};</script>
|
||||
${content}
|
||||
${rewrittenContent}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user