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

@powderworks/docs

v0.4.0

Published

Turn a tree of markdown into a tree of indexes. One small script to copy a code block, another to open the index on a narrow screen, a third to search.

Readme

powderworks-docs

Turns a tree of markdown into a tree of indexes. One small script to copy a code block, another to open the index on a narrow screen, a third to search — Pagefind, fetched only when a reader asks; nothing else runs in the browser.

Every directory is a section; every section gets an index page listing its children — title and one-line description from frontmatter. Breadcrumbs come from the path you already have. The whole aesthetic is cratebank.io: system serif, six color tokens, automatic light and dark, one readable column.

One place for the names

powderworks.toml ships with the generator and holds what the sites share and what each one varies: the workshop's name and face under [org], a site's own name, tagline, domain, logo and face under [site.<key>]. A site names itself and gets the rest.

powderworks-docs build content --out out --site straitjacket --static public
python3 node_modules/@powderworks/docs/tools/social-card.py --site straitjacket

Anything passed on the command line still wins, and --config points at a different file. Adding a site means an entry here, which is the trade: one place to change a name, one repository to open a pull request against.

Pictures live in assets/<key>/ here, not in the sites, because the workshop's own pages show each project's mark and should not have to clone a repository to find one. The generator copies them into the output, and a site that keeps its own copy under the same name overwrites it.

| file | what it is | | --- | --- | | assets/<key>/logo.* | the mark, beside the site name and in the tab | | assets/<key>/cover.* | the picture the landing opens with, and the card carries | | social.png | the card itself, drawn into the site's static directory |

The landing takes its name, tagline and tab from [site.<key>] too. A root index.md need carry no frontmatter at all; the tagline is set beneath the title as the lede.

--check compares the committed card against what the config now says. It compares the inputs rather than the pixels — the words, the picture, both faces and the arrangement, hashed into the PNG when it is drawn — because freetype hints differently from one version to the next and a card drawn on a laptop would never match one redrawn on a runner.

Installing it

bun add @powderworks/docs     # or npm install @powderworks/docs

The package is @powderworks/docs; the command it installs is powderworks-docs, which is what a site's build script calls.

The package carries what the generator reads at build time: powderworks.toml, the faces in fonts/, and the marks and covers in assets/. A site that names itself with --site <key> gets all three without cloning anything.

Releasing it

A tag whose name matches the version in package.json publishes to npm, and nothing else does:

npm version minor          # writes package.json and tags it
git push --follow-tags

The release workflow refuses a tag that disagrees with the manifest, packs the tarball, installs that into an empty directory, builds the example site from it, and only then publishes. Building from the tarball rather than from the checkout is the point: files is an easy list to leave a directory out of, and a site that cannot find powderworks.toml fails in the consumer's repository rather than in this one.

It needs no secret. npm trusts this repository's release.yml by name and issues the credential to the run itself, so there is no publish token to leak, rotate, or hand a one-time password to. The publish is signed with provenance either way, so a reader can tie the tarball on the registry back to the commit and the run that made it.

Configured on the package's npm settings page as a trusted publisher: PowderworksCode, docs, release.yml.

Usage

powderworks-docs build <contentDir> --out <outDir> \
  --site-url https://straitjacket.dev \
  --name Straitjacket \
  --description "A secret scanner, but for slop." \
  --github PowderworksCode/straitjacket \
  --license MIT \
  --wordmark Straitjacket \
  --wordmark-font '"Manufacturing Consent"' \
  --wordmark-woff2 /manufacturing-consent-latin.woff2 \
  --wordmark Powderworks \
  --wordmark-font '"IM Fell English SC"' \
  --wordmark-woff2 /im-fell-english-sc-latin.woff2 \
  --static ./public

Content shape

content/
  index.md               → /                (landing)
  getting-started.md     → /getting-started/
  guides/
    index.md             → /guides/         (section index)
    ci.md                → /guides/ci/

Frontmatter keys: title, description, order (lower sorts first within its section), and tab-title for a browser tab that should not read like the heading — given, it is the whole title, with no site name appended.

Each page also emits its markdown source as index.md beside its rendered HTML, so agents can fetch either form without content negotiation.

Emitted alongside the pages: sitemap.xml, llms.txt, 404.html, and theme.css wherever a page references it.

Values that move

A page that quotes the release it documents goes stale the day after it is written. --var name=value fills {{name}} in wherever it appears — body, frontmatter, and the markdown twin alike — so the site names the version and the page never does:

powderworks-docs build content --out out --site straitjacket \
  --var version="$(sed -n 's/^version = "\(.*\)"$/\1/p' ../Cargo.toml | head -n 1)"
