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

@airo-js/cartridge-kit

v0.8.2

Published

Airo cartridge contract — Cartridge<TData, TConfig> + DataSource / View / MCP tool / Template / PublicationAdapter / Gate primitives. The API surface every cartridge implements.

Downloads

1,364

Readme

@airo-js/cartridge-kit

The cartridge contract for the airo framework. Defines the API surface every cartridge implements and every host application consumes.

Status: v0.2.0-rc.x. Validation pair (WTB skeleton + PublicationAdapter pair skeleton) compiles. Surface still subject to refinement; cartridges should target ^0.2 until 1.0 ships.

What's in here

  • Cartridge<TData, TConfig> — the envelope every cartridge implements
  • DataSource — onboarding affordances + data-loading shape
  • Transformer / PostProcessor / RuntimePipeline — runtime pipeline (re-exported from @airo-js/core because pipeline orchestration is rendering, and rendering belongs to the framework)
  • ViewDefinition + CartridgeAppContext — typed wrapper around the framework's PageRenderer
  • Template<TConfig> — pre-composed view-set + default config
  • McpToolDefinition — agent-facing tools (POST-transformer data)
  • PublicationAdapter — fan post-pipeline data out to surface-specific outputs (Schema.org JSON-LD, vendor XML feeds, etc.)
  • Supporting: SchemaDefinition, OnboardingStep, ValidationResult, PublicationContext, SchemaFieldRef, Duration

Three contract guarantees

  1. Snapshot fidelity. Views, MCP tools, and publication adapters all consume the SAME post-Transformer snapshot. No drift between what the rendered widget shows, what an agent answers, and what a downstream indexer consumes.
  2. Coverage gating. Adapters declare requires (schema field paths). The framework can skip an adapter when required fields are absent rather than emit broken output. Host apps surface "this adapter needs field X" to the user via this metadata.
  3. Validation as a hard gate. validate(output) runs before the host app publishes. If valid: false, the host app refuses to serve the output. Output trust > publish velocity.

Authoring conventions

Transformer is shape-preserving (not for schema pivots)

Transformer.transform: (data: TData, ctx) => TData — input and output are the same type. Use Transformer for filter, sort, group, annotate.

If you need to pivot the schema (e.g. flat Product → SKU becomes nested Product → Sku → Offer), reshape upstream in DataSource.fetch, not in a Transformer. The Transformer chain assumes a stable shape so views, MCP tools, and publication adapters can each project the same snapshot consistently.

// ✅ Reshape in DataSource.fetch
const myDataSource: DataSource<NestedShape, MyConfig> = {
  fetch: async (input, ctx) => {
    const flat = await fetchFlatData(input);
    return pivotToNested(flat); // Pivot here, not in a Transformer.
  },
};

// ❌ Don't try to change shape in a Transformer
const reshape: Transformer<UnknownShape, MyConfig> = {
  transform: (data) => pivotToNested(data), // Won't typecheck — TData → TData.
};

Two-envelope pattern for browser/server bundle separation

The Cartridge envelope holds references to everything: transformers, views, MCP tools, publication adapters. Tree-shaking can't help when the envelope itself is the import — bundlers see the references and pull all of them in.

PublicationAdapters and MCP tools are typically server-only (they reference disapproval rules, taxonomy mappings, image validation, model SDKs — anywhere from 10 KB to 50+ KB each). Shipping those bytes to the browser is wasteful and exposes proprietary IP.

Convention: ship two envelopes per cartridge.

my-cartridge/
├── parts/
│   ├── schema.ts                 ← Zod, single source of truth
│   ├── transformers.ts           ← shared (browser + server)
│   ├── data-sources.ts           ← shared
│   ├── views/                    ← shared (page chunks)
│   ├── publication-adapters/     ← server-only
│   └── mcp-tools.ts              ← server-only
├── runtime.ts                    ← browser entry: schema, transformers, views, dataSources
└── full.ts                       ← server entry: everything (re-exports runtime + adds adapters/mcp)

runtime.ts exports a Cartridge with publicationAdapters and mcpTools undefined. full.ts re-exports the same Cartridge but with those slots populated. Browser builds import from <my-cartridge>/runtime; SSR / publication-runner builds import from <my-cartridge>/full.

This is transparent: no build-time magic, no conditional exports tooling. Cartridge author writes the split once. Bundlers tree-shake at the package boundary; nothing leaks.

errorPolicy on Transformers

Each Transformer can declare errorPolicy: 'fail-render' | 'skip'. Default is 'fail-render' — when a transform throws, the render breaks. Pick 'skip' only for transforms whose absence degrades gracefully (sort, enrichment). Never use 'skip' for filters whose absence widens visibility past a tenant's configured scope.

const enrichWithRatings: Transformer<MyData, MyConfig> = {
  name: 'enrich-with-ratings',
  isEnabled: () => true,
  transform: (data) => attachRatings(data),
  errorPolicy: 'skip', // Ratings are nice-to-have; don't break render if the rating service is down.
};

const filterByTenant: Transformer<MyData, MyConfig> = {
  name: 'filter-by-tenant',
  isEnabled: () => true,
  transform: (data, ctx) => data.filter((item) => item.tenantId === ctx.config.tenantId),
  // Default errorPolicy: 'fail-render' — never silently widen tenant visibility.
};

Default RuntimePipeline implementation

Host apps that want default semantics use createPipeline from @airo-js/core:

import { createPipeline } from '@airo-js/core';

const pipeline = createPipeline(cartridge.transformers ?? [], cartridge.postProcessors ?? []);

