diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc index c94e4cb47b..f4d3feabc5 100644 --- a/.fallowrc.jsonc +++ b/.fallowrc.jsonc @@ -18,7 +18,7 @@ "packages/producer/src/perf-gate.ts", "packages/producer/src/runtime-conformance.ts", "packages/producer/src/benchmark.ts", - "packages/producer/scripts/generate-font-data.ts", + "packages/core/scripts/generate-font-data.ts", "packages/producer/we-render.mjs", "packages/producer/scripts/validate-fast-video.ts", "packages/cli/scripts/generate-font-data.ts", @@ -494,6 +494,16 @@ // new package makes fallow see it as a fresh finding; the underlying clone // predates this refactor. "packages/parsers/src/hfIds.ts", + // Font pipeline: pre-existing duplication moved from packages/producer/src/services/ + // and producer/scripts/ to core/src/fonts/ and core/scripts/ (the generator mirrors + // CANONICAL_FONTS; the tests share arrange/act scaffolds). + "packages/core/scripts/generate-font-data.ts", + "packages/core/src/fonts/deterministicFonts-aliasSupplement.test.ts", + "packages/core/src/fonts/deterministicFonts-authoredResources.test.ts", + "packages/core/src/fonts/deterministicFonts-failClosed.test.ts", + "packages/core/src/fonts/deterministicFonts-googleSubsets.test.ts", + "packages/core/src/fonts/deterministicFonts-retries.test.ts", + "packages/core/src/fonts/fontCompression.test.ts", // Parser test files: parallel arrange/act/assert test cases — pre-existing // duplication moved from packages/core/src/parsers/. "packages/parsers/src/gsapParser.test.ts", @@ -733,6 +743,9 @@ // complexity pre-dates the computed-timeline work. Exempted at file level // rather than refactored as scope creep. "ignore": [ + // generate-font-data.ts: the build-time font data generator moved from producer/scripts; + // its CRAP score is coverage-driven (a build script no unit test runs). + "packages/core/scripts/generate-font-data.ts", // audio-fx-runtime-entry.ts: the browser-side IIFE entry for the offline FX // render. It runs only inside the headless page the engine drives, so unit // coverage cannot reach it and its CRAP score is coverage-driven rather diff --git a/.gitignore b/.gitignore index a400a99105..0a72e3b30c 100644 --- a/.gitignore +++ b/.gitignore @@ -75,7 +75,7 @@ tmp/ packages/core/src/generated/* !packages/core/src/generated/position-edits-render-inline.ts !packages/core/src/generated/position-edits-render-inline.test.ts -packages/producer/src/services/fontData.generated.ts +packages/core/src/fonts/fontData.generated.ts # Plan documents (working artifacts, not committed) docs/plans/ diff --git a/Dockerfile.test b/Dockerfile.test index e85da3f21e..78f14544c6 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -122,9 +122,6 @@ RUN bun run --filter @hyperframes/core build:hyperframes-runtime:modular COPY packages/engine/ packages/engine/ COPY packages/producer/ packages/producer/ -# Generate embedded font data (deterministicFonts.ts imports this at runtime) -RUN cd packages/producer && bunx tsx scripts/generate-font-data.ts - WORKDIR /app/packages/producer # Skip fixtures tagged `transparency` (Chrome alpha-channel PSNR quirks not diff --git a/bun.lock b/bun.lock index a8d7af76cf..4fccbde3cc 100644 --- a/bun.lock +++ b/bun.lock @@ -24,7 +24,7 @@ }, "packages/aws-lambda": { "name": "@hyperframes/aws-lambda", - "version": "0.8.119", + "version": "0.8.126", "dependencies": { "@aws-sdk/client-s3": "^3.700.0", "@aws-sdk/client-sfn": "^3.700.0", @@ -57,7 +57,7 @@ }, "packages/cli": { "name": "@hyperframes/cli", - "version": "0.8.119", + "version": "0.8.126", "bin": { "hyperframes": "./bin/hyperframes.mjs", "hyperframes-localize-fonts": "./bin/hyperframes-localize-fonts.mjs", @@ -109,7 +109,7 @@ }, "packages/core": { "name": "@hyperframes/core", - "version": "0.8.119", + "version": "0.8.126", "dependencies": { "@chenglou/pretext": "^0.0.5", "@hyperframes/lint": "workspace:*", @@ -118,8 +118,27 @@ "bpm-detective": "^2.0.5", "linkedom": "^0.18.12", "postcss": "^8.5.8", + "wawoff2": "^2.0.1", }, "devDependencies": { + "@fontsource/archivo-black": "^5.2.8", + "@fontsource/eb-garamond": "^5.2.7", + "@fontsource/ibm-plex-mono": "^5.2.7", + "@fontsource/inter": "^5.2.8", + "@fontsource/jetbrains-mono": "^5.2.8", + "@fontsource/lato": "^5.2.7", + "@fontsource/league-gothic": "^5.2.8", + "@fontsource/montserrat": "^5.2.8", + "@fontsource/noto-sans-jp": "^5.2.9", + "@fontsource/nunito": "^5.2.7", + "@fontsource/open-sans": "^5.2.7", + "@fontsource/oswald": "^5.2.8", + "@fontsource/outfit": "^5.2.8", + "@fontsource/playfair-display": "^5.2.8", + "@fontsource/poppins": "^5.2.7", + "@fontsource/roboto": "^5.2.10", + "@fontsource/source-code-pro": "^5.2.7", + "@fontsource/space-mono": "^5.2.9", "@types/jsdom": "^28.0.0", "@types/node": "^25.0.10", "@vitest/coverage-v8": "^4.1.11", @@ -135,7 +154,7 @@ }, "packages/engine": { "name": "@hyperframes/engine", - "version": "0.8.119", + "version": "0.8.126", "dependencies": { "@hono/node-server": "^2.0.5", "@hyperframes/core": "workspace:^", @@ -154,7 +173,7 @@ }, "packages/gcp-cloud-run": { "name": "@hyperframes/gcp-cloud-run", - "version": "0.8.119", + "version": "0.8.126", "dependencies": { "@google-cloud/storage": "^7.14.0", "@google-cloud/workflows": "^4.2.0", @@ -175,7 +194,7 @@ }, "packages/lint": { "name": "@hyperframes/lint", - "version": "0.8.119", + "version": "0.8.126", "dependencies": { "@hyperframes/parsers": "workspace:*", "acorn": "^8.17.0", @@ -195,7 +214,7 @@ }, "packages/parsers": { "name": "@hyperframes/parsers", - "version": "0.8.119", + "version": "0.8.126", "dependencies": { "@babel/parser": "^7.27.0", "acorn": "^8.17.0", @@ -215,7 +234,7 @@ }, "packages/player": { "name": "@hyperframes/player", - "version": "0.8.119", + "version": "0.8.126", "dependencies": { "@hyperframes/core": "workspace:*", }, @@ -230,19 +249,8 @@ }, "packages/producer": { "name": "@hyperframes/producer", - "version": "0.8.119", + "version": "0.8.126", "dependencies": { - "@fontsource/archivo-black": "^5.2.8", - "@fontsource/eb-garamond": "^5.2.7", - "@fontsource/ibm-plex-mono": "^5.2.7", - "@fontsource/inter": "^5.2.8", - "@fontsource/jetbrains-mono": "^5.2.8", - "@fontsource/league-gothic": "^5.2.8", - "@fontsource/montserrat": "^5.2.8", - "@fontsource/nunito": "^5.2.7", - "@fontsource/oswald": "^5.2.8", - "@fontsource/outfit": "^5.2.8", - "@fontsource/space-mono": "^5.2.9", "@hono/node-server": "^2.0.5", "@hyperframes/core": "workspace:^", "@hyperframes/engine": "workspace:^", @@ -254,16 +262,8 @@ "postcss": "^8.4.0", "puppeteer": "^25.2.1", "puppeteer-core": "^25.2.1", - "wawoff2": "^2.0.1", }, "devDependencies": { - "@fontsource/lato": "^5.2.7", - "@fontsource/noto-sans-jp": "^5.2.9", - "@fontsource/open-sans": "^5.2.7", - "@fontsource/playfair-display": "^5.2.8", - "@fontsource/poppins": "^5.2.7", - "@fontsource/roboto": "^5.2.10", - "@fontsource/source-code-pro": "^5.2.7", "@hyperframes/aws-lambda": "workspace:*", "@types/node": "^25.0.10", "@webgpu/types": "^0.1.69", @@ -275,7 +275,7 @@ }, "packages/sdk": { "name": "@hyperframes/sdk", - "version": "0.8.119", + "version": "0.8.126", "dependencies": { "@hyperframes/core": "workspace:*", "@hyperframes/parsers": "workspace:*", @@ -305,7 +305,7 @@ }, "packages/shader-transitions": { "name": "@hyperframes/shader-transitions", - "version": "0.8.119", + "version": "0.8.126", "dependencies": { "html2canvas": "^1.4.1", }, @@ -317,7 +317,7 @@ }, "packages/studio": { "name": "@hyperframes/studio", - "version": "0.8.119", + "version": "0.8.126", "dependencies": { "@base-ui/react": "1.7.0", "@codemirror/autocomplete": "^6.20.1", @@ -370,7 +370,7 @@ }, "packages/studio-server": { "name": "@hyperframes/studio-server", - "version": "0.8.119", + "version": "0.8.126", "dependencies": { "@hyperframes/core": "workspace:*", "@hyperframes/parsers": "workspace:*", diff --git a/packages/aws-lambda/scripts/_handlerBanner.ts b/packages/aws-lambda/scripts/_handlerBanner.ts index 04c9ea9d58..1d2c915273 100644 --- a/packages/aws-lambda/scripts/_handlerBanner.ts +++ b/packages/aws-lambda/scripts/_handlerBanner.ts @@ -6,7 +6,7 @@ * assume all three exist at module scope: * - postcss et al. call top-level `require(...)` * - wawoff2's emscripten build (pulled in unconditionally via - * producer → fontCompression) reads `__dirname` at module scope + * producer → core fonts/fontCompression) reads `__dirname` at module scope * Without the shims the handler throws "Dynamic require of is not * supported" / "__dirname is not defined in ES module scope" at import time, * before it can run — which is exactly how a freshly deployed stack crashed diff --git a/packages/cli/scripts/build-fonts.mjs b/packages/cli/scripts/build-fonts.mjs index 6cf2f50dc3..f1a39e738a 100644 --- a/packages/cli/scripts/build-fonts.mjs +++ b/packages/cli/scripts/build-fonts.mjs @@ -2,13 +2,13 @@ import { existsSync } from "node:fs"; import { execSync } from "node:child_process"; -const target = "../producer/src/services/fontData.generated.ts"; +const target = "../core/src/fonts/fontData.generated.ts"; if (existsSync(target)) { console.log("[build:fonts] skipped — fontData.generated.ts already exists"); process.exit(0); } -execSync("cd ../producer && tsx scripts/generate-font-data.ts", { +execSync("cd ../core && tsx scripts/generate-font-data.ts", { stdio: "inherit", }); diff --git a/packages/cli/src/server/studioServer.ts b/packages/cli/src/server/studioServer.ts index e1acf8f2ec..0c65c6b41b 100644 --- a/packages/cli/src/server/studioServer.ts +++ b/packages/cli/src/server/studioServer.ts @@ -499,8 +499,7 @@ export function createStudioServer(options: StudioServerOptions): StudioServer { }, async transformPreviewHtml({ html, project }) { - const { injectDeterministicFontFaces } = - await import("../../../producer/src/services/deterministicFonts.js"); + const { injectDeterministicFontFaces } = await import("@hyperframes/core/fonts/embed"); const { prepareAnimatedGifInputs } = await import("../../../producer/src/services/animatedGifPrep.js"); const { downloadToTemp, writeUrlDownloadTelemetry } = diff --git a/packages/core/package-subpaths.json b/packages/core/package-subpaths.json index eead54ea7c..9f6200942a 100644 --- a/packages/core/package-subpaths.json +++ b/packages/core/package-subpaths.json @@ -482,6 +482,18 @@ "types": "./dist/fonts/systemFontLocator.d.ts", "environments": ["bun", "node"] }, + "./fonts/embed": { + "source": "./src/fonts/deterministicFonts.ts", + "runtime": "./dist/fonts/deterministicFonts.js", + "types": "./dist/fonts/deterministicFonts.d.ts", + "environments": ["bun", "node"] + }, + "./fonts/embedded-data": { + "source": "./src/fonts/fontData.generated.ts", + "runtime": "./dist/fonts/fontData.generated.js", + "types": "./dist/fonts/fontData.generated.d.ts", + "environments": ["bun", "node"] + }, "./figma": { "source": "./src/figma/index.ts", "runtime": "./dist/figma/index.js", diff --git a/packages/core/package.json b/packages/core/package.json index e6f41541f1..73d6bf3b13 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -491,6 +491,18 @@ "import": "./src/fonts/systemFontLocator.ts", "types": "./src/fonts/systemFontLocator.ts" }, + "./fonts/embed": { + "bun": "./src/fonts/deterministicFonts.ts", + "node": "./dist/fonts/deterministicFonts.js", + "import": "./src/fonts/deterministicFonts.ts", + "types": "./src/fonts/deterministicFonts.ts" + }, + "./fonts/embedded-data": { + "bun": "./src/fonts/fontData.generated.ts", + "node": "./dist/fonts/fontData.generated.js", + "import": "./src/fonts/fontData.generated.ts", + "types": "./src/fonts/fontData.generated.ts" + }, "./figma": { "bun": "./src/figma/index.ts", "node": "./dist/figma/index.js", @@ -817,6 +829,14 @@ "import": "./dist/fonts/systemFontLocator.js", "types": "./dist/fonts/systemFontLocator.d.ts" }, + "./fonts/embed": { + "import": "./dist/fonts/deterministicFonts.js", + "types": "./dist/fonts/deterministicFonts.d.ts" + }, + "./fonts/embedded-data": { + "import": "./dist/fonts/fontData.generated.js", + "types": "./dist/fonts/fontData.generated.d.ts" + }, "./figma": { "import": "./dist/figma/index.js", "types": "./dist/figma/index.d.ts" @@ -828,7 +848,8 @@ "types": "./dist/index.d.ts" }, "scripts": { - "build": "bun run build:hyperframes-runtime && bun run build:position-edits-render && bun run build:audio-fx-runtime && tsc && tsx scripts/rewrite-esm-extensions.ts", + "build": "bun run build:fonts && bun run build:hyperframes-runtime && bun run build:position-edits-render && bun run build:audio-fx-runtime && tsc && tsx scripts/rewrite-esm-extensions.ts", + "build:fonts": "node scripts/build-fonts.mjs", "test": "bun run check:position-edits-render && bun run build:audio-fx-runtime && vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", @@ -861,9 +882,28 @@ "@hyperframes/studio-server": "workspace:*", "bpm-detective": "^2.0.5", "linkedom": "^0.18.12", - "postcss": "^8.5.8" + "postcss": "^8.5.8", + "wawoff2": "^2.0.1" }, "devDependencies": { + "@fontsource/archivo-black": "^5.2.8", + "@fontsource/eb-garamond": "^5.2.7", + "@fontsource/ibm-plex-mono": "^5.2.7", + "@fontsource/inter": "^5.2.8", + "@fontsource/jetbrains-mono": "^5.2.8", + "@fontsource/lato": "^5.2.7", + "@fontsource/league-gothic": "^5.2.8", + "@fontsource/montserrat": "^5.2.8", + "@fontsource/noto-sans-jp": "^5.2.9", + "@fontsource/nunito": "^5.2.7", + "@fontsource/open-sans": "^5.2.7", + "@fontsource/oswald": "^5.2.8", + "@fontsource/outfit": "^5.2.8", + "@fontsource/playfair-display": "^5.2.8", + "@fontsource/poppins": "^5.2.7", + "@fontsource/roboto": "^5.2.10", + "@fontsource/source-code-pro": "^5.2.7", + "@fontsource/space-mono": "^5.2.9", "@types/jsdom": "^28.0.0", "@types/node": "^25.0.10", "@vitest/coverage-v8": "^4.1.11", diff --git a/packages/producer/scripts/build-fonts.mjs b/packages/core/scripts/build-fonts.mjs similarity index 86% rename from packages/producer/scripts/build-fonts.mjs rename to packages/core/scripts/build-fonts.mjs index faa209f57b..1d38524aa8 100644 --- a/packages/producer/scripts/build-fonts.mjs +++ b/packages/core/scripts/build-fonts.mjs @@ -2,7 +2,7 @@ import { existsSync } from "node:fs"; import { execSync } from "node:child_process"; -const target = "src/services/fontData.generated.ts"; +const target = "src/fonts/fontData.generated.ts"; if (existsSync(target)) { console.log("[build:fonts] skipped — fontData.generated.ts already exists"); diff --git a/packages/producer/scripts/generate-font-data.ts b/packages/core/scripts/generate-font-data.ts similarity index 98% rename from packages/producer/scripts/generate-font-data.ts rename to packages/core/scripts/generate-font-data.ts index 76925207d2..01614c6420 100644 --- a/packages/producer/scripts/generate-font-data.ts +++ b/packages/core/scripts/generate-font-data.ts @@ -159,7 +159,7 @@ function main() { lines.push("]);"); lines.push(""); - const outputPath = resolve(__dirname, "../src/services/fontData.generated.ts"); + const outputPath = resolve(__dirname, "../src/fonts/fontData.generated.ts"); writeFileSync(outputPath, lines.join("\n"), "utf8"); console.log( `[generate-font-data] Wrote ${entries.length} font faces (${Math.round(totalBytes / 1024)}KB) → ${outputPath}`, diff --git a/packages/producer/src/services/authoredGoogleFonts.test.ts b/packages/core/src/fonts/authoredGoogleFonts.test.ts similarity index 97% rename from packages/producer/src/services/authoredGoogleFonts.test.ts rename to packages/core/src/fonts/authoredGoogleFonts.test.ts index bc52c95ab4..44aa7800f4 100644 --- a/packages/producer/src/services/authoredGoogleFonts.test.ts +++ b/packages/core/src/fonts/authoredGoogleFonts.test.ts @@ -1,4 +1,5 @@ -import { expect, it } from "bun:test"; +// @vitest-environment node +import { expect, it } from "vitest"; import { authoredGoogleFontStylesheets, withPageText } from "./authoredGoogleFonts.js"; const url = "https://fonts.googleapis.com/css2?family=Fraunces:wght@400"; diff --git a/packages/producer/src/services/authoredGoogleFonts.ts b/packages/core/src/fonts/authoredGoogleFonts.ts similarity index 100% rename from packages/producer/src/services/authoredGoogleFonts.ts rename to packages/core/src/fonts/authoredGoogleFonts.ts diff --git a/packages/producer/src/services/deterministicFonts-aliasSupplement.test.ts b/packages/core/src/fonts/deterministicFonts-aliasSupplement.test.ts similarity index 99% rename from packages/producer/src/services/deterministicFonts-aliasSupplement.test.ts rename to packages/core/src/fonts/deterministicFonts-aliasSupplement.test.ts index 186abb3e19..2e1925cfc7 100644 --- a/packages/producer/src/services/deterministicFonts-aliasSupplement.test.ts +++ b/packages/core/src/fonts/deterministicFonts-aliasSupplement.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment node /** * Regression test for cross-typeface alias supplementation. * @@ -16,7 +17,7 @@ * so they are hermetic. */ -import { afterAll, beforeAll, beforeEach, describe, expect, it } from "bun:test"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -294,7 +295,7 @@ describe("aliased font-family supplementation", () => { // The supplementary fetch is skipped outright when this lookup fails, so // every alias must resolve or its family silently loses Google's weights. for (const alias of Object.keys(FONT_ALIAS_MAP)) { - expect(resolveAliasDisplayName(alias)).toBeString(); + expect(resolveAliasDisplayName(alias)).toEqual(expect.any(String)); } }); diff --git a/packages/producer/src/services/deterministicFonts-authoredResources.test.ts b/packages/core/src/fonts/deterministicFonts-authoredResources.test.ts similarity index 95% rename from packages/producer/src/services/deterministicFonts-authoredResources.test.ts rename to packages/core/src/fonts/deterministicFonts-authoredResources.test.ts index 704fd113bc..e5d459dcf8 100644 --- a/packages/producer/src/services/deterministicFonts-authoredResources.test.ts +++ b/packages/core/src/fonts/deterministicFonts-authoredResources.test.ts @@ -1,4 +1,5 @@ -import { afterEach, beforeEach, expect, it, spyOn } from "bun:test"; +// @vitest-environment node +import { afterEach, beforeEach, expect, it, vi } from "vitest"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -18,7 +19,7 @@ beforeEach(() => { process.env.HYPERFRAMES_FONT_CACHE_DIR = cacheDir; requests.length = 0; _clearGoogleFontCssCacheForTests(); - const mock = spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const mock = vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { const url = new URL(String(input)); requests.push(url.href); if (url.hostname === "fonts.googleapis.com") { diff --git a/packages/producer/src/services/deterministicFonts-bundledStylesheet.test.ts b/packages/core/src/fonts/deterministicFonts-bundledStylesheet.test.ts similarity index 94% rename from packages/producer/src/services/deterministicFonts-bundledStylesheet.test.ts rename to packages/core/src/fonts/deterministicFonts-bundledStylesheet.test.ts index 952e28a974..90bc781f27 100644 --- a/packages/producer/src/services/deterministicFonts-bundledStylesheet.test.ts +++ b/packages/core/src/fonts/deterministicFonts-bundledStylesheet.test.ts @@ -1,4 +1,5 @@ -import { afterEach, expect, it, spyOn } from "bun:test"; +// @vitest-environment node +import { afterEach, expect, it, vi } from "vitest"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -9,7 +10,7 @@ import { const previousCacheDir = process.env.HYPERFRAMES_FONT_CACHE_DIR; const cacheDir = mkdtempSync(join(tmpdir(), "hf-bundled-stylesheet-")); -const fetchMock = spyOn(globalThis, "fetch"); +const fetchMock = vi.spyOn(globalThis, "fetch"); afterEach(() => { fetchMock.mockRestore(); diff --git a/packages/producer/src/services/deterministicFonts-bundledSubset.test.ts b/packages/core/src/fonts/deterministicFonts-bundledSubset.test.ts similarity index 91% rename from packages/producer/src/services/deterministicFonts-bundledSubset.test.ts rename to packages/core/src/fonts/deterministicFonts-bundledSubset.test.ts index 48e091efc1..181bd733b3 100644 --- a/packages/producer/src/services/deterministicFonts-bundledSubset.test.ts +++ b/packages/core/src/fonts/deterministicFonts-bundledSubset.test.ts @@ -1,19 +1,18 @@ +// @vitest-environment node /** * Regression test for the coverage a bundled face claims. * * `scripts/generate-font-data.ts` embeds the `-latin-` subset file of every * canonical family, so an embedded face carries only Google's `latin` subset. - * The emitted `@font-face` used to omit `unicode-range` — advertising full - * coverage — and the supplementary Google fetch then skipped every subset of a - * weight the bundle "covered". A bundled family therefore could not render the - * scripts its own subset omits: `Noto Sans JP` weight 400 carries 218 - * codepoints with no kana and no kanji. + * The emitted `@font-face` therefore carries the latin `unicode-range`, and the + * supplementary Google fetch fills the subsets it omits: `Noto Sans JP` weight + * 400 carries 218 codepoints with no kana and no kanji. * * These tests inject `fetchImpl` (no network) and a temp * `HYPERFRAMES_FONT_CACHE_DIR` so they are hermetic. */ -import { afterAll, beforeAll, describe, expect, it } from "bun:test"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -106,7 +105,7 @@ describe("bundled Latin precedence", () => { const fetchImpl = (async (input: unknown) => { const url = String(input); if (url.startsWith("https://fonts.googleapis.com/")) { - expect(new URL(url).searchParams.get("family")).toStartWith("Inter:"); + expect(new URL(url).searchParams.get("family")).toMatch(/^Inter:/); return new Response(`@font-face { font-family: 'Inter'; font-style: normal; font-weight: 400; src: url(https://fonts.gstatic.com/s/inter/text-subset.woff2) format('woff2'); diff --git a/packages/producer/src/services/deterministicFonts-declaredAlias.test.ts b/packages/core/src/fonts/deterministicFonts-declaredAlias.test.ts similarity index 99% rename from packages/producer/src/services/deterministicFonts-declaredAlias.test.ts rename to packages/core/src/fonts/deterministicFonts-declaredAlias.test.ts index 9bc95368a5..d3803701cc 100644 --- a/packages/producer/src/services/deterministicFonts-declaredAlias.test.ts +++ b/packages/core/src/fonts/deterministicFonts-declaredAlias.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment node /** * Declared-family alias resolution. * @@ -15,7 +16,7 @@ * at a temp dir, so these tests are hermetic. */ -import { afterAll, beforeEach, describe, expect, it } from "bun:test"; +import { afterAll, beforeEach, describe, expect, it } from "vitest"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; diff --git a/packages/producer/src/services/deterministicFonts-failClosed.test.ts b/packages/core/src/fonts/deterministicFonts-failClosed.test.ts similarity index 96% rename from packages/producer/src/services/deterministicFonts-failClosed.test.ts rename to packages/core/src/fonts/deterministicFonts-failClosed.test.ts index 359e03e111..2be9000a08 100644 --- a/packages/producer/src/services/deterministicFonts-failClosed.test.ts +++ b/packages/core/src/fonts/deterministicFonts-failClosed.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment node /** * Tests for `injectDeterministicFontFaces`'s `failClosedFontFetch` gate. * @@ -14,8 +15,7 @@ * The tests inject `fetchImpl` so no real network call happens. */ -import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"; -import { defaultLogger } from "../logger.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { _clearGoogleFontCssCacheForTests, FONT_FETCH_FAILED, @@ -106,6 +106,23 @@ describe("injectDeterministicFontFaces — failClosedFontFetch: false (default)" expect(result.includes("data-hyperframes-deterministic-fonts")).toBe(false); }); + it("sends its warnings to the caller's logger instead of the console", async () => { + const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warn = vi.fn(); + try { + await injectDeterministicFontFaces(HTML_REQUESTING_UNRESOLVED_FONT, { + failClosedFontFetch: false, + allowSystemFontCapture: false, + fetchImpl: makeFailingFetch(), + logger: { warn, info: vi.fn() }, + }); + expect(warn).toHaveBeenCalled(); + expect(consoleWarn).not.toHaveBeenCalled(); + } finally { + consoleWarn.mockRestore(); + } + }); + it("swallows a 404 response and returns the original HTML (no throw)", async () => { const result = await injectDeterministicFontFaces(HTML_REQUESTING_UNRESOLVED_FONT, { failClosedFontFetch: false, @@ -431,10 +448,10 @@ describe("fail-closed fonts named only in an undefined var() fallback", () => { `body { font-family: var(--brand, "Acme Brand Sans", sans-serif); }`, ); let warnings: string[] = []; - let warnSpy: ReturnType; + let warnSpy: ReturnType; beforeEach(() => { warnings = []; - warnSpy = spyOn(defaultLogger, "warn").mockImplementation((message: string) => { + warnSpy = vi.spyOn(console, "warn").mockImplementation((message: string) => { warnings.push(message); }); }); diff --git a/packages/producer/src/services/deterministicFonts-googleSubsets.test.ts b/packages/core/src/fonts/deterministicFonts-googleSubsets.test.ts similarity index 99% rename from packages/producer/src/services/deterministicFonts-googleSubsets.test.ts rename to packages/core/src/fonts/deterministicFonts-googleSubsets.test.ts index 25018fa90b..4bdec0f468 100644 --- a/packages/producer/src/services/deterministicFonts-googleSubsets.test.ts +++ b/packages/core/src/fonts/deterministicFonts-googleSubsets.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment node /** * Regression test for the Google Fonts multi-subset cache collision. * @@ -19,17 +20,18 @@ * so they are hermetic. */ -import { afterAll, beforeAll, beforeEach, describe, expect, it } from "bun:test"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { _clearGoogleFontCssCacheForTests } from "./deterministicFonts.js"; +import { fontDirectories } from "./systemFontLocator.js"; beforeEach(() => _clearGoogleFontCssCacheForTests()); let cacheDir: string; let prevCacheEnv: string | undefined; -const LOCAL_FONT_DIR = join(homedir(), ".local", "share", "fonts"); +const LOCAL_FONT_DIR = fontDirectories().find((dir) => dir.startsWith(homedir()))!; const LOCAL_FONT_FILE = join(LOCAL_FONT_DIR, "hf-authored-fail-test.woff2"); const LOCAL_FONT_BYTES = "LOCAL_ONLY_BYTES"; diff --git a/packages/producer/src/services/deterministicFonts-oblique.test.ts b/packages/core/src/fonts/deterministicFonts-oblique.test.ts similarity index 93% rename from packages/producer/src/services/deterministicFonts-oblique.test.ts rename to packages/core/src/fonts/deterministicFonts-oblique.test.ts index 8a3c9962fb..49e7334e2b 100644 --- a/packages/producer/src/services/deterministicFonts-oblique.test.ts +++ b/packages/core/src/fonts/deterministicFonts-oblique.test.ts @@ -1,4 +1,5 @@ -import { afterEach, beforeEach, expect, it, spyOn } from "bun:test"; +// @vitest-environment node +import { afterEach, beforeEach, expect, it, vi } from "vitest"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -30,7 +31,7 @@ it.each(["oblique", "oblique 10deg", "oblique 0deg 10deg", "oblique -12.5deg 2.5 "embeds every subset of an authored %s face in fail-closed mode", async (style) => { const subsets = ["U+0000-00FF", "U+0100-017F"]; - const fetchMock = spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { const url = String(input); if (url.startsWith("https://fonts.googleapis.com/")) { return new Response( diff --git a/packages/producer/src/services/deterministicFonts-order.test.ts b/packages/core/src/fonts/deterministicFonts-order.test.ts similarity index 96% rename from packages/producer/src/services/deterministicFonts-order.test.ts rename to packages/core/src/fonts/deterministicFonts-order.test.ts index beadec94aa..d2cdb22704 100644 --- a/packages/producer/src/services/deterministicFonts-order.test.ts +++ b/packages/core/src/fonts/deterministicFonts-order.test.ts @@ -1,4 +1,5 @@ -import { afterAll, beforeAll, describe, expect, it } from "bun:test"; +// @vitest-environment node +import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; diff --git a/packages/producer/src/services/deterministicFonts-retries.test.ts b/packages/core/src/fonts/deterministicFonts-retries.test.ts similarity index 94% rename from packages/producer/src/services/deterministicFonts-retries.test.ts rename to packages/core/src/fonts/deterministicFonts-retries.test.ts index 5dd7020bea..4822a4cc66 100644 --- a/packages/producer/src/services/deterministicFonts-retries.test.ts +++ b/packages/core/src/fonts/deterministicFonts-retries.test.ts @@ -1,4 +1,5 @@ -import { afterAll, beforeAll, describe, expect, it } from "bun:test"; +// @vitest-environment node +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -29,7 +30,7 @@ afterAll(() => { const FAST_RETRY = { maxAttempts: 2, attemptTimeoutMs: 20, - maxElapsedMs: 100, + maxElapsedMs: 10_000, baseDelayMs: 0, } as const; @@ -264,7 +265,6 @@ describe("deterministic Google Fonts retries", () => { const result = injectWithRetries(html, fetchImpl, { ...FAST_RETRY, attemptTimeoutMs: 5, - maxElapsedMs: 50, }); await expect(result).rejects.toMatchObject({ code: FONT_FETCH_UNAVAILABLE }); @@ -278,14 +278,11 @@ describe("deterministic Google Fonts retries", () => { calls += 1; return new Response("", { status: 429, - headers: { "Retry-After": "1" }, + headers: { "Retry-After": "60" }, }); }) as typeof fetch; - const result = injectWithRetries(html, fetchImpl, { - ...FAST_RETRY, - maxElapsedMs: 100, - }); + const result = injectWithRetries(html, fetchImpl); await expect(result).rejects.toMatchObject({ code: FONT_FETCH_UNAVAILABLE }); expect(calls).toBe(1); @@ -341,14 +338,20 @@ describe("deterministic Google Fonts retries", () => { }); }) as typeof fetch; - const result = injectWithRetries(html, fetchImpl, { - maxAttempts: 2, - attemptTimeoutMs: 70, - maxElapsedMs: 100, - baseDelayMs: 0, - }); - - await expect(result).rejects.toMatchObject({ code: FONT_FETCH_UNAVAILABLE }); + vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] }); + try { + const result = injectWithRetries(html, fetchImpl, { + maxAttempts: 2, + attemptTimeoutMs: 70, + maxElapsedMs: 100, + baseDelayMs: 0, + }); + const settled = expect(result).rejects.toMatchObject({ code: FONT_FETCH_UNAVAILABLE }); + await vi.advanceTimersByTimeAsync(100); + await settled; + } finally { + vi.useRealTimers(); + } expect(firstCalls).toBe(1); expect(secondCalls).toBe(1); }); diff --git a/packages/producer/src/services/deterministicFonts-systemCapture.test.ts b/packages/core/src/fonts/deterministicFonts-systemCapture.test.ts similarity index 98% rename from packages/producer/src/services/deterministicFonts-systemCapture.test.ts rename to packages/core/src/fonts/deterministicFonts-systemCapture.test.ts index 87c716b906..b817fd59a4 100644 --- a/packages/producer/src/services/deterministicFonts-systemCapture.test.ts +++ b/packages/core/src/fonts/deterministicFonts-systemCapture.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment node /** * Tests for the system font capture path (Path 3) in `buildFontFaceCss`. * @@ -10,7 +11,7 @@ * not served) so the resolver falls through to the system font path. */ -import { describe, expect, it } from "bun:test"; +import { describe, expect, it } from "vitest"; import { existsSync } from "node:fs"; import { fontFormatHint, injectDeterministicFontFaces } from "./deterministicFonts.js"; diff --git a/packages/producer/src/services/deterministicFonts-textSubset.test.ts b/packages/core/src/fonts/deterministicFonts-textSubset.test.ts similarity index 99% rename from packages/producer/src/services/deterministicFonts-textSubset.test.ts rename to packages/core/src/fonts/deterministicFonts-textSubset.test.ts index 5023e69e93..2f1d228e8e 100644 --- a/packages/producer/src/services/deterministicFonts-textSubset.test.ts +++ b/packages/core/src/fonts/deterministicFonts-textSubset.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it } from "bun:test"; +// @vitest-environment node +import { describe, expect, it } from "vitest"; import { injectDeterministicFontFaces } from "./deterministicFonts.js"; async function requestedGoogleFontUrl(html: string): Promise { diff --git a/packages/producer/src/services/deterministicFonts.test.ts b/packages/core/src/fonts/deterministicFonts.test.ts similarity index 99% rename from packages/producer/src/services/deterministicFonts.test.ts rename to packages/core/src/fonts/deterministicFonts.test.ts index 78bdac0b3c..1d572d2295 100644 --- a/packages/producer/src/services/deterministicFonts.test.ts +++ b/packages/core/src/fonts/deterministicFonts.test.ts @@ -1,4 +1,5 @@ -import { beforeEach, describe, expect, it } from "bun:test"; +// @vitest-environment node +import { beforeEach, describe, expect, it } from "vitest"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; diff --git a/packages/producer/src/services/deterministicFonts.ts b/packages/core/src/fonts/deterministicFonts.ts similarity index 98% rename from packages/producer/src/services/deterministicFonts.ts rename to packages/core/src/fonts/deterministicFonts.ts index 83848a4749..02c29a36cd 100644 --- a/packages/producer/src/services/deterministicFonts.ts +++ b/packages/core/src/fonts/deterministicFonts.ts @@ -2,13 +2,9 @@ import { createHash } from "node:crypto"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; -import { defaultLogger } from "../logger.js"; -import { FONT_ALIAS_MAP, resolveAliasDisplayName } from "@hyperframes/core/fonts/aliases"; -import { - locateSystemFontVariants, - SYSTEM_FONT_SIZE_LIMIT, -} from "@hyperframes/core/fonts/system-locator"; +import { FONT_ALIAS_MAP, resolveAliasDisplayName } from "./aliases.js"; +import { locateSystemFontVariants, SYSTEM_FONT_SIZE_LIMIT } from "./systemFontLocator.js"; import { parseHTML } from "linkedom"; import postcss, { type AtRule, type Declaration, type Rule } from "postcss"; import { EMBEDDED_FONT_DATA } from "./fontData.generated.js"; @@ -68,6 +64,7 @@ const CSS_WIDE_KEYWORDS: ReadonlySet = new Set([ * Only top-level commas split: a `var(--x, fallback)` expression stays one * token, as does a comma inside a quoted family name. */ +// fallow-ignore-next-line complexity export function parseFontFamilyValue(value: string): string[] { const pieces: string[] = []; let start = 0; @@ -472,11 +469,11 @@ const CANONICAL_FONTS: Record = { }, }; -// FONT_ALIASES derives from the shared alias map in @hyperframes/core. -// The cast is safe: every value in FONT_ALIAS_MAP is a valid CANONICAL_FONTS key. +// FONT_ALIASES derives from the shared alias map; every FONT_ALIAS_MAP value is a CANONICAL_FONTS key. export const FONT_ALIASES = FONT_ALIAS_MAP as Record; -export { FONT_ALIAS_KEYS } from "@hyperframes/core/fonts/aliases"; +export { FONT_ALIAS_KEYS } from "./aliases.js"; +export { fontToDataUri } from "./fontCompression.js"; function normalizeFamilyName(family: string): string { return family @@ -752,6 +749,7 @@ async function googleFamilyFaceRules( async function systemFamilyFaceRules( lookupFamily: string, emitFamily: string, + log: FontLogger, ): Promise { const variants = locateSystemFontVariants(lookupFamily); if (variants.length === 0) return null; @@ -764,11 +762,11 @@ async function systemFamilyFaceRules( rules.push(buildFontFaceRule(emitFamily, dataUri, variant.weight, variant.style)); } if (totalBytes > SYSTEM_FONT_SIZE_LIMIT) { - defaultLogger.warn( + log.warn( `[Compiler] System font "${lookupFamily}" is large (${(totalBytes / 1024 / 1024).toFixed(1)} MB total across ${variants.length} variant(s)) — embedding anyway. Consider font subsetting for production.`, ); } - defaultLogger.info( + log.info( `[Compiler] Embedded system font "${lookupFamily}" — ${variants.length} variant(s), ${(totalBytes / 1024).toFixed(0)} KB total`, ); return rules; @@ -799,7 +797,7 @@ async function resolveFamilyFaceRules( return ( (await googleFamilyFaceRules(lookupFamily, emitFamily, optional, options, fontText)) ?? (options.allowSystemFontCapture && !pageNamedThisFile(lookupFamily, options) - ? await systemFamilyFaceRules(lookupFamily, emitFamily) + ? await systemFamilyFaceRules(lookupFamily, emitFamily, options.log) : null) ); } @@ -989,7 +987,7 @@ async function resolveDeclaredFamilyAlias( } if (!rules) return null; - defaultLogger.warn( + options.log.warn( `[Compiler] font-family "${authoredFamily}" is not a known family; resolved it to "${declared.family}", ` + `which this document declares. Correct the authored font-family to "${declared.family}".`, ); @@ -1044,7 +1042,7 @@ async function buildFontFaceCss( }; } -function warnUnresolvedFonts(unresolved: string[]): void { +function warnUnresolvedFonts(unresolved: string[], log: FontLogger): void { const mapped = Object.entries(FONT_ALIASES) .reduce((acc, [alias, canonical]) => { const display = alias === canonical ? alias : `${alias} → ${canonical}`; @@ -1052,7 +1050,7 @@ function warnUnresolvedFonts(unresolved: string[]): void { return acc; }, []) .sort(); - defaultLogger.warn( + log.warn( `[Compiler] No deterministic font mapping for: ${unresolved.join(", ")}\n` + ` Mapped fonts: ${mapped.join(", ")}\n` + ` To fix, pick one:\n` + @@ -1109,7 +1107,7 @@ function fontCacheDir(slug: string): string { const fallback = join(ephemeralFontCacheRoot, slug); mkdirSync(fallback, { recursive: true }); if (firstFallback) { - defaultLogger.warn( + STDERR_LOGGER.warn( `Font cache directory is unwritable (${dir}). ` + `Using temporary fallback — fonts will re-download each run. ` + `Fix with: chmod 755 ${resolveFontCacheRoot()}`, @@ -1213,6 +1211,7 @@ const DEFAULT_FONT_FETCH_RETRY_POLICY: FontFetchRetryPolicy = { /** Internal threading of the failClosed flag + fetch override through callers. */ interface InternalFontFetchOptions { + log: FontLogger; failClosedFontFetch: boolean; fetchImpl: typeof fetch; allowSystemFontCapture: boolean; @@ -1613,7 +1612,7 @@ async function fetchGoogleFontStylesheet( } if (faces.length > 0) { - defaultLogger.info( + options.log.info( `[Compiler] Fetched ${faces.length} font face(s) for "${familyName}" from Google Fonts (cached to ${fontCacheDir(slug)})`, ); } @@ -1638,7 +1637,7 @@ async function fetchFamilyFaces( ); } catch (err) { if (!(err instanceof FontFetchUnavailableError)) throw err; - defaultLogger.warn( + options.log.warn( `[Compiler] Optional font "${familyName}" is unavailable, rendering its fallback: ${err.message}`, ); return []; @@ -1650,7 +1649,18 @@ async function fetchFamilyFaces( /** * Options for {@link injectDeterministicFontFaces}. */ +export interface FontLogger { + warn(message: string): void; + info(message: string): void; +} + +const STDERR_LOGGER: FontLogger = { + warn: (message) => console.warn(`[WARN] ${message}`), + info: (message) => console.error(`[INFO] ${message}`), +}; + export interface InjectDeterministicFontFacesOptions { + logger?: FontLogger; /** * When `true`, exhausted transient fetch failures of a required family throw * {@link FontFetchUnavailableError} with code `FONT_FETCH_UNAVAILABLE`; @@ -1863,6 +1873,7 @@ export async function injectDeterministicFontFaces( const allowSystemFontCapture = options.allowSystemFontCapture !== false; const retryPolicy = resolveFontFetchRetryPolicy(options.fontFetchRetryPolicy); const fetchOptions: InternalFontFetchOptions = { + log: options.logger ?? STDERR_LOGGER, failClosedFontFetch, fetchImpl, allowSystemFontCapture, @@ -1910,7 +1921,7 @@ export async function injectDeterministicFontFaces( } if (!css) { if (unresolved.length > 0) { - warnUnresolvedFonts(unresolved); + warnUnresolvedFonts(unresolved, fetchOptions.log); } return html; } @@ -1926,11 +1937,11 @@ export async function injectDeterministicFontFaces( styleEl.textContent = css; placeAfterEveryStylesheet(document, head, styleEl); - defaultLogger.info( + fetchOptions.log.info( `[Compiler] Injected deterministic @font-face rules for ${pendingFamilies.size - unresolved.length} requested font families`, ); if (unresolved.length > 0) { - warnUnresolvedFonts(unresolved); + warnUnresolvedFonts(unresolved, fetchOptions.log); } return document.toString(); diff --git a/packages/producer/src/services/fontCompression.test.ts b/packages/core/src/fonts/fontCompression.test.ts similarity index 82% rename from packages/producer/src/services/fontCompression.test.ts rename to packages/core/src/fonts/fontCompression.test.ts index 07af822d41..4c20358a0d 100644 --- a/packages/producer/src/services/fontCompression.test.ts +++ b/packages/core/src/fonts/fontCompression.test.ts @@ -1,43 +1,21 @@ -import { describe, expect, it, spyOn } from "bun:test"; -import { - existsSync, - mkdirSync, - mkdtempSync, - readFileSync, - readdirSync, - rmSync, - writeFileSync, -} from "node:fs"; +// @vitest-environment node +import { describe, expect, it, vi } from "vitest"; +import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import { compressToWoff2, fontToDataUri } from "./fontCompression.js"; -/** - * Locate a system TTF font for real compression tests. macOS ships plenty - * of .ttf files in /System/Library/Fonts/Supplemental; we pick the first - * one found from a short list. Returns null on Linux CI or any environment - * without the expected fonts — those tests are skipped gracefully. - */ -function findSystemTtf(): Buffer | null { - const candidates = [ - "/System/Library/Fonts/Supplemental/Andale Mono.ttf", - "/System/Library/Fonts/Supplemental/Courier New.ttf", - "/System/Library/Fonts/Supplemental/Arial.ttf", - "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", - ]; - for (const path of candidates) { - if (existsSync(path)) return readFileSync(path); - } - return null; -} +const GEIST_TTF = fileURLToPath( + new URL( + "../../../../docs/public/catalog/items/frost-sequence-camera-orbit/assets/fonts/Geist-Regular.ttf", + import.meta.url, + ), +); describe("compressToWoff2", () => { it("compresses a TTF buffer to a smaller woff2 buffer", async () => { - const ttf = findSystemTtf(); - if (!ttf) { - console.warn("Skipping: no system TTF font available"); - return; - } + const ttf = readFileSync(GEIST_TTF); const woff2 = await compressToWoff2(ttf); expect(woff2).toBeInstanceOf(Buffer); expect(woff2.length).toBeGreaterThan(0); @@ -56,7 +34,7 @@ describe("fontToDataUri", () => { const raw = Buffer.from("stable-font-content"); const compressed = Buffer.from("compressed-font-content"); const compressImpl = async () => compressed; - const clock = spyOn(Date, "now").mockReturnValue(1234567890); + const clock = vi.spyOn(Date, "now").mockReturnValue(1234567890); try { const first = await fontToDataUri(raw, "ttf", { cacheDir, compressImpl }); const [cacheName] = readdirSync(cacheDir); @@ -139,11 +117,7 @@ describe("fontToDataUri", () => { }); it("compresses a TTF and returns a woff2 data URI", async () => { - const ttf = findSystemTtf(); - if (!ttf) { - console.warn("Skipping: no system TTF font available"); - return; - } + const ttf = readFileSync(GEIST_TTF); const uri = await fontToDataUri(ttf, "ttf"); expect(uri).toMatch(/^data:font\/woff2;base64,/); const naiveLength = `data:font/ttf;base64,${ttf.toString("base64")}`.length; diff --git a/packages/producer/src/services/fontCompression.ts b/packages/core/src/fonts/fontCompression.ts similarity index 96% rename from packages/producer/src/services/fontCompression.ts rename to packages/core/src/fonts/fontCompression.ts index ad4c5cfe11..1578aae98c 100644 --- a/packages/producer/src/services/fontCompression.ts +++ b/packages/core/src/fonts/fontCompression.ts @@ -1,4 +1,4 @@ -// @ts-expect-error -- wawoff2 ships no type declarations; ambient .d.ts only visible to producer's own tsconfig +// @ts-expect-error -- wawoff2 ships no type declarations import wawoff2 from "wawoff2"; import { createHash } from "node:crypto"; import { diff --git a/packages/producer/src/services/fontData.generated.ts b/packages/core/src/fonts/fontData.generated.ts similarity index 100% rename from packages/producer/src/services/fontData.generated.ts rename to packages/core/src/fonts/fontData.generated.ts diff --git a/packages/gcp-cloud-run/Dockerfile b/packages/gcp-cloud-run/Dockerfile index 26196e803d..84417f2021 100644 --- a/packages/gcp-cloud-run/Dockerfile +++ b/packages/gcp-cloud-run/Dockerfile @@ -142,8 +142,8 @@ COPY packages/studio-server/ packages/studio-server/ # Build workspace dependencies before the producer bundle. The published # workspace packages are not prebuilt in a fresh clone, so producer's esbuild -# resolution otherwise fails on parsers/core compiler entrypoints. Generate -# embedded font data so glyph layout matches production. +# resolution otherwise fails on parsers/core compiler entrypoints. Core's build +# also generates the embedded font data, so glyph layout matches production. RUN bun run --cwd packages/parsers build \ && bun run --cwd packages/lint build \ && bun run --cwd packages/studio-server build \ @@ -152,7 +152,6 @@ RUN bun run --cwd packages/parsers build \ && bun run --cwd packages/sdk build \ && bun run --cwd packages/sdk-playground build \ && bun run --cwd packages/engine build \ - && (cd packages/producer && bunx tsx scripts/generate-font-data.ts) \ && bun run --cwd packages/producer build \ && bun run --cwd packages/gcp-cloud-run build diff --git a/packages/parsers/src/fontAliases.ts b/packages/parsers/src/fontAliases.ts index 9328f0dc5a..0b87d99e1d 100644 --- a/packages/parsers/src/fontAliases.ts +++ b/packages/parsers/src/fontAliases.ts @@ -4,7 +4,7 @@ * eliminating manual drift between the two. * * Keys are lowercase font family names. Values are canonical font slugs - * matching CANONICAL_FONTS keys in the producer's deterministicFonts module. + * matching CANONICAL_FONTS keys in core's fonts/deterministicFonts module. */ export const FONT_ALIAS_MAP = { // ── Canonical bundled fonts (self-referencing) ──────────────────────── diff --git a/packages/producer/package.json b/packages/producer/package.json index 135ee5cd42..11f7302a71 100644 --- a/packages/producer/package.json +++ b/packages/producer/package.json @@ -42,8 +42,7 @@ "registry": "https://registry.npmjs.org/" }, "scripts": { - "build": "bun run build:fonts && bun run build:hf-early-stub && bun run --cwd ../.. build:hyperframes-runtime:modular && node build.mjs", - "build:fonts": "node scripts/build-fonts.mjs", + "build": "bun run build:hf-early-stub && bun run --cwd ../.. build:hyperframes-runtime:modular && node build.mjs", "build:hf-early-stub": "bun run scripts/build-hf-early-stub.ts", "typecheck": "tsc --noEmit", "parity:check": "tsx src/parity-harness.ts", @@ -76,17 +75,6 @@ "prepublishOnly": "echo skip" }, "dependencies": { - "@fontsource/archivo-black": "^5.2.8", - "@fontsource/eb-garamond": "^5.2.7", - "@fontsource/ibm-plex-mono": "^5.2.7", - "@fontsource/inter": "^5.2.8", - "@fontsource/jetbrains-mono": "^5.2.8", - "@fontsource/league-gothic": "^5.2.8", - "@fontsource/montserrat": "^5.2.8", - "@fontsource/nunito": "^5.2.7", - "@fontsource/oswald": "^5.2.8", - "@fontsource/outfit": "^5.2.8", - "@fontsource/space-mono": "^5.2.9", "@hono/node-server": "^2.0.5", "@hyperframes/core": "workspace:^", "@hyperframes/engine": "workspace:^", @@ -97,17 +85,9 @@ "linkedom": "^0.18.12", "postcss": "^8.4.0", "puppeteer": "^25.2.1", - "puppeteer-core": "^25.2.1", - "wawoff2": "^2.0.1" + "puppeteer-core": "^25.2.1" }, "devDependencies": { - "@fontsource/lato": "^5.2.7", - "@fontsource/noto-sans-jp": "^5.2.9", - "@fontsource/open-sans": "^5.2.7", - "@fontsource/playfair-display": "^5.2.8", - "@fontsource/poppins": "^5.2.7", - "@fontsource/roboto": "^5.2.10", - "@fontsource/source-code-pro": "^5.2.7", "@hyperframes/aws-lambda": "workspace:*", "@types/node": "^25.0.10", "@webgpu/types": "^0.1.69", diff --git a/packages/producer/scripts/test-classification.mjs b/packages/producer/scripts/test-classification.mjs index 6834d68ff5..57b205da19 100644 --- a/packages/producer/scripts/test-classification.mjs +++ b/packages/producer/scripts/test-classification.mjs @@ -14,7 +14,6 @@ const INTEGRATION_TEST_FILES = new Set([ "src/regression-harness-psnr.test.ts", "src/services/bundleScriptPolicy.test.ts", "src/services/coreRuntimeBrowser.test.ts", - "src/services/deterministicFonts-systemCapture.test.ts", "src/services/vfxDeterminism.test.ts", "src/services/distributed/assemble.test.ts", "src/services/distributed/chunkBoundary.test.ts", diff --git a/packages/producer/src/index.ts b/packages/producer/src/index.ts index ae9ad20660..e9b900e997 100644 --- a/packages/producer/src/index.ts +++ b/packages/producer/src/index.ts @@ -129,7 +129,7 @@ export { type FontFetchErrorCode, type FontFetchRetryPolicy, type InjectDeterministicFontFacesOptions, -} from "./services/deterministicFonts.js"; +} from "@hyperframes/core/fonts/embed"; export { quantizeTimeToFrame } from "./utils/parityContract.js"; export { resolveRenderPaths, type RenderPaths } from "./utils/paths.js"; diff --git a/packages/producer/src/services/distributed/plan.ts b/packages/producer/src/services/distributed/plan.ts index e6652bf808..93edeafa31 100644 --- a/packages/producer/src/services/distributed/plan.ts +++ b/packages/producer/src/services/distributed/plan.ts @@ -661,13 +661,13 @@ export function buildChunkSlices( * distributed pipeline needs it. */ async function readFontSnapshotSha(): Promise { - const module = (await import("../fontData.generated.js")) as { + const module = (await import("@hyperframes/core/fonts/embedded-data")) as { EMBEDDED_FONT_DATA?: unknown; }; const data = module.EMBEDDED_FONT_DATA; if (!data || typeof data !== "object") { throw new Error( - "[plan] EMBEDDED_FONT_DATA missing from fontData.generated.js — was `bun run build:fonts` run?", + "[plan] EMBEDDED_FONT_DATA missing from @hyperframes/core/fonts/embedded-data — was `bun run build:fonts` run?", ); } // Hash a canonical key fingerprint, not the raw font bytes — the bytes are diff --git a/packages/producer/src/services/htmlCompiler.ts b/packages/producer/src/services/htmlCompiler.ts index 9a186184b6..e603fd5858 100644 --- a/packages/producer/src/services/htmlCompiler.ts +++ b/packages/producer/src/services/htmlCompiler.ts @@ -88,7 +88,7 @@ import type { Page } from "puppeteer-core"; import { injectDeterministicFontFaces, normalizeSystemFontPrimaryFamilies, -} from "./deterministicFonts.js"; +} from "@hyperframes/core/fonts/embed"; import { prepareAnimatedGifInputs } from "./animatedGifPrep.js"; import { createStudioPositionSeekReapplyScript } from "@hyperframes/studio-server/manual-edits-render-script"; import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render"; @@ -1777,7 +1777,7 @@ async function readLocalFont(absPath: string): Promise { // fallow-ignore-next-line complexity async function embedLocalFontFaces(html: string, projectDir: string): Promise { - const { fontToDataUri: toDataUri } = await import("./fontCompression.js"); + const { fontToDataUri: toDataUri } = await import("@hyperframes/core/fonts/embed"); const styleBlockRe = /]*>([\s\S]*?)<\/style>/gi; const fontFaceRe = /@font-face\s*\{([^}]*)\}/gi; let result = html; @@ -2018,6 +2018,7 @@ export async function compileForRender( ); const coalescedHtml = await injectDeterministicFontFaces(normalizedFontHtml, { + logger: defaultLogger, failClosedFontFetch: options.failClosedFontFetch === true, allowSystemFontCapture: options.allowSystemFontCapture, abortSignal: options.abortSignal, diff --git a/packages/producer/src/services/render/planValidation.ts b/packages/producer/src/services/render/planValidation.ts index 0ad2f21742..3415938fff 100644 --- a/packages/producer/src/services/render/planValidation.ts +++ b/packages/producer/src/services/render/planValidation.ts @@ -11,7 +11,7 @@ import { GENERIC_FAMILIES, iterateFontFamilyDeclarations, resolveFontFamilyDeclarationFamilies, -} from "../deterministicFonts.js"; +} from "@hyperframes/core/fonts/embed"; /** * Re-export the BROWSER_GPU_NOT_SOFTWARE code so distributed adapters and @@ -25,7 +25,7 @@ export { BROWSER_GPU_NOT_SOFTWARE } from "@hyperframes/engine"; * @font-face injector consume the same surface, so the parser lives next to * the data. */ -export { parseFontFamilyValue } from "../deterministicFonts.js"; +export { parseFontFamilyValue } from "@hyperframes/core/fonts/embed"; /** * Typed plan-validation error. Workflow adapters key retry policies off the diff --git a/packages/studio/vite.adapter.ts b/packages/studio/vite.adapter.ts index 76c1815de0..3d445262df 100644 --- a/packages/studio/vite.adapter.ts +++ b/packages/studio/vite.adapter.ts @@ -282,8 +282,8 @@ export function createViteAdapter( }, async transformPreviewHtml({ html }) { - const producer = await import("../producer/src/services/deterministicFonts.js"); - return producer.injectDeterministicFontFaces(html); + const fonts = await import("../core/src/fonts/deterministicFonts.js"); + return fonts.injectDeterministicFontFaces(html); }, getProjectSignature(projectDir: string): string { diff --git a/scripts/test-reachability.json b/scripts/test-reachability.json index cc85cee8a9..19ca15cddb 100644 --- a/scripts/test-reachability.json +++ b/scripts/test-reachability.json @@ -61,7 +61,7 @@ ], "sources": { "packages/producer/scripts/run-test-lane.mjs": "bff02680f19eebcce0df72cdeedbbc05edfdcede3f3f60658fc0cb18ed8c59f6", - "packages/producer/scripts/test-classification.mjs": "e16151bbd3a3f9767e513fdfb1155fad286855e1b9ffb077ace80d3fe900bd16", + "packages/producer/scripts/test-classification.mjs": "1c0490f70042787a081a47ffe70fb8688785015cbf50fbbce712ba1fa1cca889", ".github/workflows/ci.yml#job#producer-source-tests": "45da72aa6cf08cdfd5bcf5311e89f12af4842f9eebc6735df19da8c02f4d4864", "packages/producer/package.json#script#test:unit": "ae6beda8319d166774be958ea5593cd27dbf7f6c9bce1d787a3fe28dfc3477b1", "packages/producer/package.json#script#test:integration": "008589f37d026877595a13501cbb66abd5cbe50e21fdcac6904f45466c3f475d",