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

@rootintf/iface-gen

v0.1.8

Published

Single-source generator: one JSON Schema -> one granular npm package + one C++ header. The interface is the schema; the package/header is a generated artifact.

Readme

npm-iface (@rootintf/iface-gen)

The generator. One JSON Schema → one granular npm package + one C++ header. The interface is the schema; the package/header is a generated artifact you never write by hand — yet each interface stays separate, so a service uses only the few it needs, never a catalog of hundreds.

This repo is the generator (gen.cjs) plus a local packages/ output you can inspect.


Two ways to ship interfaces

Model B — publish the generator, generate per project ← recommended for you

You publish one package (this generator, carrying the schemas). Each consuming project generates only the interfaces it needs at build time, and gets the shared runtime from the registry:

// consumer package.json
{
  "devDependencies": {
    "@rootintf/iface-gen": "^0.1.0"      // the generator — build-time only
  },
  "dependencies": {
    "@rootintf/json-msg": "^2.2.2"       // the ONLY runtime dep — base types + constructors
  },
  "scripts": {
    "gen":   "iface-gen Discovery 1Store --delete --out-npm ./src/generated",
    "build": "npm run gen && <your build>"
  }
}
// consumer source — import the LOCAL generated code; it pulls the shared runtime above
import * as D from "./src/generated/iface-Discovery/index.mjs";       // ESM (bundler / electron)
if (D.JSON_MsgDiscover.validate(msg)) handle(msg);
// under CJS (e.g. ts-jest):  const D = require("./src/generated/iface-Discovery/index.cjs");

Why this keeps granularity: the generator + all schemas are a devDependency / build-time tool; your runtime graph is only (a) the interfaces you generated — local code — plus (b) one shared runtime, json-msg, deduped. The catalog of all interfaces never enters it.

Make generation an explicit gen/build script, not an npm install hook — reproducible and npm ci --ignore-scripts-safe. Commit ./src/generated, or .gitignore it and run gen before build — your call. (Interfaces that are plain JSON — no envelope — need neither runtime dep.)

Model A — publish each interface

Publish every @rootintf/iface-<name>; consumers just npm i @rootintf/iface-1store. No toolchain at the consumer, per-interface registry versioning — but you publish N packages. Better for third-party / external consumers who shouldn't run your generator.

Both models emit the same granular, dual ESM+CJS packages — envelope interfaces depend only on json-msg; plain-JSON ones (Echo, Hyperport) have no deps. The only difference is who runs the generator and when.


Running the generator

npm install            # ajv + typescript — build/test only
npm run vendor         # copy the protocol schemas into ./schema, injecting x-base (on schema change)
npm run generate       # all interfaces -> ./packages
npm test               # ESM runtime tests; `npm run typecheck` checks the typed responder chain

