Files
hyperframes/packages/core/src/fonts/systemFontLocator.ts
T
Miguel Ángel 6f677292ae refactor(core): simplify packages/core — dead code, dedup, type safety (#1413)
- Delete unused mediaPreloader module, 5 dead RuntimeState fields,
  emitPerformanceMetric, lintScriptUrls, 5 variable type guards
- Consolidate compiler utilities: unify CSS URL regex, relative URL
  predicate, MIME map, @import regex, bulk asset rewrite delegation
- Cache extractGsapWindows per script (eliminates 2 redundant recast
  parses per lint run), share stripJsComments and script extraction
- Deduplicate GSAP parser: share serializeValue/safeJsKey, centralize
  converted-id fallback (6 sites), keyframe codegen (3 sites),
  waypoint extraction, insert-after-anchor, script hoisting
- Replace 88 bare any annotations with typed AstNode/AstPath interfaces
- Derive RuntimeBridgeControlAction from HyperframeControlAction,
  alias RuntimePickerElementInfo, share macOS font profiler
- Gate generateHyperframesStyles on includeStyles, collapse 4 GSAP
  property mutation cases into 2
- Extract magic numbers into named constants, replace 5 double casts
  with type guards and typed accessors (runtime/globals.ts),
  reduce function complexity in htmlParser and files route
2026-06-13 18:23:36 -04:00

418 lines
12 KiB
TypeScript

import { execFileSync } from "node:child_process";
import { existsSync, lstatSync, readdirSync, realpathSync } from "node:fs";
import { homedir, platform } from "node:os";
import { join, resolve } from "node:path";
export const SYSTEM_FONT_SIZE_LIMIT = 5 * 1024 * 1024;
const PROFILER_TIMEOUT_MS = 5000;
const FC_MATCH_TIMEOUT_MS = 3000;
export type FontFileFormat = "ttf" | "otf" | "woff2" | "woff" | "ttc";
export interface LocatedFont {
path: string;
format: FontFileFormat;
}
export const FONT_EXT_RE = /\.(otf|ttf|ttc|woff2?)$/i;
const FORMAT_PRIORITY: Record<FontFileFormat, number> = {
woff2: 0,
otf: 1,
ttf: 2,
woff: 3,
ttc: 4,
};
const STYLE_SUFFIXES = new Set([
"black",
"bold",
"book",
"condensed",
"demi",
"demibold",
"display",
"extra",
"extrabold",
"hairline",
"heavy",
"italic",
"light",
"medium",
"normal",
"regular",
"roman",
"semibold",
"thin",
"ultra",
"ultralight",
]);
const REGULAR_TOKENS = new Set(["regular", "roman", "normal", "book"]);
const cache = new Map<string, LocatedFont | null>();
let allowedDirsCache: string[] | null = null;
function getAllowedFontDirs(): string[] {
if (allowedDirsCache) return allowedDirsCache;
allowedDirsCache = fontDirectories()
.filter((d) => existsSync(d))
.map((d) => {
try {
return realpathSync(d);
} catch {
return resolve(d);
}
});
return allowedDirsCache;
}
function isPathBounded(filePath: string): boolean {
try {
const real = realpathSync(filePath);
const allowed = getAllowedFontDirs();
return allowed.some((dir) => real.startsWith(dir + "/") || real.startsWith(dir + "\\"));
} catch {
return false;
}
}
function isRegularFile(filePath: string): boolean {
try {
const lst = lstatSync(filePath);
if (lst.isSymbolicLink())
return isPathBounded(filePath) && lstatSync(realpathSync(filePath)).isFile();
return lst.isFile();
} catch {
return false;
}
}
function normalizeName(name: string): string {
return name
.trim()
.replace(/^['"]|['"]$/g, "")
.trim()
.toLowerCase();
}
function extensionToFormat(ext: string): FontFileFormat {
const lower = ext.toLowerCase().replace(/^\./, "");
if (lower === "woff2") return "woff2";
if (lower === "woff") return "woff";
if (lower === "otf") return "otf";
if (lower === "ttc") return "ttc";
return "ttf";
}
export function toFamilyName(fileName: string): string | null {
const withoutExt = fileName.replace(FONT_EXT_RE, "");
if (!withoutExt || withoutExt.startsWith(".")) return null;
const spaced = withoutExt
.replace(/[_-]+/g, " ")
.replace(/([a-z])([A-Z])/g, "$1 $2")
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
.replace(/\s+/g, " ")
.trim();
const words = spaced.split(" ").filter(Boolean);
while (words.length > 1 && STYLE_SUFFIXES.has((words.at(-1) ?? "").toLowerCase())) {
words.pop();
}
const family = words.join(" ").trim();
return family.length >= 2 ? family : null;
}
function isRegularWeight(fileName: string): boolean {
const lower = fileName.toLowerCase();
if (REGULAR_TOKENS.has(lower.replace(FONT_EXT_RE, "").split(/[-_ ]/).pop() ?? "")) return true;
return !lower.includes("bold") && !lower.includes("italic") && !lower.includes("light");
}
export function fontDirectories(): string[] {
const home = homedir();
if (platform() === "darwin") {
return [
join(home, "Library", "Fonts"),
"/Library/Fonts",
"/System/Library/Fonts",
"/System/Library/Fonts/Supplemental",
];
}
if (platform() === "win32") {
return [
join(process.env.WINDIR || "C:\\Windows", "Fonts"),
join(
process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"),
"Microsoft",
"Windows",
"Fonts",
),
];
}
return [
join(home, ".fonts"),
join(home, ".local", "share", "fonts"),
"/usr/local/share/fonts",
"/usr/share/fonts",
];
}
export interface FontFileEntry {
path: string;
fileName: string;
family: string;
}
/**
* Iterates font files in a directory (up to `depth` 2) and yields each file's
* path, filename, and derived family name. Shared by the font listing route
* and the per-family locator to avoid duplicating the directory scan loop.
*/
export function collectFontFileEntries(dir: string, depth = 0): FontFileEntry[] {
if (!existsSync(dir) || depth > 2) return [];
const entries: FontFileEntry[] = [];
try {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
entries.push(...collectFontFileEntries(fullPath, depth + 1));
continue;
}
if (!FONT_EXT_RE.test(entry.name)) continue;
if (!isRegularFile(fullPath)) continue;
const family = toFamilyName(entry.name);
if (family) entries.push({ path: fullPath, fileName: entry.name, family });
}
} catch {
// Directory read failed — skip
}
return entries;
}
interface FontCandidate {
path: string;
format: FontFileFormat;
isRegular: boolean;
}
function collectCandidatesFromDir(dir: string, targetFamily: string, depth = 0): FontCandidate[] {
return collectFontFileEntries(dir, depth)
.filter((e) => normalizeName(e.family) === targetFamily)
.map((e) => {
const ext = e.fileName.match(FONT_EXT_RE)?.[1] ?? "ttf";
return {
path: e.path,
format: extensionToFormat(ext),
isRegular: isRegularWeight(e.fileName),
};
});
}
function pickBestCandidate(candidates: FontCandidate[]): LocatedFont | null {
if (candidates.length === 0) return null;
candidates.sort((a, b) => {
if (a.isRegular !== b.isRegular) return a.isRegular ? -1 : 1;
return (FORMAT_PRIORITY[a.format] ?? 9) - (FORMAT_PRIORITY[b.format] ?? 9);
});
const best = candidates[0]!;
return { path: best.path, format: best.format };
}
type SystemProfilerEntry = {
family: string;
path: string;
format: FontFileFormat;
isRegular: boolean;
};
let profilerCache: Map<string, SystemProfilerEntry[]> | null = null;
// fallow-ignore-next-line complexity
function getSystemProfilerIndex(): Map<string, SystemProfilerEntry[]> {
if (profilerCache) return profilerCache;
profilerCache = new Map();
if (platform() !== "darwin") return profilerCache;
try {
const raw = execFileSync("system_profiler", ["SPFontsDataType", "-json"], {
encoding: "utf8",
maxBuffer: 12 * 1024 * 1024,
timeout: PROFILER_TIMEOUT_MS,
});
const parsed = JSON.parse(raw);
if (!parsed?.SPFontsDataType || !Array.isArray(parsed.SPFontsDataType)) return profilerCache;
for (const fontEntry of parsed.SPFontsDataType) {
if (!fontEntry?.typefaces || !Array.isArray(fontEntry.typefaces)) continue;
for (const typeface of fontEntry.typefaces) {
if (!typeface) continue;
const family = typeface.family ?? typeface.fullname ?? typeface._name;
if (typeof family !== "string") continue;
const filePath = typeface.path;
if (typeof filePath !== "string" || !FONT_EXT_RE.test(filePath)) continue;
const normalized = normalizeName(family);
const ext = filePath.match(FONT_EXT_RE)?.[1] ?? "ttf";
const entry: SystemProfilerEntry = {
family: normalized,
path: filePath,
format: extensionToFormat(ext),
isRegular: isRegularWeight(filePath),
};
const list = profilerCache.get(normalized) ?? [];
list.push(entry);
profilerCache.set(normalized, list);
}
}
} catch {
// system_profiler unavailable
}
return profilerCache;
}
function locateViaSystemProfiler(targetFamily: string): LocatedFont | null {
const index = getSystemProfilerIndex();
const entries = index.get(targetFamily);
if (!entries || entries.length === 0) return null;
const candidates: FontCandidate[] = entries
.filter((e) => isRegularFile(e.path) && isPathBounded(e.path))
.map((e) => ({ path: e.path, format: e.format, isRegular: e.isRegular }));
return pickBestCandidate(candidates);
}
// fallow-ignore-next-line complexity
function locateViaFcMatch(targetFamily: string): LocatedFont | null {
if (platform() !== "linux") return null;
try {
const result = execFileSync("fc-match", [targetFamily, "--format=%{file}"], {
encoding: "utf8",
timeout: FC_MATCH_TIMEOUT_MS,
}).trim();
if (!result || !isRegularFile(result) || !isPathBounded(result)) return null;
const fileName = result.split("/").pop() ?? "";
const derivedFamily = toFamilyName(fileName);
if (!derivedFamily || normalizeName(derivedFamily) !== targetFamily) return null;
const ext = fileName.match(FONT_EXT_RE)?.[1] ?? "ttf";
return { path: result, format: extensionToFormat(ext) };
} catch {
return null;
}
}
export function locateSystemFont(family: string): LocatedFont | null {
const normalized = normalizeName(family);
if (!normalized) return null;
const cached = cache.get(normalized);
if (cached !== undefined) return cached;
let result: LocatedFont | null = null;
result = locateViaSystemProfiler(normalized);
if (!result) {
result = locateViaFcMatch(normalized);
}
if (!result) {
const allCandidates: FontCandidate[] = [];
for (const dir of fontDirectories()) {
allCandidates.push(...collectCandidatesFromDir(dir, normalized));
}
result = pickBestCandidate(allCandidates);
}
cache.set(normalized, result);
return result;
}
export interface LocatedFontVariant extends LocatedFont {
weight: string;
style: "normal" | "italic";
}
const WEIGHT_TOKENS: Record<string, string> = {
thin: "100",
hairline: "100",
ultralight: "200",
extralight: "200",
light: "300",
regular: "400",
normal: "400",
book: "400",
roman: "400",
medium: "500",
demibold: "600",
semibold: "600",
bold: "700",
extrabold: "800",
ultrabold: "800",
heavy: "800",
black: "900",
ultrablack: "950",
};
const WEIGHT_TOKENS_SORTED = Object.entries(WEIGHT_TOKENS).sort(([a], [b]) => b.length - a.length);
function inferWeightAndStyle(fileName: string): { weight: string; style: "normal" | "italic" } {
const lower = fileName.toLowerCase().replace(FONT_EXT_RE, "");
const style = lower.includes("italic") || lower.includes("oblique") ? "italic" : "normal";
for (const [token, weight] of WEIGHT_TOKENS_SORTED) {
if (lower.includes(token)) return { weight, style };
}
return { weight: "400", style };
}
export function locateSystemFontVariants(family: string): LocatedFontVariant[] {
const normalized = normalizeName(family);
if (!normalized) return [];
const variants: LocatedFontVariant[] = [];
const profilerIndex = getSystemProfilerIndex();
const profilerEntries = profilerIndex.get(normalized);
if (profilerEntries && profilerEntries.length > 0) {
for (const e of profilerEntries) {
if (!isRegularFile(e.path) || !isPathBounded(e.path)) continue;
const { weight, style } = inferWeightAndStyle(e.path);
variants.push({ path: e.path, format: e.format, weight, style });
}
if (variants.length > 0) return dedupeVariants(variants);
}
const allCandidates: FontCandidate[] = [];
for (const dir of fontDirectories()) {
allCandidates.push(...collectCandidatesFromDir(dir, normalized));
}
for (const c of allCandidates) {
const { weight, style } = inferWeightAndStyle(c.path);
variants.push({ path: c.path, format: c.format, weight, style });
}
return dedupeVariants(variants);
}
function dedupeVariants(variants: LocatedFontVariant[]): LocatedFontVariant[] {
const seen = new Map<string, LocatedFontVariant>();
for (const v of variants) {
const key = `${v.weight}:${v.style}`;
if (!seen.has(key)) seen.set(key, v);
}
return Array.from(seen.values());
}
export function getSystemProfilerFamilies(): string[] {
const index = getSystemProfilerIndex();
return Array.from(index.keys());
}
export function clearSystemFontCache(): void {
cache.clear();
profilerCache = null;
allowedDirsCache = null;
}