Contributing
This project has a code of conduct. By interacting with this repository, organization, or community you agree to abide by its terms.
We’re excited that you’re interested in contributing! Take a moment to read the following guidelines.
Repository layout (monorepo)
This repository is an npm workspaces monorepo (ADR 0013):
| Path | Role |
|---|---|
Root (private) | VitePress docs site, shared Biome/Lefthook, docs/adr, migration guides |
packages/webfont | Published webfont npm package — library, CLI, build, tests, CHANGELOG.md, NOTICE.md |
Run npm ci at the repo root. Library commands (npm test, npm run build, npm run test:package) delegate to the webfont workspace. User-facing markdown (README.md, FEATURES.md, …) stays at the root for the docs site; VitePress rewrites packages/webfont/CHANGELOG.md and packages/webfont/NOTICE.md.
Future workspaces (webfont-studio, optional CLI split, Homebrew assets) will live under packages/* without changing the public npm package name.
There are several ways to contribute, not just by writing code:
Improving documentation
As a user of this project you’re perfect for helping us improve our docs: typo corrections, error fixes, better explanations, and new examples.
Improving issues
Some issues lack information, aren’t reproducible, or are just invalid. Help make them easier to resolve.
Common errors: see TROUBLESHOOTING.md for symptoms and fixes on the current release. Upgrading? see MIGRATION.md for what changed between versions.
Maintainer backlog (open issues)
When working through the issue tracker (see also AGENTS.md — “GitHub issues workflow”):
- Triage: assignee, milestone
next, type label (bug,enhancement, etc.). - Fix PR: tests where needed, code change, and for issue fixes that change behavior across releases a new file
docs/migration/issue-NNNN-<slug>.md(workflow and entry structure). - Keep reporters informed on the issue thread (English):
- When investigation starts: what you are checking; link the PR when it exists.
- Explain what was discovered (root cause, not only “we’ll fix it”).
- Explain how it will be resolved (PR link, expected behavior after merge).
- State release status clearly: fix on
master/ in review / planned for the next npm release — not on npm until published. - Include a workaround on the current npm version when one exists.
- After merge: comment that the fix is on
masterand planned for the next release; leave the issue open until npm publish. - On release: comment with the version number and the exact steps from MIGRATION.md, then close the issue.
Giving feedback on issues
We’re always looking for more opinions on discussions in the issue tracker.
Writing code
Code contributions are very welcome. It’s often good to first create an issue to report a bug or suggest a new feature before creating a pull request to prevent you from doing unnecessary work.
And, if you’re raising an issue, please understand that people involved with this project often do so for fun, next to their day job; you are not entitled to free customer service.
Submitting an issue
- Search the issue tracker (including closed issues) before opening a new issue;
- Ensure you’re using the latest version of projects;
- Use a clear and descriptive title;
- Include as much information as possible: steps to reproduce the issue, error message, version, operating system, etcetera;
- The more time you put into an issue, the more we will.
Submitting a pull request
- Never commit or push directly to
master. Create a branch, open a pull request, and merge through GitHub — even for documentation-only or test-only changes. Maintainers merge; contributors and automation do not bypass review withgit push origin master. - Use lowercase branch names only. The full branch name must be lowercase letters, numbers, and slashes (for example
fix/cli-formats,test/to-ttf-unit-tests,ci/update-node-version). Do not use camelCase, PascalCase, or uppercase acronyms in branch names. - Non-trivial changes are often best discussed in an issue first, to prevent you from doing unnecessary work;
- For ambitious tasks, you should try to get your work in front of the community for feedback as soon as possible;
- New features should be accompanied with tests and documentation;
- Bug fixes should include a regression test that fails without the fix;
- Please, don’t include unrelated changes;
Maintainers and automation (squash merge, PR titles, Copilot threads, merged-branch rules): see MAINTAINERS.md.
User-facing changes and documentation
Before opening or updating a pull request, check whether your change affects how people use webfont — via the CLI (webfont / packages/webfont/dist/cli.mjs), the programmatic API (webfont({ ... })), or config files (.webfontrc, package.json webfont key, etc.).
When it does, update documentation in the same PR:
| What changed | Update |
|---|---|
| CLI flags, aliases, help text, or exit codes | Edit packages/webfont/src/cli/meow/cliFlagCatalog.ts, run npm run docs:cli → packages/webfont/docs/cli.md; update FEATURES.md when capability changes |
| Install / setup steps (npm, CLI script, config, verification) | packages/webfont/install.md |
webfont() options, defaults, or svgicons2svgfont parameters | packages/webfont/docs/configuration.md |
webfont() options, defaults, return shape, or supported inputs/outputs | packages/webfont/docs/configuration.md, README.md Capabilities at a glance, packages/webfont/README.md, FEATURES.md |
| New or removed public options or pipelines | README + FEATURES.md + TypeScript types under packages/webfont/src/types/ |
| Bug fixes or recurring user-facing errors (especially from issues) | New docs/migration/issue-NNNN-<slug>.md (workflow); TROUBLESHOOTING.md when the fix applies the same on all releases |
| Legal notices, font licensing copy, attribution, or dependency license table | packages/webfont/NOTICE.md; link from README as needed |
| Internal-only refactors with no usage change | No README or FEATURES change; say so in the PR Testing section |
Agents and automation should follow the same rule — see AGENTS.md (“Documentation”) and MAINTAINERS.md for PR workflow.
Testing and coverage
Tests are required. Every pull request that changes runtime behavior must add or update automated tests in the same PR — not in a follow-up. Docs-only changes are the exception; say so explicitly in the PR Testing section.
New behavior and bug fixes should include tests. Follow AGENTS.md (“Testing”) for Vitest patterns in this repo.
| Expectation | Guidance |
|---|---|
| Same PR | Land tests with the code they protect. Reviewers should block merges that add features or fixes without coverage. |
| Unit + integration | Error paths and guards should have unit tests in the module under test. Add integration tests when the full pipeline matters, but do not use them as the only coverage for a guard. |
| Explicit, not implicit | Name tests after the invariant they protect (for example, why a guard exists). If a dependency quirk motivated the code, add a small test that documents the quirk. |
| Pipeline ordering | When step B must not run if step A fails, assert B was not called (spy/mock), not only that the final promise rejected. |
| Fixtures | Reuse fixtures under packages/webfont/src/fixtures/ for file-based cases; add a fixture when the scenario is stable and reusable. |
| Test titles | Every it(...) description must include should or should not (for example, should return default options, should not call metadataProvider when parse fails). Avoid bare verbs (returns, throws, accepts) or prefixes like documents that without should. |
Run npm test before pushing. Integration-only coverage for a localized guard is incomplete.
Published package validation (npm run test:package)
npm run test:package runs three orthogonal layers of validation against the packed tarball (see ADR 0012):
npm run test:publint— publint lintspackage.jsonfor publish-surface issues (exports,files,main,module,types, condition ordering). Prints warnings and suggestions in every CI log; fails only on errors.npm run test:attw—@arethetypeswrong/cliprobes types resolution across node10, node16 (from CJS), node16 (from ESM), and bundler to catch types that would fail to resolve for TypeScript consumers under any module system.npm run test:pack—packages/webfont/scripts/pack-smoke-test.mjspacks withnpm pack, installs the tarball into throwaway ESM and CJS consumer projects, and asserts each import shape works end-to-end:import webfont from "webfont"(ESM default) →typeof === "function"and generates a realwoff2from fixtures.import { webfont } from "webfont"(ESM named) → same.const { webfont } = require("webfont")(CJS named) andrequire("webfont").default(CJS default) → same.- Also asserts the tarball ships
dist/index.js,dist/index.mjs,dist/browser.js, anddist/cli.mjs.
Together these guard package.json#exports, #files, #types, and the built dist/*.mjs / dist/*.js / dist/**/*.d.{ts,mts} against regressions the in-source Vitest suite cannot see (for example #618, where the ESM default import returned the module namespace object and threw TypeError: webfont is not a function).
The meta script runs on every pull request in .github/workflows/pr.yml, gates the publish job in .github/workflows/npm-publish.yml, and is wired into prepublishOnly so npm publish fails locally if the tarball would ship a broken import shape.
When adding or removing files from packages/webfont/dist/, updating packages/webfont/package.json#exports / #files / #types, or touching the Vite build modes, run npm run test:package locally before pushing.
- Lint and test before submitting code by running
$ npm test; - Run
$ npm run prettifyto apply Biome formatting and safe fixes before pushing; - Write a convincing description of why we should land your pull request: it’s your job to convince us.
Linting and formatting
This project uses Biome for linting and formatting (biome.json). Use npm run lint to check and npm run prettify to auto-fix.
Do not add legacy ESLint suppressions. This repository no longer uses ESLint (see ADR 0001). Remove eslint-disable / eslint-enable comments instead of carrying them forward. When a rule must be suppressed, use a targeted biome-ignore comment with a short reason.
Do not suppress TypeScript with @ts-expect-error or @ts-ignore. Fix the underlying type issue instead:
| Situation | Prefer |
|---|---|
| Extra keys from a loaded config file in tests | type LoadedConfig = ResultConfig & { foo: string } and cast result.config as LoadedConfig |
Untyped or partial mocks (e.g. CLI meow) | Import from __mocks__/ for assertions; if a cast is needed, use as unknown as MockType |
| Async code that should reject | await expect(promise).rejects.toThrow(...) or .rejects.toMatchObject(...) instead of try/catch + conditional expect |
| Intentionally unused parameters | Prefix with _ (e.g. _options) so Biome and TypeScript accept them |
| Regex style rules | Rewrite the pattern (e.g. /\s/gu instead of a capture group only used for whitespace) |
Only use biome-ignore when there is no reasonable code change; never use it to paper over type errors—adjust types or test helpers instead.
Do not use ignoreDeprecations in TypeScript config. When upgrading TypeScript or hitting deprecated compiler options, migrate tsconfig.json (for example, replace deprecated moduleResolution values) and fix resulting type errors. Do not silence deprecations with ignoreDeprecations.
Git hooks
Git hooks are managed by Lefthook (lefthook.yml). They install automatically when you run npm install (prepare → lefthook install).
| Hook | What runs |
|---|---|
pre-commit | Biome check (with safe fixes) on staged *.{ts,js,json} files; banned-suppression scan |
pre-push | npm run typecheck and npm run depcheck (parallel), then npm test, then npm run docs:site (sequential) |
To simulate hooks without committing or pushing:
npx lefthook run pre-commit
npx lefthook run pre-pushSee ADR 0003 for rationale.
Dependencies
- Pin exact versions in
package.json(no^,~, orlatestranges), including@types/*packages. - The repository sets
save-exact=truein.npmrc, sonpm install <package>records exact versions automatically. - When upgrading a dependency, pin its
@types/<package>counterpart in the same pull request when one exists. - Update both
package.jsonandpackage-lock.jsonin the same pull request. - Dependabot opens upgrade PRs for pinned dependencies; do not use open ranges to get updates.
- Run
npm run depcheckbefore pushing when you add, remove, or move imports — it runs Knip to find unused dependencies, unlisted imports, and dead exports (see ADR 0008). CI runs the same check on every pull request.
CI changes
Pull requests that change CI configuration (for example, GitHub Actions workflows) must follow these conventions:
- Branch name: use the
ci/prefix (e.g.ci/update-node-version). - Commit message: use the
ci:type in Conventional Commits format (e.g.ci: upgrade GitHub Actions to Node 26).
Do not use chore: or chore(ci): for CI-only changes.
- Pin tool versions. Do not install CLIs with
@latest(or other floating tags) on deploy or release paths — pin a semver in the workflow and optionally allow override via a repository variable with a safe default (for exampleVERCEL_CLI_VERSIONdefaulting to54.20.1in.github/workflows/vercel-deploy.yml). Bump pins in focusedci(deps):pull requests. - Bind secrets once. Map repository secrets to
envat the job or step level and let the tool read them from the environment. Do not repeat GitHub Actionssecrets.*expressions inline across multipleruncommands — it is error-prone and harder to audit. Seenpm-publish.yml(NODE_AUTH_TOKEN) andvercel-deploy.yml(VERCEL_TOKEN) for the pattern. - GitHub Environments for deploy jobs. Map production deploy jobs to a named GitHub Environment so runs appear under Deployments with a URL (
npm/github-packagesinnpm-publish.yml;vercelinvercel-deploy.yml). Optionally scope secrets per environment and add protection rules in Settings → Environments. - Validate the docs site. Changes that touch markdown published by VitePress (see
.vitepress/config.mtsrewrites) must passnpm run docs:site(runsdocs:demo+vitepress build; buildspackages/webfont/dist/cli.mjsfirst when missing). Pre-push and.github/workflows/pr.ymlrun it afternpm test; Vercel production deploy runs the fullnpm run docs:build. - VitePress markdown. VitePress compiles published pages as Vue templates. Do not put mustache-style double braces (Vue interpolation syntax) in those markdown files outside fenced code blocks —
vitepress buildfails when Vue tries to parse them. Rephrase (for example “NunjucksfontNameplaceholder”); if you must show literal braces, use HTML entities ({{/}}) so Vue never sees interpolation delimiters.
AppVeyor: a legacy project may still receive GitHub webhooks. Root appveyor.yml disables builds via a non-matching branch filter (appveyor-disabled) because maintainers may lack AppVeyor dashboard access. Do not delete it until the AppVeyor project is removed upstream.
Releases
Versioning is automated with Release Please (see ADR 0004).
- Merge changes to
masterusing Conventional Commits (feat:,fix:,docs:,ci:, etc.). - Release Please opens or updates a Release PR with the next version,
packages/webfont/CHANGELOG.md, andpackages/webfont/package.jsonupdates. - Review and merge the Release PR to create the git tag and GitHub Release on GitHub.
- Merging the Release PR is enough to publish (see npm publishing below): the Release Please workflow cuts the GitHub Release and then dispatches
npm-publishfor the new tag. Arelease: publishedevent created with the defaultGITHUB_TOKENdoes not start downstream workflows, so the publish is triggered explicitly viaworkflow_dispatch(the documented exception to that rule).
Git tags created by Release Please follow v{semver} (for example v12.0.0). The config sets include-component-in-tag: false so tags are not prefixed with the package name (webfont-v12.0.0). See ADR 0004.
Do not run local npm version or push version tags manually unless coordinating an emergency release with maintainers.
npm publishing
Publishing from GitHub Actions deploys the same validated build to two environments — npm (public registry, webfont) and github-packages (GitHub Packages, @itgalaxy/webfont). CI is non-interactive — there is no npm login. Each deploy job writes ~/.npmrc with //<registry>/:_authToken=${NODE_AUTH_TOKEN} and sets NODE_AUTH_TOKEN from a secret at the npm publish step, so npm expands the token at runtime and it never lands on disk or in the logs:
npm→NODE_AUTH_TOKEN=NODE_AUTH_TOKENrepository secret (an npm access token from npmjs.com; a GitHub PAT does not authenticate to npmjs.org).github-packages→NODE_AUTH_TOKEN= built-inGITHUB_TOKEN(packages: write); no extra secret. Swap for a PAT secret withwrite:packagesif you prefer a personal token.
actions/setup-nodehas nonode-auth-tokeninput —registry-urlonly wires auth to read fromenv.NODE_AUTH_TOKEN. Passing a token to a non-existent input silently publishes unauthenticated (theE404in #742); writing~/.npmrc+ settingNODE_AUTH_TOKENis the fix.
One-time setup (package maintainer):
- Create an npm Automation or Granular access token for the
webfontpackage (npmjs.com → Access Tokens) with publish permission (OTP/2FA-for-writes disabled for automation, or use a token type that bypasses it). - Add it as a GitHub repository secret named
NODE_AUTH_TOKEN(Settings → Secrets and variables → Actions). GitHub Packages needs no secret — it usesGITHUB_TOKEN.
After merging a Release PR: the Release Please workflow cuts the GitHub Release and then dispatches npm-publish with the new tag automatically. (A release: published event from the default GITHUB_TOKEN does not start downstream workflows, so the dispatch is done explicitly — workflow_dispatch and repository_dispatch are the exceptions to that rule.) If publish does not start, run it manually: Actions → npm publish → Run workflow with the release tag (e.g. v12.1.0).
Future: npm Trusted Publishing (OIDC) can replace the NODE_AUTH_TOKEN secret when a maintainer configures it on npmjs.com for workflow npm-publish.yml.
Deployment environments and GitHub Packages
The workflow runs two deploy jobs after build, each mapped to a GitHub Environment so every release appears under the repo's Deployments with a link to the published package:
| Environment | Registry | Package | Auth | Deploy URL |
|---|---|---|---|---|
npm | registry.npmjs.org | webfont | NODE_AUTH_TOKEN secret | npmjs.com/package/webfont/v/<version> |
github-packages | npm.pkg.github.com | @itgalaxy/webfont | GITHUB_TOKEN (packages: write) | repo Packages page |
GitHub Packages requires a scope matching the repo owner, so the publish-github-packages job renames the package to @itgalaxy/webfont in the checkout only (via npm pkg set name=…, never committed) — the unscoped webfont on npmjs.org is unaffected.
Add protection rules (required reviewers, wait timers) per environment in Settings → Environments if you want manual approval to gate a deploy.
Vercel docs deployment
.github/workflows/vercel-deploy.yml deploys the VitePress site to Vercel on every push to master (and via workflow_dispatch). The deploy job maps to the vercel GitHub Environment; the deployment URL is captured from vercel deploy --prebuilt --prod and shown under Deployments.
| Environment | Host | Auth secrets | Deploy URL |
|---|---|---|---|
vercel | Vercel (webfont project) | VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID (repository or environment secrets) | output of vercel deploy (production alias, e.g. webfont-js.vercel.app) |
Install from GitHub Packages (needs a GitHub token with read:packages):
echo "@itgalaxy:registry=https://npm.pkg.github.com" >> .npmrc
echo "//npm.pkg.github.com/:_authToken=\${GITHUB_TOKEN}" >> .npmrc
npm install @itgalaxy/webfontRelease assets
The release-assets job attaches two archives to each GitHub Release: the npm tarball webfont-<version>.tgz (the exact published package) and webfont-dist-<version>.zip (the built dist/ only). It needs contents: write and uploads with gh release upload "$RELEASE_TAG" --clobber.
Manual publish from a maintainer machine (browser/web auth or local npm login) is still supported:
git fetch origin --tags
git checkout v12.0.1 # tag from Release Please
npm ci
npm login # or ensure a valid token
npm publish --access publicprepublishOnly starts with npm whoami, so a local npm publish fails fast if you are not logged in — before the build and package validation run, instead of failing on authentication at the very end. In CI the workflow builds and validates dist/ once in the build job, uploads it as an artifact, and the publish-npm job runs npm publish --ignore-scripts against that artifact — so prepublishOnly (and its whoami) does not run on the publish runner and the build is not repeated (see #742). The publish-npm job depends on build, so test:package still gates every publish.
Automated publishing does not retroactively upload versions that already exist as git tags only (for example 11.5.x never published to npm).
Resources
Thanks for contributing to webfont! 👏✨