(C++ headers are generated separately by cpp-iface's own Node-free generator — this generator is npm only.)

CLI — two modes:

iface-gen <positional…> [--scope @s] [--prefix p-] [--schemas DIR] [--out-npm DIR] [--delete]
iface-gen --config <file> [--no-debug]            # bare `--config` prints a starter config to stdout

  positional…     SCHEMA FILE PATHS (./schema_Discovery.json) or interface NAMES (Discovery) resolved
                  against --schemas. A positional is a PATH if it ends .json / has a separator / exists;
                  else a NAME. Paths and names can't be mixed. (no positional + no --delete = all names)
  --scope NAME    npm scope; default @rootintf ("" = unscoped)
  --prefix STR    package-name prefix; default iface- (e.g. protocol- for drop-in protocol-* names)
  --schemas DIR   dir for NAME resolution; default ./schema
  --out-npm DIR   where to write the <prefix><Name>/ packages; default ./packages
  --delete        rsync-style: prune any OTHER generated package in --out-npm so it matches THIS run's
                  set (no positional + --delete = wipe). Only gen-shaped dirs (package.json + index.* +
                  validate.*) are touched — unrelated files/dirs are invisible to it.
  --config FILE   generate from a config (registry + targets) instead of positionals; bare --config = template
  --no-debug      fail if any registry entry has debug:true (CI guard)

Ad-hoc examples:

iface-gen ./schema_Discovery.json ./schema_Echo.json --out-npm ./src/generated   # SCHEMA_FP (paths)
iface-gen Discovery --delete --out-npm ./gen                                      # name mode + prune
iface-gen --delete --out-npm ./gen                                                # wipe ./gen

Config mode (pinned, multi-target)

A schema registry (each defined once, pinned by sha256) + per-output targets. One file routes every output; npm run geniface-gen --config ifacegenconfig.json.

{
  "schemas": {
    "Discovery": { "src": "<url|path>", "sha256": "<sha256sum of the schema bytes>", "debug": false }
  },
  "targets": [
    { "schema-cache": "./src/main/schemas", "out-npm": "./src/main/generated",
      "scope": "@rootintf", "prefix": "iface-", "delete": true, "schemas": ["Discovery"] }
  ]
}
  • Registry: each schema's src + pinned sha256 lives once; targets select by name (a name with no registry entry is a hard error). A schema used by several targets is pinned once, fanned out.
  • Verify + cache: for each pinned schema, gen uses schema-cache/<sha256>.json (content-addressed) if present, else fetches the URL / reads the path, verifies the bytes against the pin (hard error on mismatch), and writes the cache blob. Commit schema-cache → offline, reproducible builds.
  • --delete mirrors both the packages and the cache per target.
  • "debug": true skips the check for one schema and reads its src live (loud WARNING: banner); --no-debug refuses to run if any are set. The pin is the plain sha256sum of the schema bytes — keep the file byte-stable and re-pin by hand when it changes (there is no --freeze).

iface-gen --config with no file prints a starter config to fill in.

Where the schemas live (the one decision for Model B)

A published generator must carry the schemas (a consumer won't have the npm-protocol-* repos checked out). Two options:

  • Bundle them into @rootintf/iface-gen (a schema/ dir; point --schemas at it). One published package — simplest, matches "publish one thing."
  • A separate @rootintf/iface-schemas data package the generator reads. Cleaner DRY if the schemas are also consumed elsewhere, but it's two packages.

Either way the schema is the single source of truth; today gen.cjs reads the originals in place (read-only), which is right for in-repo development.

Adding a new interface/protocol

The generator reads schema/schema_<Name>.json — one file per protocol — and needs nothing else hand-mapped. To add one:

  1. Get the schema into schema/, carrying x-base. x-base is the only annotation the generator needs (it selects the base class → the constructor + the typed surface). Two ways:
    • Vendored (in-repo protocols): add the source path to vendor.cjs's SRC, and add each JSON_Msg-family message to its BASE map (MsgX: "JSON_Msg" / "JSON_MsgEffect" / "JSON_MsgAck"; plain-JSON messages get no entry). Then npm run vendor copies the file in and injects x-base.
    • Authored directly: drop a schema/schema_<Name>.json that already has x-base inline on each Msg-family message. (No vendor step needed.)
  2. Schema shape the generator expects (draft 2019-09): a top-level schemas map keyed by $id URLs; one sub-schema per message; for a versioned protocol a …/definitions sub-schema with "_v": { "const": "N" }, and each envelope message has a required prop whose $ref ends in …/_v.
  3. Naming rule (enforced): a Msg-family message's $id leaf segment must start with Msg (→ JSON_Msg…); a plain-JSON message becomes JSON_<segment>. A family segment that doesn't start with Msg is a hard error.
  4. npm run generate (or iface-gen <Name>) → packages/iface-<Name>.
  5. Keep the C++ twin in sync: copy the vendored schema/schema_<Name>.json (the one with x-base) into cpp-iface/schema/ — see that repo's README. Do not copy the raw npm-protocol-* file; it has no x-base, so its Msg messages would silently demote to plain JSON.

Worked example — MessagingEnv. vendor.cjs has SRCnpm-protocol-MessagingEnv.git/schema/schema_MessagingEnv.json and BASE{ MsgCtrl: "JSON_Msg", MsgFilter: "JSON_Msg" }. npm run vendor writes schema/schema_MessagingEnv.json with x-base on both messages; npm run generate emits @rootintf/iface-messagingenv exporting JSON_MsgCtrl and JSON_MsgFilter (both JSON_Msg, VERSION "1"), each with .validate + .merge.


Publishing each protocol as its own package (Model A, in detail)

You don't have to adopt Model B. Each generated @rootintf/iface-<name> is a complete, standalone npm packagenpm publish it exactly like a hand-written one. The generator already removed the authoring burden (package.json, dual build, validators), so per-package publishing now costs only the publish step, and your consumers' experience is unchanged:

npm i @rootintf/iface-discovery      # no toolchain, per-package semver — same as the old protocol-* packages

Two ways to organize it:

  1. One git repo per protocol (your current topology). Generate into each repo and publish from there:
    iface-gen Discovery --out-npm ../npm-protocol-discovery.git --no-cpp
    cd ../npm-protocol-discovery.git && npm publish
    The repo's contents become generated rather than hand-written; the per-repo, per-package model is otherwise untouched.
  2. One monorepo (packages/* with workspaces) — publish each package from there (e.g. changesets publishes only the ones whose version changed).

What stays separate:

  • @rootintf/json-msg is published once, on its own — the shared runtime + base types. Every envelope iface-* package depends on it (^2.2.2) and installs it transitively. It is the one thing that is not per-protocol.
  • Versioning is per-package: each publishes as <iface-gen version>-api.<_v> (e.g. 0.1.0-api.3). Bump a schema's _v → only that package's -api.N moves → republish just that one. Independent per-protocol releases, exactly like now. (Consumers pin with the tag in range: ^0.1.0-api.3.)

Naming: by default the generator emits @rootintf/iface-<name>. For drop-in replacements of existing @rootintf/protocol-<name> consumers (no import changes on their side), pass --prefix protocol- (and --scope @yourorg if it differs); --scope "" emits unscoped packages:

iface-gen Discovery --prefix protocol-     # -> @rootintf/protocol-discovery  (dir protocol-Discovery)

Model A vs Model B is purely who publishes what: Model A = publish each package (familiar, simplest for consumers, N publishes per release); Model B = publish only the generator, consumers generate locally (one publish, but consumers run a build step). Same generated artifacts either way.


What it emits (per interface)

A granular, dual ESM+CJS package (no build step — ajv-standalone is inlined). Envelope interfaces depend only on @rootintf/json-msg; plain-JSON ones have no deps:

| file | what | |---|---| | validate.mjs / validate.cjs | ajv-standalone validators (ESM + CJS), one per message | | index.mjs / index.cjs | validate_<Msg>(), and for envelope messages create_<Msg>(body) + VERSION | | index.d.ts | types (base types imported from json-msg) | | schema/schema_<Name>.json | the source schema, copied in | | package.json | @rootintf/iface-<name>, dual exports (import/require/types), granular |

const msg = Store.JSON_MsgQuery.merge({ "search-type": "text", q: "hello" });
//   -> a real JSON_Msg: { id, referer, dt, query:{…}, "1Store":"1" }   (id/dt via json-msg)
Store.JSON_MsgQuery.validate(msg);   // true   — the IPSME interest-management drop primitive
Store.VERSION;                  // "1"

Base classes, the type layer, and the responder chain

Each message declares its base class in the schema via x-base (JSON_Msg, JSON_MsgEffect, or plain JSON). That single annotation drives both the constructor and the types — but never the runtime validation:

Naming. A generated type is JSON_ + the schema's $id leaf segment. A JSON_Msg-family message (x-base JSON_Msg/JSON_MsgEffect/JSON_MsgAck) names its segment Msg…, so it becomes JSON_Msg… (MsgDiscoverJSON_MsgDiscover, MsgAnnounceJSON_MsgAnnounce); a plain-JSON message becomes JSON_<segment> (EchoRequestJSON_EchoRequest). A family message whose segment doesn't start with Msg is a hard generator error. Each JSON_<Type> is a namespace value (.validate, .merge / .create_with_cause_merge) declaration-merged with the same-named type — the same surface as the json-msg base classes (JSON_Msg.merge, JSON_MsgEffect.create_with_cause_merge) and the C++ twin's static methods.

  • <Type>.validate stays delta-only. It checks only the message's own fields; the base is validated once, upstream in the responder chain, and never re-checked at the leaf. (allOf/$ref composition would re-validate the base on every message — exactly the waste the chain avoids — so it's deliberately not used.)
  • <Type>.validate is typed as a predicate whose input is the base:
    JSON_MsgAnnounce.validate(msg: JSON_MsgEffect): msg is JSON_MsgAnnounce
    so the compiler refuses to check a leaf delta before its base is confirmed — the chain ordering is compiler-enforced, and inside if (JSON_MsgX.validate(msg)) the handler gets the exact narrowed type. See demo/responder.ts + npm run typecheck.
  • <Type>.merge / .create_with_cause_merge delegate to json-msg, so envelopes are real; effects take the causing message first: JSON_MsgAnnounce.create_with_cause_merge(cause, body).

Everything comes from @rootintf/json-msg (single source of truth) — there is no intermediate package. The generated leaf .d.ts does import type { JSON_Msg, JSON_MsgEffect } from "@rootintf/json-msg" and extends them; index.mjs/index.cjs import the same names as values and call JSON_Msg.merge / JSON_MsgEffect.create_with_cause_merge directly. json-msg exports each name as both a value (the constructor namespace) and an interface (via declaration merging), so one import is both the constructor and the extendable type. Envelope packages depend only on json-msg; plain-JSON packages (Echo, Hyperport) have no deps and no create.

Dual ESM + CJS. Every generated package ships index.mjs + index.cjs (and validate.mjs + validate.cjs) behind an exports map (import/require/types). So bundlers use the ESM build and CJS consumers (ts-jest, plain require) resolve too — a true drop-in.

Conventions the generator reads (nothing hand-mapped)

| derived | from | |---|---| | interface name (1Store) | source filename schema_1Store.json | | message names (MsgQuery) | last segment of each sub-schema $id | | VERSION ("1") | the _v const in the bundle | | version field ("1Store") | the required prop whose $ref ends in …/_v | | create body key | the other required prop(s) | | package version (0.1.0-api.3) | iface-gen's version + -api.<_v> (bare base when no _v) |

Non-envelope messages (arrays, oneOf) get a validate_ only — no create_, no VERSION. Output is regenerated wholesale: never hand-edit packages/ — change the schema and re-run.