Skip to content

feat(fallbacks): add decision-oriented fallback API#41

Merged
caio-pizzol merged 1 commit into
mainfrom
caio-pizzol/fallbacks-decision-api
Jun 5, 2026
Merged

feat(fallbacks): add decision-oriented fallback API#41
caio-pizzol merged 1 commit into
mainfrom
caio-pizzol/fallbacks-decision-api

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

The published 0.1.0 had two problems worth a release: it doesn't load in plain Node, and its result type hides the one thing docfonts is for - the honest "no open font stands in."

First, a real correctness bug: @docfonts/fallbacks@0.1.0 throws ERR_MODULE_NOT_FOUND under plain Node ESM, because the emitted imports were extensionless (from "./data"). That works in bundlers and broke everywhere else. The build now uses NodeNext and emits .js specifiers; a node-esm test imports the built artifact in real Node so it can't regress.

Second, the API. getFallback collapsed five different outcomes into null - including a measured "no open substitute" (Aptos), which read identically to a font docfonts never heard of. The new getFallbackDecision returns a discriminated union (fallback | asset_missing | no_substitute | customer_supplied | preserve_only | unknown), so a consumer can report the difference. getRenderableFallback and createFallbackMap cover the "just give me a family / a resolver map" paths and only ever hand back fonts the consumer bundles. Result fields are renamed for clarity (substituteFamily, policyAction, lineBreakSafe) and normalizeFamilyName is public so map lookups line up.

Clean cut: the 0.1 names are dropped rather than deprecated (no production consumer yet). ESM-only; require() is unsupported and documented.

This is a feat on 0.1.0, so semantic-release produces 0.2.0 - publish via the existing manual workflow (dry_run=false) after merge.

Note: built from a clean worktree off origin/main on purpose - the local tree had an unrelated, uncommitted slim-refactor (deleting the release infra + LICENSE) that I deliberately kept out of this PR.

Verified: typecheck, lint clean; bun test 14 pass incl. the Node-ESM import of the built dist; getFallbackDecision distinguishes all six kinds; createFallbackMap requires canRenderFamily.

Replace the FontFallback|null surface with three intent-named helpers and a
discriminated union that preserves docfonts' signature value - the honest no.

- getFallbackDecision returns kind: fallback | asset_missing |
  no_recommended_fallback | customer_supplied | preserve_only | unknown, so a
  consumer can tell a measured 'no open font' apart from a font docfonts never
  heard of, and from a substitute it simply does not bundle. The non-fallback
  kinds carry evidenceId back into SUBSTITUTION_EVIDENCE.
- getRenderableFallback (render) and createFallbackMap (resolver map) only ever
  return a family the consumer can render; createFallbackMap requires
  canRenderFamily. normalizeFamilyName is now public for map lookups.
- Rename result fields: family -> substituteFamily, action -> policyAction (an
  action, not a quality claim), faithful -> lineBreakSafe (true for metric_safe,
  near_metric, and monospace cell_width_only - advances preserve line breaks).
  Drop the 0.1 names (clean cut, pre-1.0).
- Fix Node ESM loadability: build with NodeNext and emit .js import specifiers,
  so the published package loads in plain Node, not only bundlers. Guarded by a
  node-esm test that imports the built artifact.

ESM-only; CommonJS require() is unsupported.
@caio-pizzol caio-pizzol force-pushed the caio-pizzol/fallbacks-decision-api branch from f359bf4 to f57ac9d Compare June 5, 2026 20:55
@caio-pizzol caio-pizzol merged commit 42b1097 into main Jun 5, 2026
1 check passed
@caio-pizzol caio-pizzol deleted the caio-pizzol/fallbacks-decision-api branch June 5, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants