Fonts
Generate font-face CSS while keeping binary hosting and browser lifecycle explicit.
Register definitions#
import { createFontRegistry } from '@zeruxjs/font';
const fonts = createFontRegistry();
fonts.add('regular', { family: 'Inter', sources: [
{ url: '/fonts/inter.woff2', format: 'woff2' }
] });
const css = fonts.css();The registry creates CSS from trusted definitions. It does not fetch fonts, bundle binaries or verify licensing. Host the selected files under an intentional cache/CSP policy.
Stylesheet endpoint#
createFontStylesheetHandler from @zeruxjs/font/http returns a Fetch handler with ETag/HEAD support, cache lifetimes and optional bounded preload Link headers. Preload only fonts needed for the initial page to avoid wasting network bandwidth.
Browser loading#
loadFont returns false when no DOM/FontFaceSet exists. Native failure, invalid options, cancellation and timeout reject. Keep imports separate from invoking DOM-only helpers such as injectFontStyles. Include explicit fallback families and test text visibility with actual fonts; fixtures do not certify rendering quality.
Install and select the integration#
npm install @zeruxjs/font@0.0.4There is no automatic font startup service in zerux.config.js for this package. Import its public APIs and create the object or browser controller in the application owner that needs it. If a config object holds custom settings, your plugin/application code must explicitly consume them. See configuration integration boundaries and TypeScript.
A practical integration workflow#
Register trusted font definitions, host actual font binaries separately and generate font-face CSS from the registry. Preload only the weights/styles needed immediately; preloading every face can compete with critical resources. A registry entry does not download, subset, optimize or license a font file.
TypeScript: inputs, results and ownership#
import { createFontRegistry } from '@zeruxjs/font';
type Inputs = Parameters<typeof createFontRegistry>;
type Result = Awaited<ReturnType<typeof createFontRegistry>>;Inputs is the function's complete argument tuple, including optional parameters. Result removes a Promise when the function is asynchronous. These aliases follow the installed declarations; no internal src/dist import is required. For a generic factory, keep the concrete call's inferred type when an application needs a narrower result than ReturnType preserves. Use satisfies or a public option type for configuration literals, and validate unknown request/file data separately.
In a JavaScript application the equivalent editor annotation is /** @type {Parameters<typeof createFontRegistry>} */ on a matching argument tuple. This provides diagnostics only when JavaScript checking is enabled. It does not validate network data or create a resource automatically. See strict application setup for the complete NodeNext checker configuration.
Failure modes and resource lifetime#
Browser loading requires a DOM and may be blocked by CSP/CORS or a missing binary. Remove injected style elements with their disposer. Verify fallback metrics and real font network responses; a generated stylesheet can be valid while the referenced font is absent. Keep third-party URL/licensing policy explicit instead of silently adding an external font service.
Verify before deploying#
- Exercise the example with the actual owning runtime/browser, not only an imported module.
- Test one invalid input and the relevant unavailable/denied dependency path; do not turn those into a silent success.
- Confirm teardown leaves no owned listeners, timers, connections or stale per-user state.
- Check the production entry point and installed export map; a workspace checkout can contain features not yet published to npm.
Public option and data contracts#
These tables list fields declared directly on the named public contract. Referenced/inherited types retain their own declarations; optional does not imply a default value. Use the lifecycle and configuration chapters above for actual initialization behavior.
FontDefinition#
| Field | Accepted type/value | Optional |
|---|---|---|
sources | FontSource[] | No |
stretch | string | Yes |
featureSettings | string | Yes |
variationSettings | string | Yes |
FontSource#
| Field | Accepted type/value | Optional |
|---|---|---|
url | string | No |
format | string | Yes |
FontFaceOptions#
| Field | Accepted type/value | Optional |
|---|---|---|
family | string | No |
src | string | No |
weight | string | number | Yes |
style | string | Yes |
display | 'auto'|'block'|'swap'|'fallback'|'optional' | Yes |
format | string | Yes |
unicodeRange | string | Yes |
Explicit build assets and provenance#
Use buildFontAssets from @zeruxjs/font/build before the host framework build, not during requests. It accepts only explicit licensed local files or an opt-in host-injected allowlisted remote transport. Outputs are content-addressed local font/SVG/CSS files plus a V1 attribution manifest. No files are acquired on import and production serving of vendored output requires no provider network access. Match publicBase to the actual mount, such as /app/fonts/; the framework copies configured public assets but does not invoke the pipeline or inject preloads.
The host must declare redistribution permission and retain license notices. Font container headers are checked; actual axes/subset cmap coverage require an explicit maintained FontTools implementation. The optional supplied adapter uses a fixed bounded POSIX Python process with shell disabled, never model/request commands. It does not install FontTools. Synthetic header/tool fixtures do not prove decoding, shaping or live-provider compatibility.
| Budget | Default / hard ceiling |
|---|---|
| Build faces / icons | 64 / 256 definitions |
| One font | 8 MiB / 16 MiB |
| Whole candidate | 64 MiB / 128 MiB |
| Preload URLs / bytes | Two / 128 KiB; ceilings 16 / 16 MiB |
| Remote redirects / deadline | Two / 15000 ms; ceilings five / 120000 ms |
| Requested subset | Explicit finite Unicode scalars; at most 65536 |
Each build owns an output lease and private sibling staging directory. Validation, acquisition and pre-promotion quota failures preserve the previous directory. Same-filesystem rename/rollback is not a crash-atomic deployment transaction; operators reconcile stale locks/backups, and post-promotion cleanup may fail after new output is committed. The host owns directory permissions, remote DNS/egress, license correctness and serving/caching. Cooperative transport cancellation must settle before its build resources can finish cleanup.
Zuixs selection and browser ownership#
@zeruxjs/font/registry supplies DOM-type-free face registration; @zeruxjs/font/zuix maps explicit matching registry/build records into theme values, a public compiler plugin and selected asset references. It imports no Zuixs runtime or downloader. Only selected faces appear in its CSS; caller-supplied route token lists union/deduplicate bounded preloads. User extend.fontFamily / extend.fontWeight overrides remain authoritative. Font icons return explicit glyph text; SVG icons remain separately licensed asset records. Neither generates an accessible name from arbitrary content.
Use @zeruxjs/font/browser for native ESM browser imports without Node ambient types. createFontLoadScope owns per-mount observations (default eight, ceiling 64); dispose it on teardown. Timeouts default to 10000 ms with a 120000 ms ceiling. Cancellation ends observation and releases timers/listeners; native FontFaceSet downloads may continue. injectFontStyles accepts trusted CSS under the host CSP and returns a disposer removing only its owned element. Prefer an external generated stylesheet when the host policy requires one. Registry values and FontTools factory configuration have no persistent close handle.
The packed public-import examples in the package README cover local vendoring and actual Zuixs compilation at a non-root mount. Tests use injected FontFaceSet, remote Fetch and inspector results; native FontTools/font shaping and real browser/provider qualification remain separate prerequisites.
Public entry points
Manifest reference for @zeruxjs/font@0.0.4. Each target describes a published entry, not automatic framework registration.
| Import | Target |
|---|---|
@zeruxjs/font | {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
} |
@zeruxjs/font/http | {
"types": "./dist/http.d.ts",
"default": "./dist/http.js"
} |
@zeruxjs/font/build | {
"types": "./dist/build.d.ts",
"default": "./dist/build.js"
} |
@zeruxjs/font/browser | {
"types": "./dist/browser.d.ts",
"import": "./dist/browser.mjs",
"default": "./dist/browser.mjs"
} |
@zeruxjs/font/zuix | {
"types": "./dist/zuix.d.ts",
"default": "./dist/zuix.js"
} |
@zeruxjs/font/registry | {
"types": "./dist/registry.d.ts",
"default": "./dist/registry.js"
} |
Runtime requirements and dependencies
{
"engines": {},
"dependencies": {},
"peerDependencies": {},
"bin": {}
}Full package README
@zeruxjs/font#
Modular font-face generation and browser font loading.
import { createFontRegistry, fontPreload, loadFont } from "@zeruxjs/font";
const fonts = createFontRegistry();
fonts.add("regular", { family: "Inter", sources: [{ url: "/fonts/inter.woff2", format: "woff2" }] });
const css = fonts.css();The core registry generates CSS; it does not download or bundle font files. The separate opt-in build pipeline below vendors approved assets. Sources are validated, and browser font loading requires a DOM.
Font stylesheet endpoint#
import { createFontRegistry } from '@zeruxjs/font';
import { createFontStylesheetHandler } from '@zeruxjs/font/http';
const registry = createFontRegistry();
registry.add('ui', { family: 'Inter', sources: [{ url: '/fonts/inter.woff2', format: 'woff2' }] });
const endpoint = createFontStylesheetHandler({ registry, preloadKeys: ['ui'], cacheSeconds: 600 });Generates a CSS @font-face stylesheet with ETag/304 and HEAD, optional bounded Link: preload headers and cache policy. Font binary hosting, licensing, Content Security Policy and font optimization remain application/deployment responsibilities. Only trusted font definitions should be registered.
Build-time font assets#
@zeruxjs/font/build is an explicit Node-only pipeline; the core registry still only generates CSS and loads browser fonts. No font is downloaded, tool launched or file written on import. buildFontAssets accepts a trusted project root, relative destination and canonical public base path, and returns a version-1 manifest.
import {buildFontAssets} from '@zeruxjs/font/build';
import type {FontLicense} from '@zeruxjs/font/build';
export async function vendorBrandFont(
projectRoot: string, source: {path: string; family: string; license: FontLicense}
) {
return buildFontAssets({
projectRoot, outputDir: 'public/fonts', publicBase: '/app/fonts/',
fonts: [{key: 'brand', family: source.family, source: {path: source.path},
format: 'woff2', weight: 400, style: 'normal', display: 'swap',
license: source.license, preload: true}],
preload: {maxCount: 2, maxBytes: 128 * 1024}
});
}The source must be an approved local WOFF2 font in this example. WOFF, TrueType and OpenType are also supported by their explicit format values. Container signatures are checked, not complete font conformance; use the optional inspector to validate actual variable axes/glyph inventory. No font files are supplied or licensed by this package.
Every font/SVG source requires {id,source,attribution,redistributionAllowed:true,text?} license metadata. source is a public HTTPS attribution URL without credentials/query/fragment, or a local:... provenance label. Raw local paths and authenticated download URLs are never placed in the manifest. Include required copyright/license text/notices and satisfy all license conditions before setting the flag; this is a host declaration, not legal verification. subsettingAllowed:true is additionally required for subsetting. Reserved font names, modifications and vendor restrictions remain the host's responsibility. Do not redistribute an asset merely because it can be fetched.
The pipeline emits SHA-256-named binary files, a SHA-256-named stylesheet, optional SVG assets and manifest.json. Font entries include key, family, style/weight, format, bytes, source/output hashes, URL/file, declared verified axes, subset coverage/tool version when used, and attribution/license metadata. The manifest digest covers its deterministic content with an empty digest field. Source order does not affect sorted CSS/manifest output. Identical font bytes share one binary/cache identity, while face/license records remain separate. Use manifest.css.url and manifest.preloads rather than guessing URLs. The mutable manifest.json needs a suitable revalidation policy; hashed assets may use immutable caching under host control.
Default budgets: 64 font faces, 256 icons, 8 MiB per font (maximum 16 MiB), 64 MiB emitted output (maximum 128 MiB), two preload URLs/128 KiB (maximum 16 URLs/16 MiB). Preload requests exceeding either budget fail rather than silently widening it; identical binary URLs count once. A preload manifest is descriptive, not automatic HTML/header injection.
Face validation accepts weights 1–1000 (including ordered variable ranges and normal/bold), normal/italic/oblique styles with bounded ordered degree ranges, bounded named/percentage stretch, validated Unicode ranges and bounded four-character feature/variation settings. Display is an explicit allowlist. Duplicate keys, equivalent overlapping family/weight/style/stretch selections, duplicate axis tags and invalid ranges reject. Unicode-range-disjoint faces with otherwise overlapping selections are conservatively rejected; this is not the whole CSS Fonts grammar. Generic registry upsert/insertion ordering remains compatible, but previously accepted invalid CSS now fails. See CSS Fonts.
The destination must stay within the canonical existing project root. Source files and output ancestors reject traversal/symlinks; files are opened without following symlinks. Each build owns a per-output lease and private sibling staging directory. Only a complete valid candidate is promoted; pre-promotion acquisition/validation/output-budget failure leaves the previous directory intact. Cleanup removes owned staging/leases, not unrelated files. Promotion uses same-filesystem rename with a rollback directory, not a crash-atomic deployment transaction. Stale locks/backups after a process crash require operator reconciliation; no automatic stale-owner deletion is performed. The directory owner is trusted, so these checks are not a sandbox against a hostile filesystem owner racing ancestor replacements. Build outputs use restricted owner permissions; configure serving/deployment permissions deliberately.
Run/await this pipeline before zerux build, placing output under a configured public root. The inspected framework builder copies such static files and includes them in its public asset manifest; it does not invoke this pipeline or invent font preloads. Match publicBase to the actual non-root hosting mount. Do not run both writers concurrently over staging files or rebuild user-managed applications implicitly. The docs app remains its own exporter. Deployment/cache retention of prior hashed files for active clients is separate from replacing this local build directory.
Optional remote acquisition and subsetting#
Remote sources are direct font binaries, not provider stylesheets or executable discovery endpoints. Opt in with remote:{fetch,allowedOrigins,maxRedirects?,timeoutMs?} and source:{url}. The injected transport is required, HTTPS origins are exact/allowlisted (maximum 32), redirects are manual and reapproved at every step (default two, maximum five), credentials are omitted and encoded responses reject. Default wall deadline is 15 seconds (maximum 120 seconds); per-file and declared/actual-length limits still apply. Readers are cancelled/released on failures, redirects, size/deadline/cancellation errors. A transport that already followed redirects rejects. Errors omit transport details/download URLs. Host transport/DNS/firewall policy must prevent SSRF/rebinding; URL allowlisting is not IP pinning. Signals/deadlines are cooperative, so a nonconforming injected Fetch can delay settlement and retain the build lease.
After vendoring, emitted CSS references only the configured local public base. Serving the directory requires no provider access or remote fetch. Provider acquisition needs its own live qualification; no Google/Adobe/all-provider mirroring, CSS discovery, authenticated licensing flow or CDN parity is claimed.
createFontToolsTool({executable?,timeoutMs?}) explicitly selects the maintained FontTools subsetter. It requires a host-installed POSIX Python 3 with FontTools major version 4+ and the compression dependencies needed for the chosen format (including Brotli for WOFF2). It never installs packages. It invokes a fixed isolated Python program with shell disabled, a minimal PATH/LANG environment rather than application credentials, and JSON/bytes over stdin/stdout—no user font paths, Python source or tool arguments are accepted. The trusted operator chooses the executable; do not expose that configuration to HTTP clients. The process is bounded to 512 MiB address space, 30 CPU seconds, no filesystem writes and a wall timeout up to 30 seconds, and is killed/reaped on cancellation/limit failure. This is a bounded parser subprocess, not a general-purpose sandbox against a compromised tool installation.
Pass tool when declaring axes:[{tag,min,default,max}] or subset:{codePoints,language?,script?}. Axes are checked against actual fvar inspection before and after processing; selected weights must fit the declared wght range. Custom tags are four ASCII letters. Variable-axis metadata is not inferred from a filename. Subsets require an explicit finite list of up to 65536 Unicode scalar values, not automatically generated language/script tables. language/script are validated attribution hints; they do not prove complete linguistic coverage. Source and emitted cmap coverage (excluding glyph zero/.notdef mappings) must include every requested code point, and selected font icons must exist in the emitted subset. Complex shaping, layout-feature preservation, hinting and display quality need separate FontTools/font/browser qualification; code-point presence alone does not prove correct shaping. Tool version and original/output hashes are retained for provenance.
The tool is optional and inert until chosen. Without it, ordinary static copying works, while variable-axis, subsetting and font-icon coverage requests fail clearly rather than bypass inspection. A host may inject the same FontToolsTool contract for a separately qualified FontTools environment; declarations/fixture results are not proof that its implementation uses the maintained tool.
Separate icon registration#
Build icons use either {key,kind:'font',fontKey,codePoint} or {key,kind:'svg',geometry,license}. Font icons reference a registered face and need verified emitted glyph coverage. fontIcon(codePoint,family) returns plain glyph text plus a validated CSS family; set textContent/style properties and provide an accessible host label or decorative semantics. It does not emit arbitrary HTML or manufacture a meaningful label.
SVG sources are structured viewBox and bounded path data/fills only. svgIconMarkup(geometry,{decorative?,label?}) generates fixed SVG elements with escaped attributes; meaningful icons require an explicit label. No raw XML/HTML, event attributes, style strings, external href/use, foreignObject or unrestricted remote SVG parsing is accepted. Path characters are constrained for safe rendering, not a complete geometry/conformance validator. Fill is none/currentColor or bounded hex colors. SVG and icon-font assets retain distinct manifest kinds/ownership; use their registered manifest URLs/data.
Browser entry and qualification#
Use native ESM @zeruxjs/font/browser for CSS, browser loading and icon helpers without Node imports or ambient Node types. The existing root/http entries remain compatible. npm run build emits package-owned declarations and the browser bundle; the existing dev command watches TypeScript only, so rerun the build to update that bundle. Browser loading uses the host's FontFaceSet and does not call the build pipeline or provider transport. Styling/CSP, fallback metrics, accessible icon labels and font display/rendering quality remain host responsibilities.
Focused checks cover deterministic/offline output, changed hashes, duplicate families/axes, invalid CSS, license denial, preload/output budgets, unsafe paths, remote failure/redirect/deadline cleanup, output-lease ownership, safe icons and injected glyph-coverage failures. Packed Node-only/DOM-only consumer fixtures verify public exports and declarations. Header fixtures are not real fonts; no font rendering or complete shaping qualification is inferred. Python FontTools is not installed in this environment, so actual native subset/axis parsing and live-provider acquisition remain explicit prerequisites, not claimed successes.
Opt-in Zuixs font tokens#
@zeruxjs/font/zuix consumes a registry plus its matching build manifest. The /registry entry also provides a DOM-type-free registry for Node-only consumers. It imports no Zuixs runtime, downloads nothing and starts no browser loading. Zuixs' public theme/plugin API is the integration boundary; no changes to its compiler or network behavior are required.
import {createFontRegistry} from '@zeruxjs/font/registry';
import {createFontZuixAdapter} from '@zeruxjs/font/zuix';
import type {FontAssetManifest} from '@zeruxjs/font/build';
import {createZuix} from 'zuixs';
export function compileFontUtilities(assets: FontAssetManifest) {
const registry = createFontRegistry();
for (const face of assets.fonts) registry.add(face.key, {
family: face.family, weight: face.weight, style: face.style,
sources: [{url: face.url, format: face.format}], display: 'swap'
});
const fonts = createFontZuixAdapter({registry, assets,
fonts: [{token: 'body', key: 'brand', fallback: ['system-ui', 'sans-serif']}]});
const utilities = createZuix({preflight: false, theme: fonts.theme,
plugins: [fonts.plugin]}).generate(['font-family-body', 'font-weight-body']);
return {css: fonts.css + '\n' + utilities.css, assets: fonts.manifest,
preloads: fonts.preloads([['body'], ['body']])};
}Use the earlier build example's brand face. Write returned CSS as an external stylesheet and reference it through the host build manifest, under the actual mount (e.g. /app/fonts/). The returned asset manifest selects only mapped font/icon records, retains hashes/licenses and references the original build digest; it is not a replacement build digest or a new file copier. Serve those referenced binary/SVG files. No inline scripts/styles or CSP relaxation are required. The host still configures style-src, font-src and img-src; no nonce is generated here.
Token names are lowercase ASCII identifiers, at most 48 characters. Duplicate font/icon token names, missing records and registry/manifest mismatches reject. font-family-NAME maps to theme.fontFamily.NAME with a scoped-name custom property declared on :root; font-weight-NAME maps to theme.fontWeight.NAME. Ranges use their minimum weight as the property default; hosts choose other weights explicitly. Prefix, important and variants are handled by Zuixs. Merge fonts.theme with your own config deliberately; Zuixs extend.fontFamily/extend.fontWeight override these defaults. Later host plugins can override matching utility names. The adapter does not mutate your registry/config. Fallbacks always end in system-ui/sans-serif; fallback layout/appearance still varies by platform. Author overrides own their fallback/CSS validity.
icons:[{token,key}] selects separately registered icons. Font icons emit icon-NAME family/style/weight utilities, but glyph text remains explicit (fontIcon(...).text with textContent); accessible labels stay host-owned. SVG icons stay URL/hash records in the selected manifest, rendered with ordinary image semantics or the structured SVG helper, never arbitrary CSS/HTML. The adapter does not turn SVG files into font glyphs or insert generated-content labels.
No faces/icons are selected implicitly. Only selected faces enter fonts.css; use a separate adapter per route/build chunk for finer CSS splitting. Merely generating a utility does not call document.fonts.load. fonts.preloads([]) returns none, independent of build-time preload suggestions. Pass explicit per-route token lists in one call to union/deduplicate URLs across routes: default two URLs/128 KiB, hard maxima 16 URLs/16 MiB; excess rejects. SVGs are not font preloads. Route planning is host-owned, with no global preload cache. The HTTP stylesheet handler also deduplicates URLs and rejects overflow through maxPreloads (default/hard maximum 16), instead of silently truncating. Its existing ETag follows selected registry CSS changes; core HTTP requires Node while /browser remains independent.
Optional browser loading: createFontLoadScope({document?,maxPending?}) owns load observations for a mount. Call scope.load(family,{signal?,timeoutMs?,sample?}), then scope.dispose() on teardown. It rejects new observations after disposal, bounds concurrent observations (default 8, maximum 64), clears timers/listeners on every outcome and redacts native failure messages. loadFont retains its boolean result; absent DOM/FontFaceSet returns false, failure/cancellation/timeout rejects. Cancellation stops observation and suppresses success delivery, not the underlying browser font download: FontFaceSet.load has no AbortSignal parameter. Host/CSS-owned faces are never removed. Native work can continue after observation ends; these limits are not a browser network quota.
A failed configured font leaves the declared fallback stack available; failed icon fonts cannot guarantee an equivalent fallback glyph, so use explicit text/SVG alternatives where meaning matters. swap favors visible fallback text but may shift layout; optional can retain fallback, while block/fallback have other timing tradeoffs. Match fallback metrics and test actual scripts/weights; no zero-layout-shift guarantee is made. See CSS Fonts display controls. These checks use injected FontFaceSet behavior and real Zuixs compilation, not live font rendering, visual layout measurements or browser/network cancellation certification.
Font loading borrows only the FontLoadDocument surface (fonts.load); ordinary browser Document objects remain compatible. Injected load owners return matched-face arrays whose elements stay unknown because the helper observes only the count. Style injection separately requires a real DOM document. Disposing a loading scope ends observation and releases its timers/listeners; it does not cancel the browser’s native download.