Files
hyperframes/packages/producer/scripts/generate-font-data.ts
Miguel ÁngelandClaude Opus 4.6 7294803fbc feat(fonts): add Playfair Display, Noto Sans JP, Roboto, and 4 more to deterministic font database (#196)
* fix(engine): suppress font-loading 404 noise in render console output

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(engine): downgrade resource 404s to buffer-only instead of suppressing

Address review feedback: instead of silently dropping "Failed to load
resource" errors (which could hide real asset failures), keep them in
browserConsoleBuffer for diagnostics but don't print to stdout. Real
asset 404s are still caught by the file server's own logging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(engine): narrow 404 filter to font CDN domains and woff2 files only

Address review: filter was too broad and could suppress real asset
failures. Now only suppresses 404s matching fonts.googleapis,
fonts.gstatic, or .woff2 file extensions. Missing images, scripts,
and videos will still surface as [Browser:ERROR] in render output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(fonts): add Playfair Display, Noto Sans JP, Roboto, and 4 more to deterministic font database

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:32:08 -07:00

170 lines
5.3 KiB
TypeScript

/**
* Generate embedded font data for deterministic font injection.
*
* Reads woff2 files from @fontsource/* packages at build time and produces
* a TypeScript module with base64 data URIs. This eliminates the runtime
* dependency on @fontsource packages, making the CLI self-contained when
* bundled via tsup.
*
* Usage: tsx scripts/generate-font-data.ts
*/
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const require = createRequire(import.meta.url);
const __dirname = dirname(fileURLToPath(import.meta.url));
type FontFaceSpec = { weight: string; style?: "normal" | "italic" };
type CanonicalFontSpec = { packageName: string; faces: FontFaceSpec[] };
// Mirror of CANONICAL_FONTS from deterministicFonts.ts — single source of truth
const CANONICAL_FONTS: Record<string, CanonicalFontSpec> = {
inter: {
packageName: "@fontsource/inter",
faces: [{ weight: "400" }, { weight: "700" }, { weight: "900" }],
},
montserrat: {
packageName: "@fontsource/montserrat",
faces: [{ weight: "400" }, { weight: "700" }, { weight: "900" }],
},
outfit: {
packageName: "@fontsource/outfit",
faces: [{ weight: "400" }, { weight: "700" }, { weight: "900" }],
},
nunito: {
packageName: "@fontsource/nunito",
faces: [{ weight: "400" }, { weight: "700" }, { weight: "900" }],
},
oswald: {
packageName: "@fontsource/oswald",
faces: [{ weight: "400" }, { weight: "700" }],
},
"league-gothic": {
packageName: "@fontsource/league-gothic",
faces: [{ weight: "400" }],
},
"archivo-black": {
packageName: "@fontsource/archivo-black",
faces: [{ weight: "400" }],
},
"space-mono": {
packageName: "@fontsource/space-mono",
faces: [{ weight: "400" }, { weight: "700" }],
},
"ibm-plex-mono": {
packageName: "@fontsource/ibm-plex-mono",
faces: [{ weight: "400" }, { weight: "700" }],
},
"jetbrains-mono": {
packageName: "@fontsource/jetbrains-mono",
faces: [{ weight: "400" }, { weight: "700" }],
},
"eb-garamond": {
packageName: "@fontsource/eb-garamond",
faces: [{ weight: "400" }, { weight: "700" }],
},
"playfair-display": {
packageName: "@fontsource/playfair-display",
faces: [{ weight: "400" }, { weight: "700" }, { weight: "900" }],
},
"source-code-pro": {
packageName: "@fontsource/source-code-pro",
faces: [{ weight: "400" }, { weight: "700" }],
},
"noto-sans-jp": {
packageName: "@fontsource/noto-sans-jp",
faces: [{ weight: "400" }, { weight: "700" }],
},
roboto: {
packageName: "@fontsource/roboto",
faces: [{ weight: "400" }, { weight: "700" }, { weight: "900" }],
},
"open-sans": {
packageName: "@fontsource/open-sans",
faces: [{ weight: "400" }, { weight: "700" }],
},
lato: {
packageName: "@fontsource/lato",
faces: [{ weight: "400" }, { weight: "700" }, { weight: "900" }],
},
poppins: {
packageName: "@fontsource/poppins",
faces: [{ weight: "400" }, { weight: "700" }, { weight: "900" }],
},
};
function packageRoot(packageName: string): string {
const packageJsonPath = require.resolve(`${packageName}/package.json`);
return dirname(packageJsonPath);
}
function resolveFontFile(
packageName: string,
weight: string,
style: "normal" | "italic" = "normal",
): string {
const root = packageRoot(packageName);
const filesDir = join(root, "files");
const slug = packageName.replace("@fontsource/", "");
const files = readdirSync(filesDir);
const exact = `${slug}-latin-${weight}-${style}.woff2`;
if (files.includes(exact)) {
return join(filesDir, exact);
}
const relaxed = files.find((file) => {
return file.endsWith(`-${weight}-${style}.woff2`) && file.includes("-latin-");
});
if (relaxed) {
return join(filesDir, relaxed);
}
throw new Error(`No font asset found for ${packageName} weight=${weight} style=${style}`);
}
function main() {
const entries: Array<{ key: string; dataUri: string }> = [];
let totalBytes = 0;
for (const [, spec] of Object.entries(CANONICAL_FONTS)) {
for (const face of spec.faces) {
const style = face.style || "normal";
const key = `${spec.packageName}:${face.weight}:${style}`;
const fontPath = resolveFontFile(spec.packageName, face.weight, style);
const content = readFileSync(fontPath);
totalBytes += content.length;
const dataUri = `data:font/woff2;base64,${content.toString("base64")}`;
entries.push({ key, dataUri });
}
}
const lines = [
"/**",
" * AUTO-GENERATED — do not edit manually.",
` * Generated by: scripts/generate-font-data.ts`,
` * ${entries.length} font faces, ${Math.round(totalBytes / 1024)}KB raw woff2`,
" */",
"",
"export const EMBEDDED_FONT_DATA: ReadonlyMap<string, string> = new Map([",
];
for (const entry of entries) {
lines.push(` ["${entry.key}", "${entry.dataUri}"],`);
}
lines.push("]);");
lines.push("");
const outputPath = resolve(__dirname, "../src/services/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}`,
);
}
main();