npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

typesnap

v0.1.0

Published

Zero-config font optimisation: detect external fonts at build time and generate preload tags, font-display rules, and metric-matched fallback stacks to eliminate layout shift.

Readme

TypeSnap

Zero-config font optimisation for the web. Scan your project, detect every external font, and generate preload tags, font-display rules, and metric-matched fallback stacks — automatically.

version license

TypeSnap is a CLI and build plugin that eliminates font-induced Cumulative Layout Shift (CLS) from your site without making you hand-write preload tags or size-adjusted fallbacks. It statically analyses your project, figures out exactly which fonts are in use, and emits the three pieces you actually need: preloads, font-display, and metric-matched fallbacks.


Table of Contents


Why TypeSnap

Fonts are the single most common source of layout shift on modern sites. Getting fonts right requires three pieces working in concert:

  1. <link rel="preload"> in the document <head> so the browser fetches font files early.
  2. font-display: swap (or optional) on every @font-face so text doesn't stay invisible forever.
  3. A size-adjusted fallback font stack so the fallback renders at the same metrics as the real font — no reflow when the real font arrives.

All three are tedious to wire up by hand, all three are easy to get wrong, and none of them are zero-config today (yes, even with next/font). TypeSnap derives all three automatically from the fonts you've already declared.

What it is not. TypeSnap doesn't self-host fonts, doesn't download font files, doesn't modify your font files, and doesn't replace runtime loaders like the Web Font Loader. It's pure static analysis at build time.


Quick Start

# Run a scan and print a report
npx typesnap scan

# Generate preload tags, fallback CSS, and a JSON report
npx typesnap generate

# Inject the preloads into your HTML
npx typesnap inject public/index.html

# Fail CI if new issues appear
npx typesnap lint --fail-on warning

Install as a dev dependency if you want to use the build plugins:

npm install -D typesnap

60-second walkthrough

Say your app/layout.tsx uses next/font/google for Inter, your styles.css has a custom @font-face for Clash Display, and your index.html loads Playfair Display from Google Fonts. Run:

npx typesnap generate

TypeSnap will:

  • Detect all three fonts, across all three file types, without any config.
  • Write public/typesnap-fallbacks.css containing an @font-face block per font with size-adjust, ascent-override, descent-override, and line-gap-override tuned to match Arial / Georgia / Courier New to each font's metrics.
  • Write public/typesnap-preloads.html with ready-to-paste <link rel="preconnect"> and <link rel="preload"> tags.
  • Write .typesnap-report.json with detected fonts, per-font CLS risk, and lint findings.

Import the generated CSS in your global stylesheet:

@import "./typesnap-fallbacks.css";

Then chain the fallback family in your CSS:

body {
  font-family: "Inter", "Inter Fallback", sans-serif;
}

Done. Your CLS from fonts is now ~0.


How It Works

Detection phase

TypeSnap walks your project (ignoring node_modules, .git, dist, .next, build, out, and a handful of other common output folders) and runs specialised parsers against each file type:

| File type | What it looks for | |---|---| | .css, .scss, .sass, .less, .pcss | @import url(...) to Google Fonts / Fontshare, @font-face blocks with font-family, src, font-weight, font-style, font-display | | .html, .htm, .vue, .svelte, .astro | <link rel="stylesheet"> to font CDNs, <link rel="preload" as="font"> tags (used to reconcile "already has preload"), embedded <style> blocks | | .js, .ts, .jsx, .tsx, .mjs, .cjs | next/font/google imports + calls (Inter, JetBrains_Mono, etc.), next/font/local imports, string literals containing Google Fonts or Fontshare URLs | | tailwind.config.* | fontFamily theme entries (primary family becomes a detected font) |

Each detector emits DetectedFont records. The orchestrator then deduplicates by family name, merging weights, styles, and file paths, and picks the "highest-confidence" source as canonical (next-font-local > next-font-google > font-face > google > fontshare > tailwind).

Generation phase

For each detected font, TypeSnap emits:

  • A preload tag. For local .woff2/.woff/.ttf files it emits <link rel="preload" as="font" type="..." crossorigin="anonymous">. For Google Fonts and Fontshare it emits a <link rel="preload" as="style"> plus the original <link rel="stylesheet"> — this pattern is the officially recommended one and avoids duplicate font-file downloads.
  • font-display injection. For every @font-face missing font-display, TypeSnap records it as a lint warning and emits the configured default (swap by default) when generating.
  • A metric-matched fallback @font-face. Using a built-in database of 30+ popular fonts (see below), TypeSnap writes a fallback block like:
