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

publish-preflight

v0.2.0

Published

Pre-publish check that runs your package the way a consumer would

Readme

publish-preflight

CI npm version license node

If someone installs the package you are about to publish, does it work?

publish-preflight answers that one question the only way that cannot drift: it packs your real artifact with npm pack, installs the tarball into a clean temp project, and loads it the way your own package.json claims it can be loaded. Static linters re-implement Node's resolution rules and must chase every change to them; a tool that runs the real thing inherits the rules for free.

It also covers the metadata nobody else checks: whether the name is publishable at all, whether the declared URLs exist, and whether a rename left the old name behind in your lockfile or badges.

npx publish-preflight              # in the package directory
npx publish-preflight --name my-idea   # or just ask whether a name is free

Where it fits

Use all three; they do not overlap:

publint            -> is my package.json correct?           (static)
@arethetypeswrong  -> do my types resolve everywhere?       (static simulation)
publish-preflight  -> does the artifact actually work
                      when installed?                       (packs, installs, executes)

Real defects in published packages that only execution can find: an ESM build doing a named import from a CommonJS dependency (works in the CJS build, fails in the ESM one), a declared entry importing something that is only in devDependencies, a dist/ that no longer matches src/. publint sees a valid exports map in all three cases.

What it checks

In order - the failure that costs the most to discover late runs first:

| Check | Question | Severity | | --- | --- | --- | | name-publishable | Can this name be published at all? The registry strips punctuation (-, ., _) from both sides before comparing, so lodash-merge is blocked by lodash.merge. Runs only for a first release or a rename. | error | | version-publishable | Is this exact version already on the registry? Is a prerelease about to become latest? | error / warn | | tarball-inventory | Is every path referenced by main, module, types, exports, bin and files actually in the tarball? Ships tests? README? A 3x size jump? | error / warn / info | | stale-dist | Does a fresh npm run build (in a temp copy, never in your repo) produce what you are about to publish? | error | | clean-install | Does the tarball install into a project that knows nothing about your repo? | error | | load-claims | Can the package be loaded with require() / import() exactly as its manifest claims? The root entry and every literal subpath in exports. Only claimed loaders run. | error | | bin-smoke | Does the command this package puts on a PATH start? Every bin target needs a shebang, and a CRLF shebang is reported against the bytes you publish. With --run-bin "<args>" each bin is started and only a startup failure counts. | error / warn / info | | ts-specifiers | Does anything the consumer receives point at a TypeScript source? Node refuses to strip types under node_modules, so such a package installs and then cannot run. | error / warn | | types-resolution | Do the shipped types resolve for a consumer under node16 and bundler resolution? Two tsc --noEmit runs, matched to the runtime claims. | error | | declared-urls | Do repository, homepage, bugs exist? A dead repository is an error (it is the provenance claim on your npm page); a dead homepage is a warning; a redirect landing on a different domain is a warning (that is what an expired, parked project domain looks like). | error / warn | | license-coherence | Does the license field match the shipped LICENSE file? | warn / info | | rename-leftovers | Is the previous name still in package-lock.json, shipped files, or declared URLs? Detected automatically from a lockfile/manifest name mismatch, or via --previous-name. | error / warn | | readme-badges | Do the README badges point at this package and this repository? | warn / info |

False positives are treated as bugs

A release gate that cries wolf gets || true appended within a week. Every case where a load failure is correct behaviour is a named, documented, fixture-tested exemption:

  • types-only-package - the root export resolves only a types condition; loading it must fail.
  • no-root-entry - the package deliberately exposes only subpaths.
  • host-provided-module - the missing module is injected by a host the package declares in engines (e.g. vscode), or is a known host module (react-native).
  • optional-peer-dependency - the missing module is an optional peer.
  • browser-only-entry - the entry needs a DOM global; reported as a warning, not an error.

