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

@visionary_software/contrax-transformer

v2026.5.10

Published

Contrax TypeScript-Compiler Transformer — enforces @precondition / @postcondition JSDoc contracts at tsc build time.

Downloads

70

Readme

contrax-transformer — Compile-time Contract Enforcement (TypeScript line)

The TypeScript-Compiler Transformer that enforces contrax @precondition / @postcondition JSDoc contracts as hard tsc build errors, before the code ever runs. Mirror of Java's ContractProcessor (annotation processor), with the same recognition + dispatch + emission split — recognition lives in contrax-annotations's check, emission lives in violate, this engine is the AST plumbing that picks the right ts.SyntaxKind nodes to dispatch at.

This is the TypeScript line. The Java line lives on java; the Kotlin K2/FIR port on kotlin.

Install

bun  add @visionary_software/contrax-transformer
npm  i   @visionary_software/contrax-transformer
pnpm add @visionary_software/contrax-transformer
yarn add @visionary_software/contrax-transformer

Direct dependency: @visionary_software/contrax-annotations for check + the SPI vocabulary types. Peer: typescript. Loaded via ts-patch (the contrax-transformer expects to run as a ProgramPattern plugin in a tspc-driven build).

Usage

In tsconfig.json:

{
  "compilerOptions": {
    "removeComments": false,
    "plugins": [
      { "transform": "@visionary_software/contrax-transformer" }
    ]
  }
}

In package.json scripts, swap tsc for tspc. The package ships a bunx contrax-init adoption CLI that does both edits idempotently:

bun add -D @visionary_software/contrax-transformer
bunx contrax-init             # apply the edits
bunx contrax-init --dry-run   # preview the edits without writing

contrax-init is always-apply and idempotent; re-running on an already-configured project reports nothing to do. Adopters never have to hand-edit either file.

Configuration

The transformer accepts a TransformerConfig:

interface TransformerConfig {
  readonly enforcements?: ReadonlyMap<string, Enforcement>;
  readonly discoverFrom?: string;  // path to a directory whose node_modules to scan
}

