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

@wilsoon/docs

v1.4.0

Published

Markdoc schema, renderer and publishing CLI for docs.wilsoon.dev

Readme

@wilsoon/docs

The Markdoc schema, the renderer, and the docs CLI behind docs.wilsoon.dev.

It exists so a repository can keep its documentation as files next to the code it describes, and publish it with one command. It is not a general-purpose tool - it publishes to one site, and pushing to that site needs a token only I hold. If you found this on the registry, the renderer and the Markdoc schema may still be of interest; the CLI will not be.

Writing docs with an agent? Run docs guide, or read AGENTS_PACKAGE_GUIDE.md, which ships inside the package. It states every export, every component, every fatal rule and every refusal in one place, so nothing has to be inferred from the source.


Install

npm install @wilsoon/docs

Requires Node 22.12 or newer - the CLI uses process.loadEnvFile, and hashing goes through Web Crypto rather than node:crypto so the same file runs in a Worker, in Node and in a browser.

react and react-dom (>= 19) are peer dependencies. npm 7+ installs them automatically. pnpm does not - add them explicitly, or docs preview fails on import, since it renders with the same React components the site uses.


Quick start

npx docs init --project my-app       # scaffold docs/ and a config
npx docs login                       # sign in with WilsoonID, once per machine
npx docs check                       # is what I wrote valid?
npx docs preview                     # what will it look like?
npx docs push                        # publish it

docs init writes docs/docs.config.mjs, one chapter, one page, and a .gitignore for the preview cache.


Commands

| | | Effect | | -------------- | ---------------------------------------- | ------------------------------------- | | docs schema | Print the tags you can write | Nothing | | docs check | Validate every page | Nothing | | docs preview | Serve the docs locally | Nothing, beyond a cached stylesheet | | docs status | Show what a push would do | Nothing | | docs login | Sign this machine in with WilsoonID | Stores a token on this machine | | docs whoami | Show which account a push would use | Nothing | | docs logout | Forget the token on this machine | Removes a local file | | docs init | Scaffold docs/ | Creates files | | docs pull | Bring the site's version into your files | Overwrites files | | docs push | Publish what changed in code | Writes to the site. Never deletes | | docs prune | Delete live pages with no file | PERMANENTLY deletes |

docs <command> help gives the full options and an EFFECT block for each. docs check exits non-zero, so docs check && docs push refuses to publish anything invalid.

Signing in

docs login opens a browser, authenticates against the same identity provider the dashboard uses, and stores a token for this machine. There is no shared secret to distribute, and a push is attributed to the account that made it.

The token is scoped to one endpoint, expires after 90 days, and is stored in your user config directory (%APPDATA%\wilsoon-docs on Windows, ~/.config/wilsoon-docs elsewhere) with owner-only permissions. Revoke it — or any other machine's — from CLI tokens in the dashboard. docs logout only removes the local copy; it does not revoke anything.

What you may push is decided per project by its editor list, never by holding a token. Signing in proves who you are; it grants nothing on its own.

CI

A pipeline has no browser, but it does not need one — you have one. Create a deploy token from CLI tokens in the dashboard, and paste it into your pipeline's secret store as DOCS_TOKEN. It is shown once and stored as a hash.

A deploy token is scoped to one project, and the project comes from the token, not from the request. A push naming any other project is refused before anything is read — so a compromised runner reaches exactly one project, whatever it sends. A pipeline that publishes two projects needs two tokens.

It differs from a docs login token in two further ways, both because it gets pasted somewhere and forgotten:

  • It can be set to never expire. A build that breaks on a timer is its own kind of outage. 90 days, 1 year and never are offered; 1 year is the default.
  • It is always role user, whatever your own role is, so it can never create or claim a project.

It also stops working the moment the account that created it stops being an editor of that project — no revocation needed. Revoke it from the same page when the pipeline is retired.

Worked example

Say the FederatedIdentityProvider repo publishes the wilsoon-id project.

  1. Open https://docs.wilsoon.dev/admin/tokens and choose New deploy token.
  2. Project wilsoon-id, label it after the pipeline that will hold it — github actions - FederatedIdentityProvider — and pick an expiry.
  3. Copy the token. It is shown once; it is stored as a hash and cannot be recovered.
  4. In that repo: Settings → Secrets and variables → Actions → New repository secret, named DOCS_TOKEN.

Then the workflow needs nothing else:

name: Publish docs
on:
  push:
    branches: [main]

jobs:
  docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npx @wilsoon/docs check
      - run: npx @wilsoon/docs push
        env:
          DOCS_TOKEN: ${{ secrets.DOCS_TOKEN }}

docs check first, so an invalid page fails the build instead of publishing.

The token names wilsoon-id, and docs.config.mjs in that repo names it too. If they ever disagree the push is refused, naming both — the token wins, because it is the half the pipeline cannot edit.

Working on that repo yourself needs none of this. Run docs login once on your machine and push; a deploy token is only for the unattended case. And never commit one — not to docs.config.mjs, not to a .env that is tracked.

The credential is resolved in this order:

  1. DOCS_TOKEN from the environment
  2. the stored login for this endpoint, from docs login

init, schema, check, preview and logout need no credential at all.


Config

docs.config.mjs describes the project itself. Only project and endpoint are required.

export default {
  // Permanent. It is the project's URL on the site.
  project: "node-oidc-kit",

  title: "Wilsoon Node OIDC Kit",
  description: "An OIDC client for Node, with sessions that survive a restart.",
  repository: "https://github.com/Wilsoon7721/node-oidc-kit",

  endpoint: "https://docs.wilsoon.dev",
  dir: ".",
};