@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  ascent-override: 90.49%;
  descent-override: 22.56%;
  line-gap-override: 0%;
  size-adjust: 107.64%;
}

For fonts outside the built-in database, TypeSnap picks a category-based fallback (Arial for sans-serif, Georgia for serif, Courier New for monospace) and uses sensible default overrides. The fallback family is named "<Font Name> Fallback". Chain it in your CSS stack right after the real font:

font-family: "Inter", "Inter Fallback", sans-serif;

Report phase

A .typesnap-report.json at your project root contains:

  • The full list of detected fonts with source, weights, styles, detectedIn files, and booleans for hasPreload / hasFontDisplay / hasFallback.
  • A per-font CLS impact estimate (low / medium / high).
  • The generated preload tags and fallback CSS blocks (so your CI or dashboard can inspect without running the tool again).
  • Lint issues with severity, rule name, and human-readable message.

CLI Reference

typesnap scan

Detect fonts and print a report to the terminal.

typesnap scan [--cwd <path>] [--write] [--json]

| Flag | Description | |---|---| | -c, --cwd <path> | Project root (default: current directory) | | -w, --write | Also write report, fallback CSS, and preload HTML to disk | | --json | Output machine-readable JSON instead of a pretty report |

Exits with code 1 if any error-severity issues were found.

typesnap generate

Write .typesnap-report.json, public/typesnap-fallbacks.css, and public/typesnap-preloads.html.

typesnap generate [--cwd <path>]

typesnap inject [files...]

Inject preconnect + preload tags directly into HTML files. Idempotent — re-running skips tags that are already present, and a <!-- typesnap:inject --> marker keeps the managed block cleanly bounded.

typesnap inject [files...] [--cwd <path>]

If no files are passed, TypeSnap auto-detects HTML files in public/, dist/, out/, and build/.

typesnap inspect

Pretty-print the most recent report (runs a fresh scan if none is cached).

typesnap inspect [--cwd <path>] [--report <path>]

typesnap lint

Exit non-zero when font issues exceed a threshold. Good for CI.

typesnap lint [--cwd <path>] [--max-warnings <n>] [--fail-on <severity>]

| Flag | Description | |---|---| | --max-warnings <n> | Fail when warnings exceed this count. -1 disables (default) | | --fail-on <sev> | Fail on this severity or higher: info, warning, or error (default: warning) |


Build Plugins

Vite

// vite.config.ts
import { defineConfig } from 'vite';
import { typeSnapPlugin } from 'typesnap/vite';

export default defineConfig({
  plugins: [
    typeSnapPlugin({
      fontDisplay: 'swap',
      injectHtml: true,
    }),
  ],
});
  • Runs on buildStart, writes fallback CSS and a report.
  • On transformIndexHtml, injects <link rel="preconnect"> + <link rel="preload"> into your index.html automatically, inside a <!-- typesnap:inject --> block.

Next.js

// next.config.js
const { withTypeSnap } = require('typesnap/next');

module.exports = withTypeSnap(
  {
    // your existing next config
    reactStrictMode: true,
  },
  {
    fontDisplay: 'swap',
  },
);
  • Runs during Webpack config, scans once per build.
  • Writes fallback CSS to public/typesnap-fallbacks.css and the report to .typesnap-report.json.
  • Import the fallback CSS from your root layout or _app.tsx.

Programmatic API

import { analyze, writeArtifacts } from 'typesnap';

const result = await analyze(process.cwd(), {
  fontDisplay: 'optional',
  exclude: ['DebugFont'],
});

console.log(result.report.stats.fontsDetected);
console.log(result.fallbackCss);

await writeArtifacts(result);

