Files
hyperframes/packages/core/src/fonts/systemFontLocator.test.ts
T
Miguel Ángel 0bf15119f8 feat: font resolution pipeline — compositions capture and embed their own fonts (#1255)
Compositions are now self-contained: the compiler captures font files
and embeds them as woff2 data URIs, eliminating silent render-time
fallback when the render environment lacks the author's fonts.

Resolution order (each tier falls through to the next):
1. Existing @font-face → use as-is
2. Bundled alias (38 cross-platform mappings) → embed data URI
3. Google Fonts → fetch, cache, embed
4. Local system font → locate on OS, compress to woff2, embed
5. Local @font-face paths → read file, compress, inline as data URI
6. External CDN stylesheets → fetch CSS, extract @font-face, inline
7. Alias map fallback → closest bundled equivalent
8. Actionable error with guidance

Key changes:
- System font locator (macOS/Windows/Linux) with path-bounding and
  symlink defense (realpathSync + O_NOFOLLOW)
- woff2 compression via wawoff2 (WASM, cross-platform)
- Multi-weight/style variant capture with length-sorted token matching
- External stylesheet inlining with SSRF defense (assertPublicHttpsUrl,
  HTTPS-only, private-host blocking, 2MB cap, 4-concurrent limit)
- Studio auto-import via GET /fonts/file API + renderAliasFor() derived
  from shared FONT_ALIAS_MAP (no more hand-curated drift)
- failClosedFontFetch throws on unresolved fonts in distributed renders
- Single source of truth: @hyperframes/core/fonts/aliases
- system_font_will_alias lint rule (escalates to warning for distributed)
- Default to Inter + JetBrains Mono in templates and CSS reset
2026-06-07 17:52:19 -04:00

79 lines
2.3 KiB
TypeScript

import { describe, it, expect, beforeAll } from "vitest";
import {
locateSystemFont,
clearSystemFontCache,
SYSTEM_FONT_SIZE_LIMIT,
} from "./systemFontLocator";
describe("systemFontLocator", { timeout: 15_000 }, () => {
beforeAll(() => {
clearSystemFontCache();
});
it("returns null for nonexistent fonts", () => {
const result = locateSystemFont("nonexistent-font-xyz-12345");
expect(result).toBeNull();
});
it("normalizes case when looking up fonts", () => {
const lower = locateSystemFont("helvetica");
const upper = locateSystemFont("HELVETICA");
if (lower === null) {
expect(upper).toBeNull();
} else {
expect(upper).not.toBeNull();
expect(upper!.path).toBe(lower.path);
}
});
it("returns a valid format field when a font is found", () => {
const result =
locateSystemFont("Helvetica") ?? locateSystemFont("Arial") ?? locateSystemFont("DejaVu Sans");
if (result) {
expect(["ttf", "otf", "woff2", "woff", "ttc"]).toContain(result.format);
expect(result.path).toBeTruthy();
}
});
it("caches results across calls", () => {
const first = locateSystemFont("nonexistent-font-xyz-12345");
const second = locateSystemFont("nonexistent-font-xyz-12345");
expect(first).toBeNull();
expect(second).toBeNull();
});
it("clearSystemFontCache resets the cache", () => {
locateSystemFont("nonexistent-font-xyz-12345");
clearSystemFontCache();
const result = locateSystemFont("nonexistent-font-xyz-12345");
expect(result).toBeNull();
});
it("exports SYSTEM_FONT_SIZE_LIMIT as 5MB", () => {
expect(SYSTEM_FONT_SIZE_LIMIT).toBe(5 * 1024 * 1024);
});
it("strips quotes from family name input", () => {
const result = locateSystemFont('"nonexistent-font-xyz-12345"');
expect(result).toBeNull();
});
it("returns null for empty string", () => {
expect(locateSystemFont("")).toBeNull();
expect(locateSystemFont(" ")).toBeNull();
});
if (process.platform === "darwin") {
it("finds Helvetica on macOS", () => {
const result = locateSystemFont("Helvetica");
expect(result).not.toBeNull();
expect(result!.path).toMatch(/\.(ttf|ttc|otf)$/i);
});
it("finds Courier on macOS", () => {
const result = locateSystemFont("Courier");
expect(result).not.toBeNull();
});
}
});