The same contract governs subpaths: only a literal subpath whose target is runtime JS (or JSON, for require()) makes a testable claim. Wildcard patterns (./icons/*) are not expanded - a wrong expansion is how false positives happen - and are disclosed as skipped, as is anything beyond the first 64 subpaths.

When the tool is unsure it says so at warn, with the reason. Networked checks never convert a request failure into a verdict: nothing is called dead on one attempt (three tries with backoff), a registry error is "could not verify", never "name is free", and all requests share one concurrency limit.

name-publishable can only prove the negative. When no spelling collides it still tells you what it cannot know: mixed-boundary spellings are not enumerable from the client, and the registry has at least one undocumented similarity rule. Only npm publish proves the positive.

Usage

npx publish-preflight              # run in the package directory
npx publish-preflight --json      # machine-readable report
npx publish-preflight --offline   # skip every check that needs the network
npx publish-preflight --no-build  # do not run the build script; verify what is on disk
npx publish-preflight --ignore-scripts        # clean install without lifecycle scripts
npx publish-preflight --previous-name md-render   # scan for leftovers of an old name
npx publish-preflight --only name-publishable     # one check
npx publish-preflight --only load-claims,types-resolution   # or several
npx publish-preflight --run-bin="--help"                    # start each bin too

Before there is a package: --name

Naming happens before there is anything to pack, so --name answers that question on its own - no package directory, nothing packed or installed, about a second:

npx publish-preflight --name express.js     # error: name 'express.js' is already taken
npx publish-preflight --name lodash-merge   # error: blocked by 'lodash.merge'
npx publish-preflight --name my-idea        # info: no blocker found (not a guarantee)

Exit codes are the usual ones, so a naming decision can gate a script. The finding still says what it cannot know: a clean answer means no collision was found, not that npm will accept the name.

--only takes check ids (comma-separated or repeated) and filters what the report evaluates; the checks left out are listed as skipped rather than dropped, so a partial run never reads like a full one. Stages a selected check depends on still run without reporting: --only load-claims packs and installs, because there is nothing to load until it does. Only name-publishable and version-publishable need neither, which makes them a fast standalone question - is this name free, is this version already out - answerable in about a second on a directory containing nothing but a package.json.

Exit codes - CI can tell "your package is broken" apart from "the checker broke":

| Code | Meaning | | --- | --- | | 0 | no errors | | 1 | at least one finding of severity error | | 2 | the tool itself failed (no package.json, npm pack failed, ...) |

API

import { preflight } from 'publish-preflight';

const report = await preflight({ cwd: process.cwd(), offline: false });
report.ok;        // no findings of severity 'error'
report.findings;  // { check, severity, title, detail, evidence?, exemption? }[]
report.skipped;   // { check, reason }[] - checks that did not run, and why

// Same selection as --only; an unknown id throws PreflightToolError.
await preflight({ cwd, only: ['name-publishable'] });

// The --name mode: same Report shape, no package directory read.
import { checkName } from 'publish-preflight';
const name = await checkName({ name: 'my-idea' });
name.ok; // false when the name is taken or blocked by an existing spelling

Every finding carries the evidence that produced it - the command and its first error line - so you can verify it in ten seconds. Reports are deterministic: for the same tarball and environment the JSON output is byte-identical apart from durationMs (temp paths are normalised to <tmp> and <pkg>).

In a release flow

This tool never publishes anything; call it from whatever does:

// package.json
"scripts": {
  "prepublishOnly": "npm run lint && npm run test && npm run build && npx publish-preflight"
}

Security posture

--run-bin=<args> starts every executable the package declares, with the arguments you give it, in the temp install directory. It is opt-in for that reason: nothing runs your CLI unless you ask.

The clean install runs lifecycle scripts by default, and load-claims imports your package - because that is exactly what a consumer's npm install and first require() do. The code that executes is your package and its dependency tree, the same code your own npm install already ran on your machine. Do not point this tool at a package you would not install. --ignore-scripts disables install scripts at the cost of fidelity.

The tool writes only to a temp directory it deletes afterwards (the stale-dist build runs in a temp copy of your project, never in your working tree), and it never edits the package it audits.

Non-goals

Version bumping, tagging and publishing (np, release-it, semantic-release), static package.json linting (publint), the full types matrix (@arethetypeswrong/cli), vulnerability scanning (npm audit), bundle-size budgets (size-limit). Run this alongside them, not instead of them. No plugin system: the check list is the product, and the false-positive contract is only enforceable over a known list.

Requirements and dependencies

Node >= 20 and the npm CLI on your PATH. Zero runtime dependencies: the registry is spoken to with the global fetch, names are probed with ~40 lines that match npm-name >= 8.1.1 semantics, and TypeScript (for types-resolution) is borrowed from your own node_modules or fetched --no-save into the temp project only when the package under test declares types.

License

MIT