Files
stanza/REGISTRY.md
T
jake 19b5e51075 feat: add-on module kind, testing-vitest + testing-playwright, and multi-choice CLI/web support
- Introduce a `kind: "slot" | "addon"` discriminated union on `Module` — slot modules carry `slot`, add-on modules carry `category` (one of `KNOWN_ADDONS`). `isAddon()` narrows the union; `moduleGroup()` returns the slot-or-category key. Add-on categories (`testing`, `tooling`, `deploy`, `email`, `monorepo`) live in `KNOWN_ADDONS`/`ADDON_CATEGORIES`, deliberately disjoint from `KNOWN_SLOTS` so the resolver never lets add-ons participate in peer dispatch (they can still declare a one-way `peers` + per-framework adapters)
- Ship the first two add-ons: `testing-vitest` (jsdom + RTL, `test`/`test:watch` scripts, per-framework adapters) and `testing-playwright` (per-framework `webServer`, `test:e2e`/`test:e2e:ui` scripts — disjoint from vitest's so region claims don't collide); both under `registry/modules/testing-*/`
- Manifest gains `addons: Partial<Record<AddonCategoryId, StanzaAddonRecord[]>>` (`.default({})` keeps existing `stanza.json` valid); resolver gains `addonOrder` for deterministic codemod sequencing
- CLI: `stanza add <slot|category> <module>` accepts both slots and add-on categories; duplicate-id guard for add-ons instead of the single-fill slot guard; `stanza remove <slot|category> [id]` removes a specific add-on by id (or the whole slot); `stanza list` shows add-ons alongside slots; `stanza init --yes` gains `--testing vitest,playwright` flag; wizard prompts multi-select for add-on categories
- Web builder: add-on slots render as checkbox multi-select cards (`SlotSection` detects `isAddon` and switches to `<CheckboxCard>`), `?testing=vitest,playwright` comma-joined URL param, `builder-state.functions.ts` parses/serializes per-category add-on lists, OG and sitemap routes updated for add-on module paths
- Codemod runner branches on `isAddon` to resolve add-on records from `manifest.addons[category]` rather than `manifest.modules[slot]`; region ownership keys on `module.id` so co-existing add-ons writing the same file at disjoint dot-paths never conflict
- Add `commands.test.ts`, `selection.test.ts`, `manifest.test.ts`, and `resolver.test.ts` coverage for add-on add/remove/list flows and schema validation
2026-05-21 16:34:09 -04:00

6.6 KiB

First-party module registry

This is the canonical roadmap for the first-party modules stanza ships. Each entry maps to a registry/modules/<slot>-<id>/ directory. Update this file when a module lands, gets renamed, or is dropped.

Legend: [x] added · [ ] planned

Categories: slots vs add-ons