Two ways to populate the Enforcement registry, combinable:

  1. Auto-discovery (the analog of Java's ServiceLoader.load(Enforcement.class)): set discoverFrom to your project root. The transformer walks <discoverFrom>/node_modules for packages whose package.json carries a contrax.enforcements field pointing at the module that exports the Enforcements (Babel/PostCSS-style ecosystem convention — name-agnostic). Each declared module is required, and every callable named export is registered under its export name. An enforcement-providing package looks like:

    {
      "name": "@visionary_software/contrax-enforcements",
      "main": "./dist/index.js",
      "contrax": { "enforcements": "./dist/index.cjs" }
    }

    Adopting it = bun add @visionary_software/contrax-enforcements. No code change.

  2. Explicit map (for tests, power users, or one-off overrides): pass an enforcements map directly. Explicit entries override discovery on key collision.

Lookup keys match the export names users reference in @precondition <Name> / @postcondition <Name>.

Discovery is silent on missing or invalid entries

The walk is permissive at every boundary:

  • Package without a contrax.enforcements field — skipped (most packages aren't Enforcement providers).
  • Field present but the relative path doesn't resolve — skipped (stale install / partial publish).
  • Entry file present but require() throws — propagates (a broken Enforcement bundle is a build-time programmer error worth surfacing).
  • Entry's named exports include non-callables — non-callables are filtered out, callables registered.

This matches the dispatch behavior in contrax-annotations's check(): a use site referencing an enforcement that didn't make it into the registry is a silent pass, not a build failure. Failing the build because a CI agent forgot to install an enforcement plugin would be operationally fragile.

The XMOD-3 meta-diagnostic (see below) catches the closely-related "upstream package was emitted with removeComments: true" case explicitly so it doesn't masquerade as silent dispatch.

Behavior

The transformer walks every non-.d.ts source file in the program and dispatches per node kind:

| AST node | What fires | |---|---| | ts.CallExpression / ts.NewExpression | One Pre check per argument, dispatched via check(details, preCheck) | | ts.ReturnStatement (with an expression) | One Post check, dispatched via check(details, postCheck) |

Every other recognition / emission decision lives downstream in contrax-annotations. The transformer itself owns no contrax-specific behavior beyond picking which AST nodes to dispatch at.

TypeScript peculiarities

The transformer is the heaviest concentration of TypeScript-specific quirks in the contrax line — none of the items below have direct Java/Kotlin counterparts.

1. ServiceLoader analog via package.json#contrax.enforcements

The Java line uses ServiceLoader.load(Enforcement.class); the Kotlin line uses ServiceLoader.load(Enforcement::class.java). TypeScript has no equivalent. The transformer instead uses a Babel/PostCSS-style ecosystem convention: any package whose package.json declares a contrax.enforcements relative-path field is recognized as an Enforcement provider; the path is require()d at transformer-startup and every callable named export is registered into the registry.

{
  "name": "@visionary_software/contrax-enforcements",
  "main": "./dist/index.js",
  "contrax": { "enforcements": "./dist/index.cjs" }
}

This convention is opt-in by field, not by name prefix. Any package can ship Enforcements regardless of its name; conversely, an unrelated package that happens to start with contrax- is not auto-discovered.

2. CJS bundle requirement for Enforcement packages

The transformer runs on the Node runtime (tspc spawns a node process; Bun-based builds shell out to node for the transformer phase). Node's require() cannot consume native ESM, so every Enforcement-providing package must publish a CJS artifact and point contrax.enforcements at the .cjs. The package's primary main / exports.import can still be ESM for downstream TS consumers; the CJS sibling exists purely for the transformer's discovery walk. See contrax-enforcements and contrax-range-check for working examples.

3. JSDoc repair for the upstream parser quirk

tspc defaults to ts.JSDocParsingMode.ParseForTypeErrors, which strips every JSDoc tag except @see and @link from the parsed AST. That mode is correct for type-error-only compilation, but it would silently strip every contrax tag (@precondition, @postcondition, @rangeBounds, custom typedefs) before the transformer ever ran. The transformer's repairJSDocOn pass re-parses each source file with ts.JSDocParsingMode.ParseAll, then walks the original and reparsed ASTs in parallel, copying the reparsed JSDoc arrays back onto the original nodes (and clearing jsDocCache so subsequent ts.getJSDocTags calls see the patched arrays). This runs once per source file at transformer-startup, before any walking begins.

If you write a custom typedef walker on top of apt's primitives, prefer node.jsDoc[i].tags over ts.getJSDocTags(node) — see apt's "TypeScript peculiarities" section for the cache-invalidation rationale.

4. The bunx contrax-init adoption CLI

The Java/Kotlin lines have no adoption CLI — adopters wire META-INF/services/... files by hand or rely on Gradle plugins. TypeScript's adoption surface is two file edits (tsconfig.json plugins entry; tsc → tspc script rewrite), which is mechanical enough to fully automate. bunx contrax-init makes both edits idempotently using jsonc-parser so existing comments and trailing commas survive the rewrite. Re-running on a wired project is a no-op.

The CLI exports a programmatic entry (contraxInit({ cwd, dryRun })) that tests use to assert on the exact transformation without spawning a subprocess.

5. XMOD-3 — removeComments: true upstream meta-diagnostic

A consumer that imports a contrax-shipping library has no way to enforce its contracts if the upstream package was emitted with compilerOptions.removeComments: true — the JSDoc tags are gone from the .d.ts, so neither @precondition nor any custom registered tag survives. The naïve failure mode is a silent no-op: tags missing → engine sees no contracts → build passes despite real violations.

The transformer catches this via a two-signal heuristic that runs once per program build, after the JSDoc repair pass:

  1. The consumer's own non-.d.ts source has at least one @precondition / @postcondition tag (proves contrax is genuinely wired up — won't false-positive on consumers who happen to import a JSDoc-light library).
  2. A call from the consumer resolves to a declaration in an upstream .d.ts whose source file contains zero JSDoc nodes anywhere.

When both hold, the transformer emits one Warning-severity diagnostic per upstream .d.ts with the message:

Upstream package <fileName> appears to have been emitted with removeComments: true (zero JSDoc nodes in the .d.ts). Any contrax contracts it declared have been stripped and cannot be enforced. Re-emit it with removeComments: false.

The fix on the upstream side: re-emit with removeComments: false (preserving JSDoc in the published .d.ts).

License

GPL-3.0-or-later. See COPYING. Contact Visionary Software Solutions for commercial licensing.