mirror of
https://github.com/jakejarvis/stanza.git
synced 2026-07-16 18:05:58 -04:00
5.3 KiB
5.3 KiB
Stanza
Shadcn-style CLI for assembling modular full-stack TS monorepos. Currently
ships init, add, remove, list, search against five slots:
framework, styling, db, orm, auth. swap + update verbs and
additional slots (api/ai/ui/payments/email/tooling/testing/deploy) are
planned — the manifest already reserves the fields they'll need
(modules[slot].version, regions). See REGISTRY.md for the
module roadmap and TODO.md for active work.
Three things differentiate stanza from other scaffolders:
- Post-init
stanza addworks on existing projects (manifest-driven, peer-aware). - Generated code is vendored verbatim — no
@stanza/runtimedep. - Open registry spec — third parties can host their own static JSON.
Layout
apps/cli/—@stanza/cli, Bun entrypoint atsrc/bin.tsapps/web/—@stanza/web, TanStack Start visual builder (Vite-native, no Vinxi)packages/registry/— shared schema, slot/peer resolver, Zod manifest validatorpackages/codemods/— ts-morph helpers (idempotent + reversible)packages/create-stanza/—pnpm create stanzashimpackages/registry-build/— Bun script that emits the static CDN JSONregistry/modules/<slot>-<id>/— first-party modules:module.ts+templates/(+ optionalcodemods/)
Commands
bun apps/cli/src/bin.ts <verb>— run CLI directly without buildbun packages/registry-build/src/build.ts— regeneratedist/registry/{index,modules/*}.jsonpnpm lint/pnpm lint:fix— Oxlint across the whole repo (config:.oxlintrc.json)pnpm fmt/pnpm fmt:check— oxfmt across the whole repo (config:.oxfmtrc.json)cd packages/<x> && node_modules/.bin/vitest run— unit tests (per workspace; no rootvitestbinary)cd <pkg> && node_modules/.bin/tsc --noEmit— typecheck (per workspace)cd apps/web && node_modules/.bin/vite build— generatessrc/routeTree.gen.ts(required before first typecheck)- E2E smoke: seed
$TMPDIR/xwithstanza.json+apps/web/package.json, thenbun .../bin.ts add <slot> <module>
Toolchain invariants
- pnpm 10 +
node-linker: isolated— each workspace MUST declare@types/nodein its own devDeps and settypes: ["node"]in tsconfig (auto-discovery doesn't reach into the isolatednode_modules/@types) - TypeScript 6 —
allowImportingTsExtensions: true+noEmit: trueis set intsconfig.base.json; build withbun build, nottsc tsconfig.base.jsonexcludes**/templates/**globally — template files target user projects, not this repo- Zod 4: use
z.partialRecord(K, V)for finite-key partial records (z.record(z.enum, V)requires exhaustive keys) - TanStack Start:
verbatimModuleSyntax: falseinapps/web/tsconfig.json(server bundles leak otherwise);tanstackStart()MUST precedereact()in vite plugins - Bun runs the CLI directly from
.ts; don't add a TS compile step
Architecture rules
- Modules are vendored: their templates land in the user's repo verbatim; no
@stanza/runtimedep - Slot taxonomy is currently
framework | styling | db | orm | auth(seeKNOWN_SLOTS); adding a slot is a manifest schema bump — updateKNOWN_SLOTS,slotOrder, and the Zod manifest schema together - Adapter keys encode peer choices (e.g.,
next+drizzle); the resolver picks the most specific match - Region ownership in
stanza.jsonis the source of truth forremove/future-swap; two modules claiming the same region is a hard error (RegionConflictError) - Declarative beats imperative: prefer
templates/dependencies/env/scriptsover imperative codemods; the runner applies declarative fields generically - Reserved manifest fields:
modules[slot].versionandregionsare written today but only fully consumed by the upcomingswap/updateverbs — do not drop them
Module authoring
module.tsexportsdefineModule({...})with at least one adapter (usematch: {}for "default / no peer")- Templates go in
templates/, referenced bysrcpath;scope: "app"resolves againstmanifest.appDir,scope: "repo"against the repo root - Framework modules MUST NOT ship a
package.json.tpl— it collides withaddPackageDependency. Let the runner merge deps into the host's package.json - Codemod functions (when needed) live in
<module>/codemods/index.tsas a default-exported map of{ id: Codemod }
Gotchas
- Clack spinners (
p.spinner()) don't auto-stop on promise rejection — wrap awaits in try/catch and callspinner.stop(...failed)in the catch - LSP diagnostics on template files (e.g. "Cannot find module 'react'") are noise; the global exclude works for
tscbut tsserver still indexes them - The dev registry is found by walking up from
import.meta.urllooking forregistry/modules/;STANZA_REGISTRYenv var overrides (FS path or HTTP URL) pnpm installsays "Already up to date" when only workspacepackage.jsonfiles changed without lockfile-affecting bumps — usepnpm install --forceto re-link- Oxlint has most but not all ESLint plugin rules —
prevent-abbreviations,react/jsx-uses-react, etc. don't exist. Checknode_modules/oxlint/configuration_schema.jsonif a rule name fails - oxfmt auto-loads
.gitignoreand.prettierignore; we use.oxfmtrc.json'signorePatternsinstead so we don't masquerade as a Prettier project