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

@termwright/screenshot

v0.7.7

Published

terminal screenshots: cell grid to self-contained SVG (embedded glyph outlines) and PNG via resvg, no Chromium

Readme

@termwright/screenshot

Terminal screenshots for termwright, without a browser.

A cell grid becomes an SVG with the glyph outlines embedded, and resvg turns that into a PNG. No Chromium, no headless browser starting up in the middle of a test run, and no dependency on the viewer having the right font installed — which is the whole point when the screen is full of Nerd Font icons.

Install

pnpm add @termwright/screenshot

Requires Node >= 22. ESM only.

Usage

import { writeFile } from 'node:fs/promises';
import { renderPng, renderSvg } from '@termwright/screenshot';
import { frameAt, openTrace } from '@termwright/trace';

// A live session: the driver's screen snapshot is already a valid frame.
const shot = renderSvg(harness.screen(), { fontSize: 16 });
await writeFile('now.svg', shot.svg);
if (!shot.selfContained) {
  console.warn('no outline for', shot.fallbackCharacters.join(''));
}

// A recorded one: reconstruct the moment a step failed, then rasterise it.
// The frame measures characters with the profile the recording was made with,
// so the screenshot matches the screen the test saw.
const trace = await openTrace('out/login.twtrace');
const steps = await trace.steps();
const failing = steps.find((step) => step.status === 'failed');
const frame = await frameAt(trace, failing?.castEndOffset ?? 0);
await writeFile('failure.png', renderPng(frame, { scale: 2 }).png);
await trace.close();

What it renders

Layout comes from the grid — cell (row, column) is drawn at padding + column × cellWidth — so a double-width cell occupies exactly two columns because the emulator said so, not because a font did. Emoji, CJK and box drawing all stay on the grid.

Colours (256-colour palette and 24-bit), inverse, dim, underline, strikethrough and the cursor (block, bar, underline) are all drawn. bold and italic use the real faces when the system has them — a font collection like Menlo.ttc carries all four — and fall back to stroking and shearing the regular face only when it does not.

Colour emoji are embedded as images lifted straight out of the font's bitmap strike (sbix/CBDT), and COLR/CPAL fonts as their coloured layers, so a screen full of emoji still reports selfContained: true. Cells holding several code points — a variation selector, a ZWJ family — are shaped through the font rather than looked up by first code point.

What you get back

const shot = renderSvg(frame);
shot.svg; // the document
shot.width; // user units, so a caller can size the element
shot.height;
shot.selfContained; // true when no character needed a font at view time
shot.fallbackCharacters; // the ones that did, e.g. ['\u{F0000}']
shot.fontsUsed; // font files whose glyphs were embedded

renderPng returns the same story in pixels: png, width, height (SVG units × scale), plus selfContained, fallbackCharacters and systemFontsLoaded — the last one says whether this render paid for the font scan described below.

The cost of a fallback

A fully self-contained frame rasterises in tens of milliseconds. A frame with fallback characters makes resvg enumerate the installed fonts, and it does that on every call — about a second on macOS, several on Windows, with no cache between renders. Rendering a batch of frames that all contain one uncoverable character therefore costs seconds per frame.

Whether a frame has fallback characters is a property of the machine's fonts, not of the content: a desktop with CJK and emoji faces embeds almost everything, while a minimal CI container with one Latin font falls back on both. The slow path is therefore rarest where it is usually measured and commonest where it costs the most.

Point the renderer at a font that covers your screen and the problem disappears. When it cannot, and blank glyphs are acceptable, decline the scan:

renderPng(frame, { systemFontFallback: false });

Self-containment

Characters no configured font covers fall back to <text> with a monospace family, still positioned per cell. Not embedded is not the same as not drawn: a viewer with a suitable font renders them normally, and renderPng loads system fonts on their behalf by default. What the list really tells you is that the output depends on the machine displaying or rasterising it — those characters come out blank only where nothing covers them, which for PNG means systemFontFallback: false.

Fonts are resolved from font.files, then TERMWRIGHT_FONT, then platform defaults (Menlo on macOS, DejaVu/Liberation/Noto on Linux, Consolas on Windows, each followed by a CJK face for coverage). Point it at your own font to match what your users actually see:

renderSvg(frame, { font: { files: ['/fonts/JetBrainsMonoNerdFont-Regular.ttf'] } });

Emoji are looked up in a colour font last in that chain and embedded as artwork — a bitmap strike (sbix, CBDT) as a data: image, a COLR/CPAL font as its coloured layers — so a screen full of emoji still reports selfContained: true.

In a failure report

@termwright/trace embeds images the caller hands it, rather than rasterising anything itself — that keeps a native renderer out of every test run:

import { generateHtmlReport } from '@termwright/trace';

await generateHtmlReport({
  outFile: 'out/report.html',
  results: [
    {
      id: 't1',
      title: 'login',
      status: 'failed',
      tracePath: 'out/login.twtrace',
      screenshots: [{ label: 'at failure', image: renderPng(frame, { scale: 2 }).png }],
    },
  ],
});

Development

pnpm build && pnpm typecheck && pnpm test

Implementation decisions: NOTES.md.