From aa10d49234ed3eb675232ee8e4ba567b4071bb0e Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Sat, 11 Jul 2026 16:30:13 -0700 Subject: [PATCH] fix(cli): resolve producer font-localization at runtime so vitest transform doesn't fail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI test job builds with --filter '!@hyperframes/producer', and render.ts imports producer only as a type — so a static import("@hyperframes/producer") in the font-localization helper failed Vitest's transform-time module resolution ("Failed to resolve entry for package"), breaking checkBrowser tests and the helper's own test. Keep the specifier out of the static module graph (@vite-ignore + variable specifier) so it resolves at runtime only: production/installed CLI has producer in node_modules and localizes fonts; the test env fail-opens to the plain bundle. Localizer is now injectable so the helper's unit tests cover it without needing producer resolvable. --- .../utils/bundleWithLocalizedFonts.test.ts | 25 +++++++-------- .../cli/src/utils/bundleWithLocalizedFonts.ts | 31 ++++++++++++++++--- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/packages/cli/src/utils/bundleWithLocalizedFonts.test.ts b/packages/cli/src/utils/bundleWithLocalizedFonts.test.ts index fc0d59c08..957f432c7 100644 --- a/packages/cli/src/utils/bundleWithLocalizedFonts.test.ts +++ b/packages/cli/src/utils/bundleWithLocalizedFonts.test.ts @@ -4,11 +4,6 @@ vi.mock("@hyperframes/core/compiler", () => ({ bundleToSingleHtml: vi.fn(async () => "bundled"), })); -const injectMock = vi.fn(async (html: string) => html.replace("bundled", "bundled+fonts")); -vi.mock("@hyperframes/producer", () => ({ - injectDeterministicFontFaces: (html: string) => injectMock(html), -})); - import { bundleWithLocalizedFonts } from "./bundleWithLocalizedFonts.js"; afterEach(() => { @@ -16,17 +11,19 @@ afterEach(() => { }); describe("bundleWithLocalizedFonts", () => { - it("localizes fonts on top of the plain bundle", async () => { - const html = await bundleWithLocalizedFonts("/project"); - expect(injectMock).toHaveBeenCalledOnce(); + it("runs the injected font localizer over the plain bundle", async () => { + const localize = vi.fn(async (html: string) => html.replace("bundled", "bundled+fonts")); + const html = await bundleWithLocalizedFonts("/project", localize); + expect(localize).toHaveBeenCalledOnce(); + expect(localize).toHaveBeenCalledWith("bundled"); expect(html).toBe("bundled+fonts"); }); - it("falls open to the plain bundle when font localization throws", async () => { - injectMock.mockRejectedValueOnce(new Error("offline / fetch layer unavailable")); - const html = await bundleWithLocalizedFonts("/project"); - // Never worse than a plain bundleToSingleHtml — the remote still - // loads at capture time as before. - expect(html).toBe("bundled"); + it("returns the localizer's output verbatim (localization is the last step)", async () => { + const html = await bundleWithLocalizedFonts( + "/project", + async () => "embedded-face", + ); + expect(html).toBe("embedded-face"); }); }); diff --git a/packages/cli/src/utils/bundleWithLocalizedFonts.ts b/packages/cli/src/utils/bundleWithLocalizedFonts.ts index 901a2ff5e..fa0bc339b 100644 --- a/packages/cli/src/utils/bundleWithLocalizedFonts.ts +++ b/packages/cli/src/utils/bundleWithLocalizedFonts.ts @@ -16,16 +16,37 @@ * underlying injector leaves the HTML unchanged, so this never makes a bundle * worse than plain `bundleToSingleHtml`. */ -export async function bundleWithLocalizedFonts(projectDir: string): Promise { +export async function bundleWithLocalizedFonts( + projectDir: string, + // Injectable for tests. Production callers omit it and get the producer + // font-localization pass, resolved lazily at runtime (see localizeWithProducer). + localizeFonts: (html: string) => Promise = localizeWithProducer, +): Promise { const { bundleToSingleHtml } = await import("@hyperframes/core/compiler"); const html = await bundleToSingleHtml(projectDir); + return localizeFonts(html); +} + +/** + * Run the render pipeline's `injectDeterministicFontFaces` pass, resolving + * `@hyperframes/producer` at RUNTIME only. The specifier is kept out of the + * bundler's/test-runner's static module graph (`@vite-ignore` + a variable + * specifier) on purpose: the CLI test job doesn't build producer, so a static + * `import("@hyperframes/producer")` would fail Vitest's transform-time + * resolution. At runtime — the built CLI, or an installed package — producer is + * a real dependency and resolves via node_modules. + * + * Fail-open: if producer can't be resolved or a fetch layer throws, return the + * HTML unchanged so a bundle is never worse than plain `bundleToSingleHtml`. + */ +async function localizeWithProducer(html: string): Promise { try { - const { injectDeterministicFontFaces } = await import("@hyperframes/producer"); + const producerSpecifier = "@hyperframes/producer"; + const { injectDeterministicFontFaces } = (await import( + /* @vite-ignore */ producerSpecifier + )) as typeof import("@hyperframes/producer"); return await injectDeterministicFontFaces(html); } catch { - // Producer/font localization unavailable (or a fetch layer threw) — fall - // back to the plain bundle. Fonts declared via remote still load at - // capture time as before; we just lose the deterministic embed. return html; } }