Full surface (all exported from typesnap):

  • analyze(projectRoot, overrides?) => Promise<AnalyzeResult>
  • writeArtifacts(result) => Promise<{ reportPath, fallbackPath, preloadPath }>
  • injectIntoHtmlFile(filePath, preconnect, preloads) => Promise<{ injected, skipped }>
  • scanProject(projectRoot, scanDirs) => Promise<ScanResult>
  • generateFallbackBlock(font), generateFallbackStylesheet(fonts)
  • generatePreloadTags(font), generatePreconnectTags(fonts)
  • buildReport(params), generateLintIssues(fonts)
  • lookupFontMetrics(family), getMetricsForFont(family), isKnownFont(family), listKnownFonts()
  • loadConfig(projectRoot, overrides), detectScanDirs(projectRoot)

TypeScript types: DetectedFont, FontMetrics, FontSource, FontDisplay, TypeSnapReport, TypeSnapConfig, PreloadTag, FallbackBlock, LintIssue, ClsImpact, Severity.


Configuration

Zero config works out of the box. If you need to override, drop a typesnap.config.js, typesnap.config.mjs, typesnap.config.cjs, or typesnap.config.json at your project root.

// typesnap.config.js
/** @type {import('typesnap').PartialConfig} */
module.exports = {
  fontDisplay: 'swap',       // 'swap' | 'optional' | 'block' | 'fallback' | 'auto' — default: 'swap'
  scanDirs: 'auto',          // 'auto' | string[] — default: 'auto' (scans from projectRoot, ignores node_modules/.git/dist/.next/etc.)
  exclude: ['DebugFont'],    // Font families to ignore
  outputDir: 'public',       // Where to write typesnap-fallbacks.css and typesnap-preloads.html
  injectPreloads: true,      // Used by build plugins
};

Detection Coverage

| Source | What's detected | How | |---|---|---| | Google Fonts (fonts.googleapis.com) | Family, weights (incl. :wght@ and :ital,wght@ axes), styles, display param | CSS @import, HTML <link>, JS string literals | | Fontshare (api.fontshare.com) | Family, weights, display | CSS @import, HTML <link>, JS string literals (supports f= and f[]=) | | Custom @font-face | Family, weight (single value or range like 200 700), style, font-display, src URLs | CSS parsers across .css/.scss/.sass/.less/.pcss + embedded <style> blocks | | next/font/google | Family (from the imported function name), weights, styles, display, subsets | AST-like regex on import { Inter } from 'next/font/google' + subsequent const font = Inter({...}) calls | | next/font/local | Family (derived from file path or variable), weights, styles, display, src paths | Same pattern against import localFont from 'next/font/local' | | Tailwind config | Primary fontFamily entries (skipping system fonts) | Scans tailwind.config.js/ts/mjs/cjs for fontFamily: { ... } block | | Adobe Fonts (Typekit) | Source detection only — surfaces the font but doesn't parse internal family list | URL-based heuristic |

Files whose names match typesnap-* or start with .typesnap are excluded from detection so TypeSnap's own output never feeds back into subsequent scans.

HTML entities (&amp;, &quot;, etc.) in href attributes are decoded before URL parsing, so multi-round inject/scan cycles are stable.


Output Artifacts

| File | Location | Purpose | |---|---|---| | .typesnap-report.json | Project root | Full detection + generation report, including per-font CLS estimates and lint issues | | typesnap-fallbacks.css | public/ (configurable) | Metric-matched @font-face fallback blocks — @import this from your global CSS | | typesnap-preloads.html | public/ (configurable) | Ready-to-paste preconnect + preload <link> tags |


Lint Rules

| Severity | Rule | Triggers when | |---|---|---| | warning | missing-preload | A font has no corresponding <link rel="preload"> tag | | warning | missing-font-display | A custom @font-face has no font-display declaration | | warning | missing-fallback | A font has no metric-matched fallback stack | | warning | too-many-weights | A single family loads more than 4 weights (performance risk) | | info | tailwind-only | A font is declared in Tailwind config but no @font-face or CDN import was found |

Use --fail-on warning in CI to block PRs that regress your font setup.


Supported Fonts (Metric Database)

TypeSnap ships with precomputed metric overrides for these families. For any other font, it falls back to sensible category-based defaults (serif → Georgia, monospace → Courier New, everything else → Arial).

Inter · Roboto · Roboto Mono · Open Sans · Lato · Poppins · Montserrat · Raleway · Nunito · Nunito Sans · Oswald · Source Sans Pro · Source Sans 3 · Merriweather · Playfair Display · DM Sans · DM Serif Display · IBM Plex Sans · IBM Plex Mono · Mulish · Ubuntu · Rubik · Work Sans · Plus Jakarta Sans · Karla · Fira Code · JetBrains Mono · Space Grotesk · Space Mono · Satoshi · Clash Display · Geist · Geist Mono

