Skip to content

Commit 2061154

Browse files
fix(core): font lookup on a Mac no longer waits 5 s on system_profiler (#5810)
1 parent 1a2194c commit 2061154

5 files changed

Lines changed: 25 additions & 98 deletions

File tree

‎packages/core/src/fonts/deterministicFonts-systemCapture.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ describe("system font capture — allowSystemFontCapture option", () => {
6363
// allowSystemFontCapture intentionally omitted
6464
});
6565
expect(result.includes("data-hyperframes-deterministic-fonts")).toBe(false);
66-
}, 15_000); // The system font locator runs system_profiler on macOS which can take >5s.
66+
});
6767
});
6868

6969
describe("system font capture — integration (macOS only)", () => {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
const execFileSync = vi.hoisted(() => vi.fn(() => ""));
4+
vi.mock("node:child_process", async (importOriginal) => ({
5+
...(await importOriginal<typeof import("node:child_process")>()),
6+
execFileSync,
7+
}));
8+
vi.mock("node:os", async (importOriginal) => ({
9+
...(await importOriginal<typeof import("node:os")>()),
10+
platform: () => "darwin",
11+
}));
12+
13+
const { locateSystemFont, locateSystemFontVariants } = await import("./systemFontLocator");
14+
15+
describe("systemFontLocator on macOS", () => {
16+
// system_profiler SPFontsDataType takes about 10 s, so every lookup would block a compile for its timeout.
17+
it("finds fonts without spawning a process", () => {
18+
locateSystemFontVariants("Hf Darwin Probe");
19+
locateSystemFont("Hf Darwin Probe");
20+
expect(execFileSync).not.toHaveBeenCalled();
21+
});
22+
});

‎packages/core/src/fonts/systemFontLocator.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
SYSTEM_FONT_SIZE_LIMIT,
66
} from "./systemFontLocator";
77

8-
describe("systemFontLocator", { timeout: 15_000 }, () => {
8+
describe("systemFontLocator", () => {
99
beforeAll(() => {
1010
clearSystemFontCache();
1111
});

‎packages/core/src/fonts/systemFontLocator.ts‎

Lines changed: 1 addition & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { homedir, platform } from "node:os";
44
import { join, resolve } from "node:path";
55

66
export const SYSTEM_FONT_SIZE_LIMIT = 5 * 1024 * 1024;
7-
const PROFILER_TIMEOUT_MS = 5000;
87
const FC_MATCH_TIMEOUT_MS = 3000;
98

109
export type FontFileFormat = "ttf" | "otf" | "woff2" | "woff" | "ttc";
@@ -221,70 +220,6 @@ function pickBestCandidate(candidates: FontCandidate[]): LocatedFont | null {
221220
return { path: best.path, format: best.format };
222221
}
223222

224-
type SystemProfilerEntry = {
225-
family: string;
226-
path: string;
227-
format: FontFileFormat;
228-
isRegular: boolean;
229-
};
230-
231-
let profilerCache: Map<string, SystemProfilerEntry[]> | null = null;
232-
233-
// fallow-ignore-next-line complexity
234-
function getSystemProfilerIndex(): Map<string, SystemProfilerEntry[]> {
235-
if (profilerCache) return profilerCache;
236-
profilerCache = new Map();
237-
if (platform() !== "darwin") return profilerCache;
238-
239-
try {
240-
const raw = execFileSync("system_profiler", ["SPFontsDataType", "-json"], {
241-
encoding: "utf8",
242-
maxBuffer: 12 * 1024 * 1024,
243-
timeout: PROFILER_TIMEOUT_MS,
244-
});
245-
const parsed = JSON.parse(raw);
246-
if (!parsed?.SPFontsDataType || !Array.isArray(parsed.SPFontsDataType)) return profilerCache;
247-
248-
for (const fontEntry of parsed.SPFontsDataType) {
249-
if (!fontEntry?.typefaces || !Array.isArray(fontEntry.typefaces)) continue;
250-
for (const typeface of fontEntry.typefaces) {
251-
if (!typeface) continue;
252-
const family = typeface.family ?? typeface.fullname ?? typeface._name;
253-
if (typeof family !== "string") continue;
254-
const filePath = typeface.path;
255-
if (typeof filePath !== "string" || !FONT_EXT_RE.test(filePath)) continue;
256-
const normalized = normalizeName(family);
257-
const ext = filePath.match(FONT_EXT_RE)?.[1] ?? "ttf";
258-
const entry: SystemProfilerEntry = {
259-
family: normalized,
260-
path: filePath,
261-
format: extensionToFormat(ext),
262-
isRegular: isRegularWeight(filePath),
263-
};
264-
const list = profilerCache.get(normalized) ?? [];
265-
list.push(entry);
266-
profilerCache.set(normalized, list);
267-
}
268-
}
269-
} catch {
270-
// system_profiler unavailable
271-
}
272-
273-
return profilerCache;
274-
}
275-
276-
function locateViaSystemProfiler(targetFamily: string): LocatedFont | null {
277-
const index = getSystemProfilerIndex();
278-
const entries = index.get(targetFamily);
279-
if (!entries || entries.length === 0) return null;
280-
281-
const candidates: FontCandidate[] = entries
282-
.filter((e) => isRegularFile(e.path) && isPathBounded(e.path))
283-
.map((e) => ({ path: e.path, format: e.format, isRegular: e.isRegular }));
284-
285-
return pickBestCandidate(candidates);
286-
}
287-
288223
// fallow-ignore-next-line complexity
289224
function locateViaFcMatch(targetFamily: string): LocatedFont | null {
290225
if (platform() !== "linux") return null;
@@ -311,13 +246,7 @@ export function locateSystemFont(family: string): LocatedFont | null {
311246
const cached = cache.get(normalized);
312247
if (cached !== undefined) return cached;
313248

314-
let result: LocatedFont | null = null;
315-
316-
result = locateViaSystemProfiler(normalized);
317-
318-
if (!result) {
319-
result = locateViaFcMatch(normalized);
320-
}
249+
let result = locateViaFcMatch(normalized);
321250

322251
if (!result) {
323252
const allCandidates: FontCandidate[] = [];
@@ -373,18 +302,6 @@ export function locateSystemFontVariants(family: string): LocatedFontVariant[] {
373302
if (!normalized) return [];
374303

375304
const variants: LocatedFontVariant[] = [];
376-
377-
const profilerIndex = getSystemProfilerIndex();
378-
const profilerEntries = profilerIndex.get(normalized);
379-
if (profilerEntries && profilerEntries.length > 0) {
380-
for (const e of profilerEntries) {
381-
if (!isRegularFile(e.path) || !isPathBounded(e.path)) continue;
382-
const { weight, style } = inferWeightAndStyle(e.path);
383-
variants.push({ path: e.path, format: e.format, weight, style });
384-
}
385-
if (variants.length > 0) return dedupeVariants(variants);
386-
}
387-
388305
const allCandidates: FontCandidate[] = [];
389306
for (const dir of fontDirectories()) {
390307
allCandidates.push(...collectCandidatesFromDir(dir, normalized));
@@ -405,13 +322,7 @@ function dedupeVariants(variants: LocatedFontVariant[]): LocatedFontVariant[] {
405322
return Array.from(seen.values());
406323
}
407324

408-
export function getSystemProfilerFamilies(): string[] {
409-
const index = getSystemProfilerIndex();
410-
return Array.from(index.keys());
411-
}
412-
413325
export function clearSystemFontCache(): void {
414326
cache.clear();
415-
profilerCache = null;
416327
allowedDirsCache = null;
417328
}

‎packages/studio-server/src/routes/fonts.ts‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type { Hono } from "hono";
33
import {
44
collectFontFileEntries,
55
fontDirectories,
6-
getSystemProfilerFamilies,
76
locateSystemFont,
87
SYSTEM_FONT_SIZE_LIMIT,
98
} from "@hyperframes/core/fonts/system-locator";
@@ -49,11 +48,6 @@ function listInstalledFontFamilies(): string[] {
4948
if (cachedFonts) return cachedFonts;
5049
const families = new Set<string>();
5150

52-
for (const family of getSystemProfilerFamilies()) {
53-
families.add(family);
54-
if (families.size >= MAX_FONT_RESULTS) break;
55-
}
56-
5751
for (const dir of fontDirectories()) {
5852
for (const family of collectFontsFromDir(dir)) {
5953
families.add(family);

0 commit comments

Comments
 (0)