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

v3.1.29

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.

Downloads

85

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 (src/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": "^1.0.0"      // the generator — build-time only
  },
  "dependencies": {
    "@rootintf/json-msg": "^4.0.2-api.4" // 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 gen            # all interfaces -> ./packages, from ifacegenconfig.json (pinned)
npm test               # regenerates fixtures (pretest) + ESM runtime tests; `npm run typecheck` checks the typed responder chain

Generation reads the pinned schemas declared in ifacegenconfig.json (see Config mode below); the schemas themselves are authored — already carrying x-base — in the central schemas.readonly store. (C++ headers are generated separately by cpp-iface's own Node-free generator — this generator is npm only.)

Running it as a dependency (npx)

When a consuming project has @rootintf/iface-gen installed, you don't need a script — npm puts the binary on the path (node_modules/.bin/iface-gen, shebang'd so it works on POSIX and Windows). Run it directly from the consumer root:

npx iface-gen --config ifacegenconfig.json                 # generate
npx iface-gen --config ifacegenconfig.json --seed-bases    # generate, adopting any pre-existing output that has no base

npx resolves the local node_modules/.bin/iface-gen first, so it uses the installed version — no download. Equivalents: ./node_modules/.bin/iface-gen … or npm exec -- iface-gen …. Add --no-install to guarantee npx never reaches the registry (it errors if the local dep is missing rather than silently fetching): npx --no-install iface-gen --config ifacegenconfig.json. Config paths resolve relative to the config file, so outputs land in the consumer (not in node_modules).

CLI — two modes:

iface-gen <positional…> [--scope @s] [--prefix p-] [--schemas DIR] [--out-npm DIR] [--delete]
iface-gen --config <file> [--no-debug] [--seed-bases]   # 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)
  --seed-bases    adopt: record a base for an existing IPSME_Bridge output that has none, leaving the
                  output untouched (deliberate one-time opt-in; a plain run errors on a missing base)

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 gen → iface-gen --config ifacegenconfig.json.

{
  "schemas": {
    "Discovery": { "src": "../schemas.readonly/schema_Discovery.json", "sha256": "<sha256sum of the schema bytes>", "debug": false }
  },
  "targets": [
    { "out-npm": "./packages",
      "scope": "@rootintf", "prefix": "iface-", "delete": true, "schemas": ["Discovery"] }
  ]
}

This repo's own ifacegenconfig.json is exactly this shape: it points every entry's src at ../schemas.readonly/schema_<Name>.json and pins each to the .sha256 sidecar shipped alongside it.

  • 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. A blob whose bytes no longer hash to its name (classic cause: a core.autocrlf=true checkout smudging LF → CRLF) is a hard error; --force is the explicit override — it re-reads the src, still verifies it against the pin, and rewrites the blob.
  • LF discipline: schemas must be LF — a CRLF/CR src is rejected (the pin is over the raw bytes), and the merge-managed scaffold diffs on content (merge inputs normalized to LF). Checkout-side, gen writes a * text eol=lf .gitattributes into each managed dir (out-npm, schema-cache) so an autocrlf=true checkout can't smudge the committed artifacts; marker-guarded — a hand-written .gitattributes is never touched.
  • --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.

Introspection — what does this target support?

Config mode also emits one <prefix>introspection package per target (e.g. packages/iface-introspection): the registry of WHAT the target supports — friendly name (the config registry key, e.g. "Discovery-svc") → sha256 of the schema bytes generated from (== the config pin when verified; a debug entry reports the hash of the live bytes actually read). It's the runtime end of the config's pins, so consumers stop hand-transcribing hash literals:

import { INTERFACES, sha256 } from "@rootintf/iface-introspection";   // or the relative generated path

json_body["interfaces"] = { ...INTERFACES };   // a capabilities reply: { "Discovery": "6ddb…", … }
sha256("Discovery-svc");                       // one pin; null when the interface isn't supported

INTERFACES is frozen, keyed by the config names (not $id-derived identifiers), in config order. The package is dependency-free (versioned GEN_VERSION, no -api suffix — the registry has no _v of its own), emitted only when the target generated ≥1 interface, and covered by --delete like any other package. The C++ generator emits the functional twin (<prefix>introspection.h) from the same config — same names, same hashes — so a capabilities reply built from either side matches.


IPSME participant scaffold (merge-on-gen)

Beyond the per-interface npm packages, a config target can generate an editable IPSME participant scaffold — the wiring you'd otherwise write by hand. Add an IPSME block to a target:

{ "root": "app",
  "IPSME": { "main": "App.js", "shell": "electron-renderer", "out": "IPSME" },
  "out-npm": "packages", "scope": "@rootintf", "prefix": "iface-",
  "delete": true, "schemas": ["MessagingEnv", "Discovery", "1Store", "Echo"] }
  • main — the participant filename (default main.js). $main$ in the templates → its stem, so App.js → the component App and interface_App.

  • shell — the participant's runtime context, which selects its SHAPE (required; unknown or missing is a hard error). Three shapes, five shells — each shape is a different answer to the one hard requirement, construct the bridge exactly once per participant lifetime:

    • node / electron-main → a plain class (a hosted object; your entrypoint constructs it once).
    • electron-boot → flat top-level code — THE entry module of electron's main process (the singleton tier's main.ts). A module body executes exactly once per process, so the once-guarantee needs no guard; the rest of the entry (app lifecycle, windows, reflector wiring) composes around the scaffolded block.
    • electron-renderer / browser → a React component (a useRef lazy-init constructs the bridge exactly once — see below).

    The shell names are runtime contexts (not shape names) on purpose: pairs like node/electron-main emit identical shapes today, but the runtime identity is what future env differences hang off — which MsgEnv the bridge imports, and each context's reflector obligations (electron-boot owns Reflector_IPC_main; a renderer rides the IPC reflector wired at its entry; a browser participant needs the ws SharedWorker reflector) — without configs ever migrating.

  • out — directory for the bridge + responders (default IPSME), relative to the target root.

  • root (target-level) — namespaces all the target's write paths (out-npm, the IPSME out, and the base mirror) under <config-dir>/<root>/, so multiple targets never collide.