| Field | Effect | | ------------- | ------------------------------------------------------- | | title | The project's name on the site | | description | One sentence, shown under the title on the home page | | repository | Linked from the home page, with a GitHub or GitLab icon |

repository must be an https:// URL on github.com or gitlab.com, without the .git suffix - the site only links hosts it vouches for, so anything else is refused at push time rather than stored and silently never rendered. docs init fills it in from your git remote, so it is usually already correct.

These three are pushed once per run, not per section, and only when the config mentions them. A config with no description key leaves the column alone; a config with description: '' clears it. That distinction is what stops a push from erasing something written in the dashboard.

Changing only title or description still publishes. Before 1.1.0 the title travelled inside a section payload, so editing it without touching a page meant every hash still matched, nothing was pushed, and the new title never arrived - while docs push reported that everything was already published.


On disk

Folder is a chapter, file is a section. Two levels only - the database has exactly two, and a third is refused rather than silently flattened.

docs/
  docs.config.mjs
  01-getting-started/
    _chapter.md          # chapter title and order
    01-introduction.md
    02-installation.md
    assets/              # images and video, beside the pages using them
  • NN- prefixes set the order and are stripped from the identifier.
  • assets/ inside a chapter is expected and passed over in silence. A nested folder holding .md files warns, because those pages are ignored.
  • Two folders reducing to one chapter identifier - guide/ and 01-guide/ - is a fatal error, not a merge.

Frontmatter is a contract

---
title: Installation
identifier: installation
order: 2
---

Never change identifier: on a page that has been published. It is the URL and it is permanent. Changing it does not rename the page - it publishes a second one and orphans the first. Renaming the file is safe as long as the identifier stays put; that is what identifier: is for.

title and order can change freely.


Media

Write the path you can see on disk:

{% image src="./assets/diagram.png" alt="How a page reaches the database" /%}
{% video src="./assets/walkthrough.mp4" caption="Publishing a page" /%}

docs push uploads the file and rewrites src to a /media/... path on the way out. The file on disk keeps its relative path - do not "fix" it to a /media/ one by hand. A page that came down through docs pull already carries absolute paths, because that is what the site stores; leave those alone.

Alt text is required on images. Pass alt="" for a decorative one - that is the correct value, and it is explicit.

Keys are content-addressed, so re-pushing an unchanged image is a no-op and two pages referencing one picture share one object. Images are capped at 10MB, video at 95MB, mp4 and webm only.

Markdoc syntax inside a code fence stays literal, so a page can document the syntax without the example being interpreted - and push skips fenced regions when uploading and rewriting media for the same reason.


The lockfile

docs.lock.json is committed. It records what was last published, and it is what lets the CLI say who changed a page rather than only that it differs.

Each entry holds two hashes, and they are not interchangeable:

  • hash - the body on disk when it was pushed, compared against local
  • published - what the site was given, compared against remote

They differ whenever a media path was rewritten. published: null is a real observation - the site reported no hash - and is not the same as the key being absent, which means a lockfile written before the two were split.

A push refuses any page edited in the dashboard since your last push, and names it. --force <key> overrides one deliberately; there is no global force.


Programmatic use

The renderer is the same one the site and the dashboard use, which is the point of it living here - a page pushed from a repo and the same page edited in the dashboard produce byte-identical rows, including the search text.

import { renderSource, blocking } from "@wilsoon/docs/render";
import { toReact, asProseRoot } from "@wilsoon/docs/render/react";
import { tags, config } from "@wilsoon/docs/markdoc";
import { sha256Hex } from "@wilsoon/docs/hash";

const { tree, headings, diagnostics } = await renderSource(source);
if (blocking(diagnostics).length) throw new Error("invalid");

| Entry | What it is | | --------------------------------- | ---------------------------------------------------------------------------- | | @wilsoon/docs/markdoc | The tag schema. No React, so a CLI can import it alone | | @wilsoon/docs/render | parse → validate → transform → headings → tables → highlight | | @wilsoon/docs/render/react | toReact(tree, React) - React is passed in, so there is never a second copy | | @wilsoon/docs/render/components | Callout, CodeBlock, Figure, Aside, Video | | @wilsoon/docs/render/text | Plain text for search, so every writer ranks identically | | @wilsoon/docs/hash | One sha256, shared with the Worker and with Postgres |

Plain ESM, no build step: the same files are imported by the site through Vite, by the dashboard island, and by the CLI through bare Node. The render components use createElement rather than JSX for that reason - Node cannot load .jsx, and a build step would mean the site importing from dist/, where a stale build is a bug that looks exactly like a code bug.

There are no type declarations. The package is plain JavaScript with JSDoc.


Known limitations

  • A docs login token carries the role you had when you created it. Roles are snapshotted at login, because a token outlives the browser session it came from. The role only decides who may create or claim a project, and the snapshot is bounded by the 90-day expiry — but a demoted admin keeps that ability until their token expires or is revoked. CI tokens are unaffected: they are pinned to user at creation and can never create or claim.
  • A CI token with no expiry ends only when revoked. That is the point of the option, but it means a forgotten one stays live indefinitely. The tokens page shows when each was last used, so an unused one is visible.
  • A section longer than one page has no repeated header when printed. Browsers do not implement @page margin boxes; the site's print route measures and splits in JavaScript instead.
  • docs preview borrows another project's stylesheet before you publish. The site splits CSS per route, so a project with nothing live yet has no page of its own to read - the preview finds any published section page and uses that. It is exact, because every docs page shares one layout, but it does mean the first preview against a site with nothing at all published falls back to the home page and says the chrome is approximated.
  • docs new is described in DESIGN.md and was never built. Creating a file is one cat away and the frontmatter is three lines.

Licence

MIT. Copyright (c) 2026 Wilson Oon.