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

@aiquants/platform-packaging

v0.1.1

Published

Build tooling that turns a Go command into per-platform npm packages (esbuild/Biome-style os+cpu split with optionalDependencies, bin-field executable bits, and publish-order guards)

Readme

@aiquants/platform-packaging

Build tooling that turns a Go command into per-platform npm packages, using the same distribution model as esbuild, Biome, and turbo:

  • one small parent package (JavaScript only, no binary),
  • one <parent>-<platform>-<arch> package per target holding exactly one binary,
  • the parent references them through optionalDependencies, so npm/pnpm/yarn downloads only the package matching the installing machine.

Why this exists

Shipping a Go binary inside a single npm package forces every consumer to download every platform's binary, breaks the executable bit unless a postinstall chmod runs (which pnpm 10+ blocks by default), and pins the published binary to whatever machine ran the publish. The platform-split model fixes all three, but doing it by hand is a minefield of publish-ordering and lifecycle-script pitfalls. This package encodes the model once — it drives @aiquants/storage-api and @aiquants/markdown, and works the same way in any repository.

Usage

Add the dev dependency (workspace:* inside this monorepo, a normal version range anywhere else):

pnpm add -D @aiquants/platform-packaging

Requirements: Node >= 20 and a Go toolchain on PATH (the builder shells out to go build with per-target GOOS/GOARCH and CGO_ENABLED=0).

Then create two thin scripts in the consuming package:

// scripts/build-platform-packages.mjs
import { runPlatformPackaging } from "@aiquants/platform-packaging"

runPlatformPackaging({
    packageRoot,             // the npm package root
    goModuleDir,             // the Go module directory
    goPackage: "./cmd/foo",  // go build target (use "." for a root main.go)
    binaryBaseName: "foo",   // binary name; ".exe" is appended on Windows
})
// scripts/publish-platform-packages.mjs
import { publishPlatformPackages } from "@aiquants/platform-packaging"

publishPlatformPackages({ packageRoot })

And wire the lifecycle in package.json:

{
  "scripts": {
    "build:platforms": "node scripts/build-platform-packages.mjs",
    "publish:platforms": "node scripts/publish-platform-packages.mjs",
    "prepack": "node scripts/build-platform-packages.mjs --inject",
    "postpack": "node scripts/build-platform-packages.mjs --restore",
    // MUST stay cheap. In a pnpm workspace with injectWorkspacePackages,
    // every `pnpm install` runs prepublishOnly for injected packages
    // (measured) — use a cached build command (turbo shown here; any
    // fast/cached equivalent works) and NEVER put build:platforms
    // (6 cross-compiles) here.
    "prepublishOnly": "pnpm exec turbo run build --filter=<pkg>",
    "publish:patch": "pnpm run typecheck && pnpm run --if-present test && pnpm version patch --no-git-tag-version --no-git-checks && pnpm run build:platforms && pnpm run publish:platforms && pnpm publish --no-git-checks"
  }
}

Lifecycle facts (measured on pnpm 11)

Which scripts fire in which context decides where work and guards may live:

| Context | Scripts that run | npm_command | | --- | --- | --- | | pnpm install (injected workspace dep, dedupeInjectedDeps: false) | prepare, prepublishOnly | — | | pnpm pack | prepack, prepare, postpack | pack | | pnpm publish (incl. --dry-run) | prepublishOnly, prepack, prepare, postpack | publish | | pnpm --prod deploy | none of the above | — |

Consequences baked into this package:

  • prepublishOnly runs on every install, so it must be turbo-cached and must never cross-compile. It exists so that install-time injection packs a built package (turbo rebuilds later do not re-sync injected copies).
  • prepack/postpack do not run on install, so --inject/--restore and their guards never slow down or break installs.
  • --inject distinguishes publish from pack via npm_command, enabling a publish-only registry guard while pnpm pack (tarball E2E of unpublished versions) keeps working.

Rules that are easy to get wrong

  • Platform packages must be published before the parent. The parent pins them at its own version, so publishing the parent first leaves a window where installs cannot resolve a binary. publish:* must call the platform publisher first.
  • optionalDependencies is never committed. It points at a version that does not exist yet; committing it dirties the lockfile and breaks --frozen-lockfile CI. --inject writes it at prepack, --restore reverts it at postpack.
  • The executable bit comes from the bin field, not from postinstall. npm pack normalizes file modes to 0644, but package managers restore 0755 for bin targets. A postinstall chmod is additionally unreliable: pnpm 10+ refuses dependency lifecycle scripts unless the consumer allow-lists them.
  • GOOS/GOARCH are pinned per target and CGO_ENABLED=0 is forced. Without this the publisher's own machine decides what ships, and a dynamically linked binary breaks on any host with an older glibc.

Editing this package (monorepo-internal note)