What it emits

| file | what | |---|---| | <main> (e.g. App.js) | the participant — class, flat entry module, or React component per shell; destructures the responder chain from the bridge | | <out>/IPSME_Bridge.js | wires the chain: responder imports + logr aliases, the LOGR_.wire list, responder declarations, per-base (json_{msg,eff,ack}) dispatch, and the process_msgs reprocess loop | | <out>/Responder_<iface>.js | one per envelope schema — a live handler per message + the base handler_json_{ack,eff,msg} dispatch |

Only envelope schemas get a responder. A schema with ≥1 JSON_Msg/JSON_Eff/JSON_Ack message (an x-base) yields a Responder_<iface>; a plain-JSON schema (Echo, Hyperport) still produces its npm package but no responder. A participant with zero envelope schemas is valid too — the chain just collapses to empty ({ MsgEnv, process_msgs }, []).

These files are yours to edit — merge-on-gen keeps your edits

packages/ is regenerated wholesale (never hand-edit). The scaffold is the opposite: a skeleton you fill in, and re-running gen must not clobber your edits. So each scaffold file is written through a git 3-way merge:

  • base — the pristine generated output from the last run
  • ours — the file on disk now (base + your edits)
  • theirs — the freshly generated output

git merge-file folds template changes into your file, raising conflict markers only where a regenerated line overlaps a line you touched — clean, silent merges everywhere else. The base is kept in a mirror tree next to the config — ifacegen-base/<target>/<out-rel>/<file>.v<version> — each snapshot stamped with the generator version that produced it (a non-source extension, so it never compiles in). Commit ifacegen-base/ alongside your scaffold: it stays out of the compile path but travels with the repo so merges are reproducible.

