diff --git a/packages/cli/src/server/studioServer.ts b/packages/cli/src/server/studioServer.ts index 5666ca3799..b58207112d 100644 --- a/packages/cli/src/server/studioServer.ts +++ b/packages/cli/src/server/studioServer.ts @@ -209,6 +209,12 @@ export function createStudioServer(options: StudioServerOptions): StudioServer { } }, + async transformPreviewHtml({ html }) { + const { injectDeterministicFontFaces } = + await import("../../../producer/src/services/deterministicFonts.js"); + return injectDeterministicFontFaces(html); + }, + getProjectSignature(dir: string): string { if (resolve(dir) !== resolve(projectDir)) return createProjectSignature(dir); cachedProjectSignature ??= createProjectSignature(projectDir); diff --git a/packages/core/src/studio-api/routes/preview.test.ts b/packages/core/src/studio-api/routes/preview.test.ts index 1c5b1c9521..276d0b7c31 100644 --- a/packages/core/src/studio-api/routes/preview.test.ts +++ b/packages/core/src/studio-api/routes/preview.test.ts @@ -143,6 +143,56 @@ describe("registerPreviewRoutes", () => { expect(html).toContain("compositions/scene.html"); }); + it("applies adapter preview transforms to bundled root previews", async () => { + const projectDir = createProjectDir(); + const app = new Hono(); + registerPreviewRoutes( + app, + createAdapter(projectDir, { + bundle: async () => "Preview", + transformPreviewHtml: async ({ html, activeCompositionPath }) => + html.replace( + "", + ``, + ), + }), + ); + + const response = await app.request("http://localhost/projects/demo/preview"); + const html = await response.text(); + + expect(response.status).toBe(200); + expect(html).toContain(''); + }); + + it("applies adapter preview transforms to sub-composition previews", async () => { + const projectDir = createProjectDir(); + mkdirSync(join(projectDir, "compositions"), { recursive: true }); + writeFileSync( + join(projectDir, "compositions/scene.html"), + ``, + ); + const app = new Hono(); + registerPreviewRoutes( + app, + createAdapter(projectDir, { + transformPreviewHtml: async ({ html, activeCompositionPath }) => + html.replace( + "", + ``, + ), + }), + ); + + const response = await app.request( + "http://localhost/projects/demo/preview/comp/compositions/scene.html", + ); + const html = await response.text(); + + expect(response.status).toBe(200); + expect(html).toContain(''); + }); + it("uses the adapter project signature when available", async () => { const projectDir = createProjectDir(); const getProjectSignature = vi.fn(() => "cached-signature"); diff --git a/packages/core/src/studio-api/routes/preview.ts b/packages/core/src/studio-api/routes/preview.ts index 447d080f18..d65348484b 100644 --- a/packages/core/src/studio-api/routes/preview.ts +++ b/packages/core/src/studio-api/routes/preview.ts @@ -124,6 +124,20 @@ function injectStudioPreviewAugmentations( ); } +async function transformPreviewHtml( + html: string, + adapter: StudioApiAdapter, + project: { id: string; dir: string; title?: string; sessionId?: string }, + activeCompositionPath: string, +): Promise { + if (!adapter.transformPreviewHtml) return html; + return adapter.transformPreviewHtml({ + html, + project, + activeCompositionPath, + }); +} + export function registerPreviewRoutes(api: Hono, adapter: StudioApiAdapter): void { const previewCacheHeaders = (etag: string) => ({ "Cache-Control": "private, no-cache", @@ -167,14 +181,19 @@ export function registerPreviewRoutes(api: Hono, adapter: StudioApiAdapter): voi bundled = bundled.replace(//i, ``); } - bundled = injectStudioPreviewAugmentations(bundled, adapter, project.dir, "index.html"); + bundled = injectStudioPreviewAugmentations( + await transformPreviewHtml(bundled, adapter, project, "index.html"), + adapter, + project.dir, + "index.html", + ); return c.html(bundled, 200, previewCacheHeaders(etag)); } catch { const file = resolve(project.dir, "index.html"); if (existsSync(file)) { return c.html( injectStudioPreviewAugmentations( - readFileSync(file, "utf-8"), + await transformPreviewHtml(readFileSync(file, "utf-8"), adapter, project, "index.html"), adapter, project.dir, "index.html", @@ -214,6 +233,7 @@ export function registerPreviewRoutes(api: Hono, adapter: StudioApiAdapter): voi const baseHref = `/api/projects/${project.id}/preview/`; let html = buildSubCompositionHtml(project.dir, compPath, adapter.runtimeUrl, baseHref); if (!html) return c.text("not found", 404); + html = await transformPreviewHtml(html, adapter, project, compPath); return c.html( injectStudioPreviewAugmentations(html, adapter, project.dir, compPath), 200, diff --git a/packages/core/src/studio-api/types.ts b/packages/core/src/studio-api/types.ts index 0f66a5764f..40e1c4460f 100644 --- a/packages/core/src/studio-api/types.ts +++ b/packages/core/src/studio-api/types.ts @@ -52,6 +52,16 @@ export interface StudioApiAdapter { /** URL to the hyperframe runtime JS (injected into preview HTML). */ runtimeUrl: string; + /** + * Optional: post-process preview HTML before Studio augments it. + * Useful when preview must mirror render-time compilation steps. + */ + transformPreviewHtml?: (opts: { + html: string; + project: ResolvedProject; + activeCompositionPath: string; + }) => Promise | string; + /** Directory where render output files are stored. */ rendersDir(project: ResolvedProject): string; diff --git a/packages/studio/vite.adapter.ts b/packages/studio/vite.adapter.ts index 7babcf916e..10820a9f58 100644 --- a/packages/studio/vite.adapter.ts +++ b/packages/studio/vite.adapter.ts @@ -154,6 +154,11 @@ export function createViteAdapter(dataDir: string, server: ViteDevServer): Studi return html; }, + async transformPreviewHtml({ html }) { + const producer = await import("../producer/src/services/deterministicFonts.js"); + return producer.injectDeterministicFontFaces(html); + }, + getProjectSignature(projectDir: string): string { const cacheKey = resolve(projectDir); const cached = projectSignatureCache.get(cacheKey);