Stanza's module taxonomy splits into two categories. Slots are single-choice and constrain other modules' adapter dispatch (picking Next.js influences which auth adapters are available). Add-ons can coexist freely and don't influence anyone else's adapters (vitest doesn't constrain anything).

Category Type Examples
Slots (constraint-bearing, one choice each) framework, styling, db, orm, auth, api, ai, ui, payments next vs tanstack-start; drizzle vs prisma
Add-ons (no constraints, many allowed) testing, tooling, deploy, email, monorepo vitest + playwright together; eslint or biome

The add-on schema is live as of the testing modules. Add-ons are modeled with a kind: "addon" discriminator on Module (carrying a category instead of a slot) and a manifest.addons record keyed by category, each holding a list of records (Partial<Record<AddonCategoryId, StanzaAddonRecord[]>>). Add-on categories live in KNOWN_ADDONS / ADDON_CATEGORIES, deliberately disjoint from KNOWN_SLOTS so they never participate in peer resolution — yet they can still declare a one-way peers (e.g. { framework: [...] }) and framework-varying adapters.

framework

  • tanstack-start — TanStack Start on Vite (no Vinxi). Provides web, react, ssr, node
  • next — Next.js 16 (App Router). Provides web, react, ssr, rsc, node, edge
  • nuxt — Vue. Will require relaxing the React-implicit assumption in many peer modules (capability tag vue)
  • svelte — SvelteKit. Capability svelte
  • solid — SolidStart. Capability solid

api

New slot (single-choice, constraint-bearing). Optional layer between the framework and the database/services.

  • trpc — tRPC v11; per-framework adapters (next, tanstack-start, nuxt, svelte, solid)
  • orpc — oRPC

ai

New slot (single-choice, constraint-bearing).

  • vercel-ai-sdk — ai package + provider sub-recipes
  • tanstack-ai — TanStack AI

auth

  • better-auth — headless, peers orm: [drizzle, prisma], framework: [next, tanstack-start]
  • clerk — hosted UI, peers framework: [next] (TanStack Start adapter planned)
  • workos — WorkOS AuthKit

orm

  • drizzle — Drizzle ORM 0.45, peers db: [postgres, sqlite]
  • prisma — Prisma 7, peers db: [postgres, sqlite]

db

  • postgres — postgres driver 3.4.x
  • sqlite — better-sqlite3 12.x

styling

  • tailwind — Vanilla Tailwind v4, adapters per framework
  • shadcn-radix — classic shadcn/ui (Radix primitives)
  • shadcn-base — shadcn on react-base-ui

payments

New slot (single-choice, constraint-bearing).

  • stripe — Checkout Sessions + webhooks
  • polar — Polar SDK

email

New add-on (multi-allowed, no constraints).

  • resend — Resend SDK + React Email templates

tooling

New add-on. Lint/format toolchain. Conventionally one per project but doesn't constrain others.

  • eslint-prettier — ESLint + Prettier
  • biome — Biome (lint + format)
  • oxlint-oxfmt — Oxlint + oxfmt

testing

New add-on. Vitest and Playwright are independent and routinely coexist.

  • vitest — unit + integration; per-framework adapters (next, tanstack-start), jsdom + RTL, test/test:watch scripts
  • playwright — e2e; per-framework webServer (next dev / vite dev), test:e2e/test:e2e:ui scripts (disjoint from vitest's)

deploy

New add-on.

  • vercel — vercel.json + framework-specific output
  • cloudflare — Workers / Pages adapter per framework
  • railway — railway.toml + Dockerfile
  • docker — generic Dockerfile + compose for self-host

monorepo

Currently hardcoded in bootstrapShell as Turborepo. Promotes to an add-on when a second option (Nx, Moonrepo) lands.

  • turborepo — Turbo 2.x (current default; not yet a configurable choice)

packageManager

Not a slot — a top-level field in stanza.json (packageManager: "pnpm" | "bun" | "npm"). Wizard prompts; codemods only touch package.json, never lockfiles.

  • pnpm (default)
  • bun
  • npm
  • yarn — needs lockfile/workspace handling that differs from the others

Slot-package extraction

Generated projects place each slot's output in one of two homes:

  • App-scoped (framework, styling) — files land in manifest.appDir (e.g. apps/web/). These slots wire the app shell itself, so there's no useful extraction boundary.
  • Package-scoped (auth, db, orm) — files land in packages/<dir>/, named @<manifest.name>/<dir>, and the app gets a workspace:* dep. db and orm share a single packages/db/ package so the ORM client sits next to the schema it queries.

The mapping lives in the canonical SLOTS array as the packageDir field — SLOT_PACKAGE_DIR is just a Record<SlotId, ...> view derived from it. When you add a new slot, decide upfront whether it extracts (data layer, observability, payments) or wires the shell (router, UI primitives that mount in <html>) and set packageDir accordingly.

Slot taxonomy changes required

Adding a new slot is now a two-line edit: append the id to KNOWN_SLOTS (the as const tuple Zod needs) and append a Slot entry to SLOTS with { id, label, description, packageDir }. Order is topological — earlier slots become peer candidates for later ones. Existing stanza.json files don't break: new slots are optional, so adding them is additive.

Adding an add-on category is the same two-line edit against KNOWN_ADDONS + ADDON_CATEGORIES. Authoring an add-on module sets kind: "addon" + category (instead of slot) on defineModule; the manifest's addons record and the runner/CLI/web surfaces already handle multi-choice. See the category table at the top of this file for which planned modules are add-ons vs slots.