Repository navigation
Description
Describe the bug
Two defects in the local-font embedding pass in packages/producer/src/services/htmlCompiler.ts.
The second is the serious one: an unanchored String.replaceAll over the entire compiled
document can corrupt unrelated URLs.
1. file: is not excluded from the local-font pattern
// packages/producer/src/services/htmlCompiler.ts:1658
const LOCAL_FONTFACE_URL_RE = /url\(["']?(?!data:|https?:\/\/)([^"')]+)["']?\)/gi;The negative lookahead skips data: and http(s):// but not file:. So a
@font-face { src: url("file:///abs/path/font.ttf") } is classified as a local project
path and passed to resolve(projectDir, localPath). Since the captured string starts with
f, not /, it is treated as relative and resolved into a path like
<projectDir>/file:/abs/path/font.ttf, which does not exist.
The subsequent read throws into an empty catch (line 1731):
} catch {
// File read or compression failed — keep the original path
}so the failure is silent, the rule is left untouched, and the browser then refuses the
file:// fetch with net::ERR_FAILED. The composition renders with the CSS fallback font
and render exits 0.
2. The embed replaces by bare substring across the whole document
// packages/producer/src/services/htmlCompiler.ts:1729
result = result.replaceAll(localPath, dataUri);localPath here is a bare relative path such as fonts/testfont.ttf, with no surrounding
url(...) or quotes. Every other replacement in this file anchors on the surrounding
syntax:
// :1310
result = result.replaceAll(`"${url}"`, `"${relPath}"`).replaceAll(`'${url}'`, `'${relPath}'`);
// :1434
(h, url, rel) => h.replaceAll(`url(${url})`, `url(${rel})`),
// :1654
(h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`),Line 1729 is the only unanchored one, so it matches that path text anywhere in the
compiled HTML — including inside a longer URL that merely ends with the same characters.
Concretely: a document containing both
url("fonts/testfont.ttf")— correctly embedded, andurl("file:///abs/path/fonts/testfont.ttf")— the untouched rule from defect 1
produces, after the global replace, a corrupted second rule of the form
url("file:///abs/path/data:font/woff2;base64,..."). This was observed in the compiled
debug output under .debug/<jobId>/compiled/index.html while investigating defect 1.
The collision does not require file:// to be involved. Any two paths where one is a
trailing substring of the other — img/logo.png and assets/img/logo.png, a/font.ttf
and b/a/font.ttf — are enough for the shorter one's embed to rewrite text inside the
longer one.
Expected behavior
An absolute file:// font src is either embedded or left alone, and either way the
compiler does not modify text belonging to a different URL.
Actual behavior
The file:// rule is silently skipped, and if a relative font with a colliding path
suffix is embedded in the same document, the file:// rule is rewritten into a URL that
is neither the original nor a valid data URI.
Suggested fix
Both are small and independent:
- Add
file:to the lookahead inLOCAL_FONTFACE_URL_RE, so an absolutefile://src is
not mistaken for a project-relative path. Consider also surfacing the swallowed failure
at line 1731 — an empty catch on a font read means a composition can silently render
with the wrong typeface. - Replace by matched
url()occurrence rather than by bare substring, matching what
lines 1310, 1434 and 1654 already do. The regex at line 1699 already yields match
positions, so the anchored form is available without new parsing.
Fix 2 matters more than the file:// case that surfaced it: today the blast radius is any
project where one font path is a suffix of another.
Environment
hyperframes 0.8.4
Affected packages/producer/src/services/htmlCompiler.ts:1658, 1729, 1731
Activity
fix(producer): anchor local-font embedding to its url() occurrence (#…