- uses: PowderworksCode/straitjacket@v{{version}}

A name is lowercase letters, digits and dashes, and a {{name}} with no --var behind it fails the build rather than shipping its braces to a reader. A $ in front is left alone, so a workflow example's ${{ github.token }} passes through untouched.

The card a link shows

--social names the image; tools/social-card.py draws one to the same bones for every site — the name in the site's own face, its tagline, its domain, its logo down the right. Needs Pillow, and a ttf or otf rather than the woff2 the site serves, because Pillow cannot read woff2. A URL is fetched once.

Two faces go into a card, and --site fills both: the name's, which the site names for itself, and the tagline's, which is [org.body] and the same on every card in the fleet. Both are named rather than looked for on the machine drawing. That is the difference between a card and a page: a page is set in whatever serif the reader has, a card is drawn once and committed, so a face found rather than named means a laptop and a runner draw two different cards from one config. Where neither the registry nor --body-font names one, the script uses a system serif if it can find one and stops if it cannot — Pillow will hand out a default for the asking and it is a sans, which is a card that is wrong in a way nobody notices until it is posted.

python3 node_modules/@powderworks/docs/tools/social-card.py \
  --name Straitjacket \
  --tagline "A secret scanner, but for slop." \
  --url straitjacket.dev \
  --logo public/engraving.jpg \
  --font https://raw.githubusercontent.com/google/fonts/main/ofl/x/X.ttf \
  --body-font https://raw.githubusercontent.com/google/fonts/main/ofl/y/Y.ttf \
  --out public/social.png

A long name wraps rather than shrinking past the point where it would read smaller than its own tagline. Without a logo the text simply has the width.

The index on a narrow screen

The index is the sidebar on a wide screen and a button on a narrow one: the name on one side of the head of the page, the button on the other, and the whole tree — sub-lists included — beneath them when it is asked for. It pushes the page down rather than covering it, so there is no overlay to trap, dim or lock, and the button is a button, which is what a screen reader and a keyboard already know how to work.

The button is only shown once the page has said, in a line in its head, that scripts run here. Where they do not, the index sits above the words as it always has, top level only, and nobody is handed a control that cannot open anything.

Search

A site with a tree gets a box above it, answered by Pagefind. The index is built at the end of the build from the pages as written, so what search finds is what is served, and it ships as static fragments beside them: a page load fetches none of it, and the first search fetches the engine and the few pieces the query touches.

Results stand where the tree stood — a list in place of a list, in the same column and the same voice — and clearing the box, or Escape, puts the tree back. Like the menu button, the box only appears once the page has said that scripts run here, so a reader without them is never offered a box that cannot answer.

The trail, the pager, the footer and the heading marks stay out of the index, so a section's name finds the section rather than every page that links to it; so does the 404 page, which is the one page a search should never find. A site with a single page builds no index and shows no box — there is nothing to find that is not already on screen — and --no-search, or search = false on the site's registry entry, says no for the rest.

A site with nothing to index

A content tree with no pages below the landing gets no sidebar and a single centred column. There is no flag: a site with one page has no index to show.

Such a page can say <!--projects--> and be handed the fleet — every other site in the registry, with its tagline, its links and the mark that ships from assets/<key>/. The marks are copied into the output; covers are not, because no site needs another site's cover.

The example site

example/ is this generator pointed at a small tree that uses everything it has: a hero and a cover, sections and section indexes, ordered frontmatter and a tab-title, code blocks, tables, quotes, the fleet, the markdown twins, and the index that folds behind a button on a phone. It builds from ../src rather than from a published copy, so a change to the generator shows up on the next build with nothing to publish first.

cd example
bun install
bun run build      # writes example/out
bun run dev        # serves it at localhost:8787, worker and all
bun run preview    # uploads a version and prints its preview URL
bun run deploy     # promotes a build to the Worker's route

bun run preview is wrangler versions upload: a URL nobody else is routed to, which is the one to paste into a pull request. Workers Builds settings are root directory example, build bun run build, deploy bun run deploy.

build runs bun install at the repository root before it runs the generator, because it runs the generator from ../src and a build that starts in example installs only what example declares — which is wrangler, and not the two packages the generator itself imports.

The demonstration is also the selfcheck: bun run selfcheck at the root builds the same tree and throws away the output, which is enough to catch a generator change that stops producing pages at all.

Serving

The output is plain files; anything that serves static assets works. On Cloudflare Workers, point [assets] at the output directory and set not_found_handling = "404-page".