// Run on every render:
const snapshot = pipeline.runTransformers(rawData, { config, navState, locale });

// Mount post-processors after view renders, collect teardown:
const teardown = pipeline.runPostProcessors({ container, config, data: snapshot, events, navState });
// On unmount:
teardown();

Host apps with custom semantics (async support, custom error reporting, OTel tracing) implement their own RuntimePipeline<TData, TConfig>.

Gate persistence is metadata, not behaviour

Gate.persist is a hint cartridges declare; the framework writes nothing based on it. Host apps implement the actual storage primitive — cookies, localStorage, server-side session table — with their own compliance posture (sameSite, domain rules, GDPR scope, SSO interaction).

This mirrors how DataSource.cacheTtlMs works: cartridge declares the hint, host app decides actual cache policy. Same shape, same envelope.

const ageGate: Gate<MyConfig> = {
  id: 'age-verification',
  // ... isEnabled, precheck, mount, destroy
  persist: {
    key: 'wtb:age-verified',
    ttl: 30 * 24 * 60 * 60 * 1000, // 30 days
    scope: 'persistent',
  },
};

Host-app side, two patterns. Pattern A — read in precheck, write before mount returns 'allow':

precheck: async (ctx) => {
  const cookie = readMyAppCookie(myGate.persist.key);
  return cookie && !expired(cookie) ? 'allow' : 'gate-required';
},

mount: async (host, ctx) => {
  // ... paint UI, await user verification
  if (verified) {
    writeMyAppCookie(myGate.persist.key, { /* ... */ }, myGate.persist.ttl);
    return 'allow';
  }
  return 'block';
},

Pattern B — host apps with their own auth/session stack skip persist entirely and use their existing primitives directly (e.g. session tokens from a hosted auth provider, or a self-managed session store). The host app ignores Gate.persist and writes through its existing layer.

Future: a separate @airo-js/gate-persist helper package may ship for greenfield apps that want a default writeGatePersist({ key, ttl, scope }). Opt-in. Not in @airo-js/core or @airo-js/cartridge-kit — those stay rendering-only.

Boot sequence

A host app mounting a cartridge follows this sequence:

import { createApp } from '@airo-js/core';
import {
  createCartridgeRegistry,
  createCartridgeApp,
  createPipeline,
} from '@airo-js/cartridge-kit';

import { wtbCartridge } from '@my-org/wtb-cartridge';

// 1. Build the registry. Multi-cartridge apps pass an array; single-
//    cartridge apps pass one. Cartridges can also be added later via
//    registry.register(...).
const registry = createCartridgeRegistry([wtbCartridge]);

// 2. The end user picks a template (UI-driven). For headless boot, default
//    to cartridge.defaultTemplateId.
const cartridge = registry.get('wtb')!;
const template = cartridge.templates.find(
  (t) => t.id === cartridge.defaultTemplateId,
)!;

// 3. Build the AppConfig the framework consumes. Pages come from the
//    template; layout/styles default to whatever the cartridge ships
//    in its component metadata. Host apps with their own page editor
//    rewrite the page tree here.
const appConfig = {
  appId: 'my-widget-id',
  pages: template.pages.map((p) => ({
    ...p,
    layout: { regionOrder: [], regions: {} },
  })),
};

// 4. Load data via the cartridge's DataSource and run the pipeline.
//    Snapshot is what views, MCP tools, and publication adapters all read.
const dataSource = cartridge.dataSources[0];
const rawData = await dataSource.fetch(input, { config });
const pipeline = createPipeline(
  cartridge.transformers ?? [],
  cartridge.postProcessors ?? [],
);
const snapshot = pipeline.runTransformers(rawData, { config, navState: { page: '' } });

// 5. Mount. createCartridgeApp builds CartridgeAppContext, derives the
//    resolveRenderer from the cartridge's views[], and delegates to
//    createApp.
const app = createCartridgeApp(cartridge, appConfig, snapshot, config, {
  host: containerEl,
  enableRouter: true,
  // resolveRenderer auto-derived; pass registry.resolverFor('wtb') to use
  // the multi-cartridge / chunk-mailbox resolution path instead.
});

Multi-cartridge resolution

When a host app runs more than one cartridge, pass registry.resolverFor(cartridgeId) instead of letting createCartridgeApp walk a single cartridge's views[]:

const app = createCartridgeApp(cartridge, appConfig, snapshot, config, {
  host: containerEl,
  resolveRenderer: registry.resolverFor('wtb'),
});

The registry's resolver checks the cartridge's static views[] first, then falls back to the per-cartridge chunk mailbox (named by cartridge.mailboxName). Cartridges shipping views as separate chunks register them via pushToMailbox(cartridge.mailboxName, { key: pageType, factory }) from each chunk's IIFE — a stub-queue pattern that lets late-loading chunks register themselves whenever they finish loading.

Validation skeletons

Two compile-only skeletons live in examples/ of the airo-js repo and prove the contract holds:

  • examples/cartridge-wtb-skeleton/ — full Cartridge envelope with 6 transformers, 4 views, 1 template, 3 MCP tools, 2 publication adapters.
  • examples/publication-adapter-skeleton/ — two adapters (JSON-LD inline + XML signed-feed) sharing one snapshot type. Stresses fan-out.

Run pnpm typecheck from the workspace root to verify both still compile.

Contract feedback loop

Cartridge authors finding gaps in the contract: open an issue with a minimal repro. The contract's verification bar is "a real cartridge compiles with at least one PublicationAdapter, with no any." If your cartridge needs any to fit the contract, that's a gap worth surfacing.

License

Apache 2.0.