External consumers can skip this section. The published entry point is dist/index.mjs, a single minified bundle that pnpm run build produces from src/ with esbuild; src/ itself is not shipped. This monorepo also resolves workspace dependencies as injected hardlink copies (injectWorkspacePackages: true), so a consumer keeps using the copy it was installed with. After editing anything under src/, run pnpm run build in this package — it rebuilds the bundle, and syncInjectedDepsAfterScripts then refreshes every consumer's copy. Skipping the build means consumers silently run the previous bundle.

Modes

| Command | Effect | | --- | --- | | node scripts/build-platform-packages.mjs | Cross-compiles all targets into platforms/<platform>-<arch>/. | | ... --inject | Backs up package.json, adds optionalDependencies for every target. | | ... --restore | Restores the backed-up package.json and deletes the backup. | | node scripts/publish-platform-packages.mjs | Publishes every generated platform package, skipping versions already on the registry. |

Each mode fails loudly rather than doing something plausible-but-wrong:

  • An unrecognized mode exits 1 instead of falling through to a build. Otherwise a typo in the prepack argument would publish a parent with no optionalDependencies, and every consumer would get "binary not found".
  • A leftover backup (a previous pack/publish crashed before postpack, so the manifest is probably still injected) is handled by version: same version → self-heal (restore the pristine backup, then re-inject, with a warning); different version → exit 1, because silently restoring would resurrect the old version.
  • --inject requires a non-empty dist/ — packing without a build would publish a tarball whose entry points do not exist.
  • --inject runs a publish-only registry guard: when npm_command === "publish", every platform package must already exist on the registry at the parent's version, or it exits 1. This makes even a raw pnpm publish unable to ship a parent whose binaries are missing. pnpm pack is exempt (unpublished versions are normal there). The guard is tri-state: a 404-class answer (ERR_PNPM_PACKAGE_NOT_FOUND / ERR_PNPM_FETCH_404, measured on pnpm 11) means unpublished and prints the publish-first instruction; any other query failure (network down, auth, timeout) is reported separately as could not verify so an outage is never misdiagnosed as "not published". Because pnpm view can keep serving a stale cached 404 right after a successful publish (observed on the real 0.3.0 publish, 2026-07-30), every non-published verdict is confirmed by a direct, cache-free registry fetch before the guard concludes; only when that fetch cannot answer does the pnpm verdict stand. pnpm publish --dry-run also passes through this guard (dry-run is not detectable from lifecycle env — measured); for that rehearsal, and for verified-but-unreachable emergencies, set PLATFORM_PACKAGES_SKIP_PUBLISH_CHECK=1.
  • The publisher waits for registry visibility after publishing: npmjs is eventually consistent, and on the real 0.3.0 publish the six packages took ~60 s to become fetchable after the publish API returned success. Without the wait, the parent's prepack guard fails on every first attempt. The publisher polls the registry directly (default: 5 s interval, 180 s timeout — tunable via PLATFORM_PACKAGES_VISIBILITY_TIMEOUT_MS / PLATFORM_PACKAGES_VISIBILITY_INTERVAL_MS) and exits 1 if visibility is not reached, so the chain stops before the parent rather than at it. PLATFORM_PACKAGES_SKIP_PUBLISH_CHECK=1 skips the wait too.
  • Direct fetches honor PLATFORM_PACKAGES_REGISTRY_URL (tests point it at a local stub), then npm_config_registry, then https://registry.npmjs.org.
  • The publisher requires the platforms/ directory names to match the target set one-to-one (<platform>-<arch> for every target, nothing extra) and exits 1 listing what is missing or unexpected. A bare count check would let "one stray leftover + one missing target" slip through; the name check closes that, so a skipped or partial build:platforms cannot let the parent ship ahead of its binaries.

Tests

pnpm run test runs a hermetic suite (tests/guards.test.mjs, node --test) covering every guard above: mode validation, inject/restore byte-exact round trip, crash-leftover self-heal (same version) vs refusal (version drift vs corrupted backup), dist presence, all registry-guard outcomes (including the stale-pnpm-cache-but-actually-published case), and the publisher's visibility wait (success and timeout). Registry states are simulated by a fake pnpm placed first on PATH (tests/helpers/fake-bin/pnpm) plus a metadata stub server (tests/helpers/registry-stub.mjs) targeted via PLATFORM_PACKAGES_REGISTRY_URL — the stub runs as a separate process because spawnSync blocks the test's own event loop. The suite needs no network and cannot be fooled by pnpm's metadata cache. Guards call process.exit(1), so every case runs through a subprocess driver (tests/helpers/*-driver.mjs) and asserts exit codes plus on-disk manifest state. Cross-compilation (--build) is out of scope here — that is covered by the real-tarball E2E documented in the consuming packages' test specs.