To extend: fork the repo and add entries to src/utils/fonts.ts. A PR with the source of the metrics (ideally Google Fonts' unitsPerEm / ascent / descent / x-height values) is welcome.


CI Integration

GitHub Actions

name: Fonts
on: [pull_request]
jobs:
  typesnap:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx typesnap lint --fail-on warning

Pre-commit hook (husky)

npx husky add .husky/pre-commit "npx typesnap lint --fail-on warning"

Project Structure

typesnap/
├── src/
│   ├── cli.ts                    # CLI entrypoint (commander)
│   ├── core.ts                   # analyze() + writeArtifacts() + injectIntoHtmlFile()
│   ├── index.ts                  # Public API surface
│   ├── types.ts                  # Shared types
│   ├── commands/
│   │   ├── scan.ts               # typesnap scan
│   │   ├── generate.ts           # typesnap generate
│   │   ├── inject.ts             # typesnap inject
│   │   ├── inspect.ts            # typesnap inspect + renderInspect() for other commands
│   │   └── lint.ts               # typesnap lint
│   ├── detectors/
│   │   ├── index.ts              # orchestrator + deduplication
│   │   ├── css.ts                # @import + @font-face parsing
│   │   ├── html.ts               # <link> + entity decoding
│   │   ├── js.ts                 # next/font/google, next/font/local, URL literals
│   │   └── tailwind.ts           # fontFamily config parsing
│   ├── generators/
│   │   ├── preload.ts            # preload + preconnect tags
│   │   ├── fallback.ts           # metric-matched @font-face blocks
│   │   └── report.ts             # buildReport + generateLintIssues + CLS estimation
│   ├── plugins/
│   │   ├── vite.ts               # Vite build plugin
│   │   └── next.ts               # withTypeSnap() for next.config.js
│   └── utils/
│       ├── files.ts              # walk(), writeFile(), path helpers
│       ├── config.ts             # loadConfig() + detectScanDirs()
│       ├── fonts.ts              # metric database + category inference
│       ├── logger.ts             # picocolors wrappers
│       └── parse-font-url.ts     # Google Fonts + Fontshare URL parsing
└── test/
    └── fixtures/
        └── sample-project/       # End-to-end test fixture
            ├── index.html
            ├── styles.css
            ├── tailwind.config.js
            └── app/layout.tsx

Development

# Install
npm install

# Build (tsup → dist/)
npm run build

# Typecheck
npm run typecheck

# Rebuild on change
npm run dev

# Run against the bundled fixture
node dist/cli.js scan --cwd test/fixtures/sample-project
node dist/cli.js generate --cwd test/fixtures/sample-project
node dist/cli.js inspect --cwd test/fixtures/sample-project
node dist/cli.js lint --cwd test/fixtures/sample-project --fail-on warning

The fixture at test/fixtures/sample-project/ exercises every detector and every lint rule. Use it as a smoke test after any change to src/detectors/* or src/generators/*.

Stack

  • Runtime: Node 18+
  • Build: tsup (esbuild under the hood), emits ESM + .d.ts
  • CLI parsing: commander
  • Colors: picocolors (tiny, no ANSI escape-hatching needed)
  • No runtime parser — everything is regex-based static analysis. This keeps TypeSnap fast (sub-200ms on most projects) and dependency-light.

Roadmap

| Status | Phase | |---|---| | ✅ Shipped | Phase 1 — CLI MVP: scan, generate, inspect, inject, lint | | ✅ Shipped | Phase 2 — Vite + Next.js build plugins | | 🚧 Planned | Phase 3 — Astro, SvelteKit, Nuxt adapters | | 🚧 Planned | Phase 4 — Live metric fetching from Google Fonts for unlisted families | | 🚧 Planned | Phase 5 — VS Code extension surfacing inline CLS warnings | | 🚧 Planned | Phase 6 — typesnap doctor command: compare real CLS in a headless browser before/after generation |


License

MIT © Om Rajguru

TypeSnap is an original concept by Om Rajguru, April 2026.