diff --git a/packages/cli/src/server/studioServer.ts b/packages/cli/src/server/studioServer.ts index 5666ca379..b58207112 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 1c5b1c952..54cd0a6c4 100644 --- a/packages/core/src/studio-api/routes/preview.test.ts +++ b/packages/core/src/studio-api/routes/preview.test.ts @@ -143,6 +143,122 @@ 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("applies adapter preview transforms when bundle() returns null (reads from disk)", async () => { + const projectDir = createProjectDir(); + const app = new Hono(); + registerPreviewRoutes( + app, + createAdapter(projectDir, { + // bundle: async () => null <-- default; falls back to reading index.html from disk + 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 in the bundle error fallback path", async () => { + const projectDir = createProjectDir(); + const app = new Hono(); + registerPreviewRoutes( + app, + createAdapter(projectDir, { + bundle: async () => { + throw new Error("bundler unavailable"); + }, + 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("falls back to original HTML when transformPreviewHtml throws", async () => { + const projectDir = createProjectDir(); + const app = new Hono(); + registerPreviewRoutes( + app, + createAdapter(projectDir, { + bundle: async () => "Preview", + transformPreviewHtml: async () => { + throw new Error("transform failed"); + }, + }), + ); + + const response = await app.request("http://localhost/projects/demo/preview"); + const html = await response.text(); + + expect(response.status).toBe(200); + expect(html).toContain("Preview"); + }); + 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 447d080f1..a8cf5293d 100644 --- a/packages/core/src/studio-api/routes/preview.ts +++ b/packages/core/src/studio-api/routes/preview.ts @@ -124,6 +124,25 @@ function injectStudioPreviewAugmentations( ); } +async function transformPreviewHtml( + html: string, + adapter: StudioApiAdapter, + project: { id: string; dir: string; title?: string; sessionId?: string }, + activeCompositionPath: string, +): Promise