Per-file status each run: created (first time — writes output + base together) · unchanged · merged (clean fold) · CONFLICTS(n) (resolve the markers, as with any git conflict).

Reading a conflict — the markers are git's diff3 style, so a conflict shows three sections (not two), with the base (common ancestor) in the middle:

<<<<<<< yours (edited)      ← on disk now (base + your edits)
   ...A...
||||||| base (generated)    ← the last pristine generated skeleton (the common ancestor)
   ...B...
=======
   ...C...                  ← the freshly generated output
>>>>>>> generated (new)

Resolve by comparing each side to the base B (the middle):

  • A == B → you didn't touch this region, only the generator did → take C.
  • C == B → the generator didn't change it, only you did → keep A.
  • A and C both differ from B → you each changed it → merge by hand.

That middle (base) section is why diff3 is worth the extra markers: without it you'd see only your version vs the new one and have to guess who changed what.

Conflicts are resolved by hand each run — there is no travelling resolution cache (git rerere). That's deliberate: rerere is local-only (it doesn't survive clone/push), and the honest portable alternative is a DIY preimage→postimage cache committed under ifacegen-base/ — essentially reimplementing rerere. That's held until conflict volume actually justifies the extra machinery.

Adopting a scaffold file that already exists

The invariant is strict — output present ⇒ base must exist, else gen errors. It will not fabricate an ancestor under a file that never descended from one (that's the self-healing trap; it silently corrupts on the first merge). So you don't retrofit a base — you reach the managed state through created, then bring your edits onto the generated skeleton:

cp out/IPSME/IPSME_Bridge.js /tmp/bridge.mine.js   # 1. preserve your current bridge
rm out/IPSME/IPSME_Bridge.js                        # 2. remove it so gen takes the `created` path
npm run gen                                          # 3. [created]: pristine bridge + its base, in sync
git difftool --no-index out/IPSME/IPSME_Bridge.js /tmp/bridge.mine.js   # 4. 2-way MERGE your logic into the generated file (meld/vimdiff/VS Code)
git add out/IPSME/IPSME_Bridge.js ifacegen-base/     # 5. commit the reconciled file AND its base
git commit -m "adopt IPSME_Bridge under iface-gen management"

After step 3 the base equals the pristine output; in step 4 you edit that file, so your bridge genuinely descends from the base — and every later regen is a clean 3-way.

Never paste your old file over the generated stub. The base must be a true ancestor. Drop a from-scratch file on top and record the stub as base, and the first template change merges as a paste-over — silently duplicating or corrupting your code (a failure reproduced and locked in a test). "Conform to the generated bridge" means re-derive your bridge from the skeleton (step 4), not bolt a base under a hand-written file.

For the reverse direction there's --seed-bases (npm run seed): a deliberate one-time opt-in that records a base for existing output that has none, leaving the output untouched. Use it only when you're confident the output already descends from the generated skeleton — otherwise future merges may duplicate.

The React shell: construct-once, and why it doesn't dispose on unmount

The electron-renderer / browser shell constructs the bridge in a useRef lazy-init in the component body (so the bridge, MsgEnv, and responders are available synchronously during render), guarded so it runs exactly once. It deliberately does not dispose the MsgEnv on React unmount:

  • The bridge is a page-lifetime singleton — its _constructed_ guard never resets, so it can't be reconstructed, which makes the owning component page-lifetime by construction. "Unmount" and "window teardown" are the same event in practice; the browser/OS reclaims the transport's socket/channel/OS observer when the page dies.
  • Disposing on unmount only introduces a StrictMode footgun (dev's transient unmount would kill the surviving ref-held bridge) for no real gain.
  • Known mismatch to reconcile later: the bridge returns dispose: () => IPSME_MsgEnv.dispose(), but two transports (broadcastchannel, electron-NSDNC) export no dispose — only per-handler unsubscribe — so calling it there throws. This is another reason dispose is kept out of the shell lifecycle; fix it in the bridge (a no-op dispose on those transports, or have the bridge tolerate its absence). None of the four transports (MQTT, sdbus, broadcastchannel, NSDNC) emit an application-level "leaving" message on teardown, so nothing needs an explicit goodbye. If a transport ever holds something the OS won't reclaim (a SharedWorker, a server session), tear it down on window beforeunload, not on React unmount.

Where the schemas live

The schemas are authored once, centrally, in the read-only schemas.readonly store (a sibling of this repo), each already carrying its x-base annotation and pinned by a .sha256 sidecar. That store is the single source of truth. ifacegenconfig.json references each schema from there by relative path and hash — so a run verifies the exact bytes before generating, and there is no injection step and no per-protocol repos to check out. json-msg's own base schema (schema_json-msg.json) is the sole exception: it lives in json-msg and is not generated (it is the base classes).

Adding a new interface/protocol

The generator reads each schema named in ifacegenconfig.json from schemas.readonly — one file per protocol — and needs nothing else hand-mapped. To add one:

  1. Author schema_<Name>.json in schemas.readonly, with x-base inline. x-base is the only annotation the generator needs (it selects the base class → the constructor + the typed surface): put "x-base": "JSON_Msg" / "JSON_Eff" / "JSON_Ack" on each envelope message; plain-JSON messages get no x-base. Generate its .sha256 sidecar (sha256sum schema_<Name>.json).
  2. Register it in ifacegenconfig.json: add a schemas entry ("<Name>": { "src": "../schemas.readonly/schema_<Name>.json", "sha256": "<pin>" }) and list "<Name>" in a target.
  3. 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.
  4. Naming rule (enforced): the generated name is the message's $id leaf segment verbatim (only non-identifier chars sanitised to _). An x-base (Msg-family) message must be authored JSON_… in its $id; a plain-JSON message must not start with JSON_. A segment that contradicts its x-base is a hard error.
  5. npm run gen (or iface-gen <Name>) → packages/iface-<Name>.
  6. Keep the C++ twin in sync: cpp-iface's generator reads the same schemas.readonly files (they already carry x-base), so no copy step — just add the schema to its config too. See that repo's README.

Worked example — MessagingEnv. schemas.readonly/schema_MessagingEnv.json carries x-base: "JSON_Msg" on both messages, whose $id segments are authored JSON_MsgCtrl / JSON_MsgFilter. npm run gen emits @rootintf/iface-messagingenv exporting JSON_MsgCtrl and JSON_MsgFilter (both JSON_Msg, VERSION "4"), 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 package — npm 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
    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 (^4.0.2-api.4) 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. 1.0.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: ^1.0.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":"3" }   (id/dt via json-msg)
Store.JSON_MsgQuery.validate(msg);   // true   — the IPSME interest-management drop primitive
Store.VERSION;                  // "3"

Base classes, the type layer, and the responder chain

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

Naming. A generated type is the schema's $id leaf segment verbatim (non-identifier chars sanitised to _). The schema carries the final name itself: a Msg-family message (x-base JSON_Msg/JSON_Eff/JSON_Ack) is authored JSON_… (JSON_MsgDiscover, JSON_EffAnnounce); a plain-JSON message has no prefix (EchoRequest, hyperport). An x-base message whose segment doesn't start with JSON_, or a plain one that does, 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_Eff.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_EffAnnounce.validate(msg: JSON_Eff): msg is JSON_EffAnnounce
    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_EffAnnounce.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_Eff } from "@rootintf/json-msg" and extends them; index.mjs/index.cjs import the same names as values and call JSON_Msg.merge / JSON_Eff.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) | the bundle's top-level $id leaf segment (source filename as fallback) | | 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 (1.0.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.