diff --git a/.changeset/extract-schema-package.md b/.changeset/extract-schema-package.md new file mode 100644 index 0000000..234fbeb --- /dev/null +++ b/.changeset/extract-schema-package.md @@ -0,0 +1,10 @@ +--- +"@withstanza/schema": minor +"stanza-cli": patch +--- + +Extract the schema/contract layer into a standalone, npm-published `@withstanza/schema` package. + +`@withstanza/schema` now owns the `stanza.json` manifest schema, the registry module/index schemas, the contract types, the canonical `CATEGORIES` taxonomy, and the package-manager + registry-config schemas — everything previously bundled into the private `@withstanza/registry`. It's published so third-party registry authors and editor tooling can validate against the exact same Zod source of truth the CLI uses; `StanzaManifestSchema` backs the JSON Schema served at . A new private `@withstanza/utils` package holds the shared path-safety (`safeRelativePath`) and env-file (`appendEnvVar`) helpers. + +No change to CLI behavior — this is an internal restructure. `@withstanza/registry` keeps only the resolver, install-field synthesis, and template rendering, depending on `@withstanza/schema`. The static registry build moves out of the registry package to the standalone `scripts/compile-registry.ts` (writing `index.json` + `modules/*.json` directly under its output dir, no `registry/` wrapper), and the manifest JSON Schema is served by the web app's `/schema.json` route rather than emitted as a build artifact. diff --git a/.gitignore b/.gitignore index 2f5d38e..987d71d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ dist/ *.tsbuildinfo .tanstack/ .source/ -.vercel # IDE .vscode/* @@ -31,6 +30,5 @@ Thumbs.db coverage/ __snapshots__/.tmp/ -# misc -apps/web/public/registry/ -apps/web/public/schema.json +# Vercel +.vercel diff --git a/AGENTS.md b/AGENTS.md index 8b1a8cc..4ab02c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,15 +1,18 @@ # Stanza -Shadcn-style CLI that assembles modular full-stack TS monorepos. Two unusual properties: (1) generated code is vendored verbatim — no `@stanza/runtime` dep — and (2) `stanza add` works on existing projects, not just at init. Verbs today: `init`, `add`, `remove`, `list`, `search` (`swap` + `update` planned — manifest already reserves `modules[category][].version` and `regions`). The canonical category list lives in [packages/registry/src/module.ts](packages/registry/src/module.ts) (`CATEGORIES`); module roadmap in [registry.mdx](apps/web/content/docs/registry.mdx). +Shadcn-style CLI that assembles modular full-stack TS monorepos. Two unusual properties: (1) generated code is vendored verbatim — no `@stanza/runtime` dep — and (2) `stanza add` works on existing projects, not just at init. Verbs today: `init`, `add`, `remove`, `list`, `search` (`swap` + `update` planned — manifest already reserves `modules[category][].version` and `regions`). The canonical category list lives in [packages/schema/src/category.ts](packages/schema/src/category.ts) (`CATEGORIES`); module roadmap in [registry.mdx](apps/web/content/docs/registry.mdx). ## Layout - `apps/cli/` — `stanza-cli`, entry `src/bin.ts` (publishable, ESM via tsdown) -- `apps/web/` — `@stanza/web`, TanStack Start visual builder (private, deployed to Vercel) -- `packages/registry/` — schema, resolver, manifest validator (private; inlined into CLI bundle) +- `apps/web/` — `@withstanza/web`, TanStack Start visual builder (private, deployed to Vercel) +- `packages/schema/` — `@withstanza/schema`: the `stanza.json` + module schemas, contract types, `CATEGORIES` list, package-manager + registry-config schemas (publishable; also inlined into CLI bundle). The single Zod source of truth — `StanzaManifestSchema` backs the published JSON Schema +- `packages/registry/` — `@withstanza/registry`: peer/adapter resolver + `package.json`/env/README/template synthesis (private; inlined into CLI bundle). Depends on `@withstanza/schema` + `@withstanza/utils` - `packages/codemods/` — ts-morph helpers, idempotent + reversible (private; inlined) +- `packages/utils/` — `@withstanza/utils`: path-safety (`safeRelativePath`) + env-file (`appendEnvVar`) helpers shared by schema/registry/codemods (private; inlined) - `packages/create-stanza/` — `pnpm create stanza` shim (publishable) - `registry/modules/-/` — first-party modules: `module.ts` + `templates/` (private; data, not code) +- `scripts/compile-registry.ts` — standalone static-registry build (not part of any package; imports `@withstanza/schema`). Run via `jiti`; exports `compileRegistry()` for in-process callers - `skills/stanza-cli/SKILL.md` — agent skill for the published CLI. Update whenever the public CLI contract changes; agents using this skill won't have the source repo ## Commands @@ -20,10 +23,10 @@ The repo runs on the **Vite+ toolchain** (`vp`). All config lives in the root [` - `vp check` — format + lint + type-check (oxfmt + oxlint + type-aware `tsgolint`). **Use this for validation loops.** `--fix` autofixes - `vp test` — Vitest 4 via `vite-plus/test`; project selection via `test.projects` in root config - `vp run -r build` — build everything (`vp pack` per package, wraps tsdown) -- `vp run @stanza/web#dev` — TanStack Start dev server. Its `dev`/`build` scripts first run the `compile-registry` vp task (defined under `run.tasks` in [`apps/web/vite.config.ts`](apps/web/vite.config.ts)) → `jiti packages/registry/src/build.ts apps/web/public`, emitting `apps/web/public/registry/{index,modules/*}.json` + `apps/web/public/schema.json` -- `src/routeTree.gen.ts` is generated by `vp run @stanza/web#build` — run it before the first `vp check` if missing -- E2E smoke: build the registry (`jiti packages/registry/src/build.ts $TMPDIR/reg`), seed `$TMPDIR/x` with `stanza.json` + `apps/web/package.json`, then `STANZA_REGISTRY=$TMPDIR/reg/registry/index.json tsx apps/cli/src/bin.ts add `. The CLI reads a built registry **main file** (full path/URL, any filename) — there is no source-tree loader; `STANZA_REGISTRY` unset hits the production registry -- `pnpm changeset` — drop a markdown file after a substantive PR; the release workflow handles versioning + publish. Only `stanza-cli` + `create-stanza` ship to npm +- `vp run @withstanza/web#dev` — TanStack Start dev server. Its `dev`/`build` scripts first run the `compile-registry` vp task (defined under `run.tasks` in [`apps/web/vite.config.ts`](apps/web/vite.config.ts)) → `jiti scripts/compile-registry.ts apps/web/public/registry`, emitting `apps/web/public/registry/{index.json,modules/*.json}`. The manifest JSON Schema is no longer a build artifact — it's served on request by the `/schema.json` route (`apps/web/src/routes/schema[.]json.ts`) +- `src/routeTree.gen.ts` is generated by `vp run @withstanza/web#build` — run it before the first `vp check` if missing +- E2E smoke: build the registry (`jiti scripts/compile-registry.ts $TMPDIR/reg` → writes `$TMPDIR/reg/index.json` + `$TMPDIR/reg/modules/*.json` directly under the out dir, no `registry/` wrapper), seed `$TMPDIR/x` with `stanza.json` + `apps/web/package.json`, then `STANZA_REGISTRY=$TMPDIR/reg/index.json tsx apps/cli/src/bin.ts add `. The CLI reads a built registry **main file** (full path/URL, any filename) — there is no source-tree loader; `STANZA_REGISTRY` unset hits the production registry +- `pnpm changeset` — drop a markdown file after a substantive PR; the release workflow handles versioning + publish. `stanza-cli`, `create-stanza`, and `@withstanza/schema` ship to npm; every other workspace is private Use `agent-browser` for web automation (`agent-browser --help`); prefer it over built-in browser tools. @@ -43,16 +46,16 @@ Use `agent-browser` for web automation (`agent-browser --help`); prefer it over - **Adding a generic codemod**: drop `.ts` under `builtins/`, default-export a `Codemod`, register in [`builtins/index.ts`](packages/codemods/src/builtins/index.ts). Module-specific identifiers don't belong — factor them into args - **Codemod catalog ids are part of the public contract.** Once published, renaming an id breaks third-party manifests. Treat ids like npm package names — additions are free, renames need a deprecation cycle, removals need a manifest schema version bump - **Third-party codemods are deferred** until a sandboxed-execution + signing model lands. Third-party modules can invoke existing catalog ids but can't add new ones -- **Template distribution**: [`packages/registry/src/build.ts`](packages/registry/src/build.ts) inlines each template file's contents into `templates[].content` so HTTP-loaded manifests are self-contained. The runner prefers `tpl.content` and only reads from `templates/` when absent (local dev). New templates need no build wiring +- **Template distribution**: [`scripts/compile-registry.ts`](scripts/compile-registry.ts) (the standalone registry build) inlines each template file's contents into `templates[].content` so HTTP-loaded manifests are self-contained. The runner prefers `tpl.content` and only reads from `templates/` when absent (local dev). New templates need no build wiring - **Categories** carry orthogonal **`cardinality`** (`one` / `many`) and **`home`** (`app` / `repo` / `package`); both are declared on each entry in `CATEGORIES`. Adding a category is a one-line edit there — `KNOWN_CATEGORIES`, `PEER_CATEGORIES`, `PACKAGE_DIRS`, and `categoryHome()`/`categoryLabel()` all derive. Peer-resolution iterates **`PEER_CATEGORIES`** (the `one`-cardinality ids) only — a `many` category never participates in others' dispatch. `CATEGORIES` order is topological (each category appears after everything it peers on; `many` categories last) - **Manifest layout**: selections live in one `modules` record keyed by `CategoryId`, each an array of `StanzaModuleRecord`. Records carry optional `apps?: string[]` — **required** for `home: app`, **optional** for `home: package` (omitted means "ship app-scoped shims into every app"; an explicit list restricts), **forbidden** for `home: repo`. The app `id` doubles as the workspace package suffix (`id: "web"` → `@/web`, regardless of `dir`). `cardinality: "one"` enforcement is **per-app** for `home: app` categories (`selectedOne(m, cat, appId)`), per-project otherwise - **Install home routing**: `categoryHome(id)` is the single decision point, read by both the CLI runner and the web's `synthesizePackageJsons`. `home: package` → `packages//` workspace package named `@/` (auto-bootstrapped by `ensureSlotPackage`); every consuming app gets a `workspace:*` dep. `home: repo` → repo-root config + scripts in root `package.json`. `home: app` → the targeted app - **Generated projects don't share a tsconfig base.** Every `apps/*/tsconfig.json` and `packages/*/tsconfig.json` is self-contained. The Stanza repo's root `tsconfig.json` exists only for this repo — never emit one into generated trees -- **Cross-package wiring**: declare `consumesPackages: [""]` at the **module level** (not per-adapter) when source imports another internal package. Reference other packages in templates / codemod `args` via `{{packages..name}}` (e.g. `{{packages.db.name}}` → `@my-app/db`). Full template context: `{ project, app, package, packages }`; `app.*` is rebound per target app in the apply loop. `renderTemplate` lives in `@stanza/registry` so CLI apply and web preview produce byte-identical output +- **Cross-package wiring**: declare `consumesPackages: [""]` at the **module level** (not per-adapter) when source imports another internal package. Reference other packages in templates / codemod `args` via `{{packages..name}}` (e.g. `{{packages.db.name}}` → `@my-app/db`). Full template context: `{ project, app, package, packages }`; `app.*` is rebound per target app in the apply loop. `renderTemplate` lives in `@withstanza/registry` so CLI apply and web preview produce byte-identical output - **Region ownership** in `stanza.json` is the source of truth for `stanza remove`. Two modules claiming the same region throws `RegionConflictError`. Regions key on `module.id`, so disjoint dot-paths (vitest `scripts.test` vs playwright `scripts.test:e2e`) coexist. Package bootstrap files (`package.json`, `tsconfig.json`, workspace deps) are system-owned, not tracked in regions — `stanza remove`'s sweep over `PACKAGE_DIRS` cleans them when no claims remain. _Regions are not yet sufficient for `swap`_ — that verb needs adapter-region remapping; design pending - **Reserved manifest fields**: `modules[category][].version` and `regions` are written today but only fully consumed by upcoming `swap`/`update`. Don't drop them -- **Web previews are SSR.** Shiki runs in [`highlighter.server.ts`](apps/web/src/server/highlighter.server.ts); the client-safe `Preview` type lives in [`highlighter.ts`](apps/web/src/server/highlighter.ts). Never import `shiki` from a client component. After `vp run @stanza/web#build`, grep `.output/public/assets/*.js` for the runtime (`createHighlighter`/`codeToHtml`/`loadWasm`) — must be absent. A bare `grep shiki` yields false positives because MDX docs ship statically-rendered code blocks with `class="shiki"` + CSS vars (fine) -- **Web hosts the canonical registry.** The `compile-registry` task writes the registry into `apps/web/public/registry/`; the deployed site serves it at the published CLI's default `DEFAULT_REGISTRY_URL`. `STANZA_REGISTRY` (URL or FS path) overrides for self-hosters / CI. `public/registry/` is also a Nitro `serverAssets` dir (vite.config.ts) — SSR reads via `useStorage("assets:registry")` (see [`registry-base.server.ts`](apps/web/src/server/registry-base.server.ts)), which is why Vercel works where `public/` is absent from the function fs +- **Web previews are SSR.** Shiki runs in [`highlighter.server.ts`](apps/web/src/server/highlighter.server.ts); the client-safe `Preview` type lives in [`highlighter.ts`](apps/web/src/server/highlighter.ts). Never import `shiki` from a client component. After `vp run @withstanza/web#build`, grep `.output/public/assets/*.js` for the runtime (`createHighlighter`/`codeToHtml`/`loadWasm`) — must be absent. A bare `grep shiki` yields false positives because MDX docs ship statically-rendered code blocks with `class="shiki"` + CSS vars (fine) +- **Web hosts the canonical registry.** The `compile-registry` task writes the registry into `apps/web/public/registry/`; the deployed site serves it at the published CLI's default `DEFAULT_REGISTRY_URL`. `STANZA_REGISTRY` (URL or FS path) overrides for self-hosters / CI. `public/registry/` is also a Nitro `serverAssets` dir (vite.config.ts) — SSR reads via `useStorage("assets:registry")` (see [`registry-base.server.ts`](apps/web/src/server/registry-base.server.ts)), which is why Vercel works where `public/` is absent from the function fs. The deployed site also serves the manifest JSON Schema at `/schema.json` (route `apps/web/src/routes/schema[.]json.ts`), compiled from `@withstanza/schema`'s `StanzaManifestSchema` on request rather than emitted to `public/` — this is the URL `MANIFEST_SCHEMA_URL` / a manifest's `$schema` resolves to - **Third-party registries**: namespaces declared under `stanza.json#registries`. Modules addressed as ` @/`. `telemetry.captureModule` redacts non-`@stanza` ids to ``; stderr still prints full ids (CI log forwarders will see them) ## Module authoring diff --git a/README.md b/README.md index 6ba7081..daa5d0c 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,15 @@ apps/ cli/ # stanza-cli — the CLI binary web/ # https://stanza.tools (TanStack Start) packages/ - registry/ # shared schema, slot/peer/capability resolver + schema/ # @withstanza/schema — stanza.json + module schema, contract types, category list + registry/ # slot/peer/capability resolver + package.json/env/template synthesis codemods/ # ts-morph helpers for region-aware patching + utils/ # shared path-safety + env-file helpers create-stanza/ # `npm init stanza` template shim registry/ modules/ # first-party modules (framework, orm, db, auth, ui, tooling, testing) +scripts/ + compile-registry.ts # builds the static registry JSON the CLI consumes ``` ## License diff --git a/apps/cli/package.json b/apps/cli/package.json index 7eed975..8e862e9 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -57,10 +57,12 @@ "zod": "^4.4.3" }, "devDependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*", "@types/node": "^25.9.1", "@types/semver": "^7.7.1", + "@withstanza/codemods": "workspace:*", + "@withstanza/registry": "workspace:*", + "@withstanza/schema": "workspace:*", + "@withstanza/utils": "workspace:*", "typescript": "^6.0.3", "vite-plus": "catalog:", "vitest": "catalog:" diff --git a/apps/cli/src/commands/add.ts b/apps/cli/src/commands/add.ts index 2651191..ce31c73 100644 --- a/apps/cli/src/commands/add.ts +++ b/apps/cli/src/commands/add.ts @@ -1,5 +1,6 @@ import * as p from "@clack/prompts"; -import type { AppSpec, StanzaManifest } from "@stanza/registry"; +import { resolveAdapter } from "@withstanza/registry"; +import type { AppSpec, StanzaManifest } from "@withstanza/schema"; import { categoryHome, DEFAULT_NAMESPACE, @@ -9,9 +10,8 @@ import { isValidModuleId, KNOWN_CATEGORIES, parseModuleSpec, - resolveAdapter, selectedAll, -} from "@stanza/registry"; +} from "@withstanza/schema"; import { defineCommand } from "citty"; import pc from "picocolors"; @@ -71,7 +71,7 @@ export async function cmdAdd(args: CliArgs): Promise { // The id is about to be interpolated into a registry URL — reject anything // that could escape its segment (path traversal, query strings, encoded - // bytes). See `isValidModuleId` in @stanza/registry for the exact shape. + // bytes). See `isValidModuleId` in @withstanza/registry for the exact shape. if (!isValidModuleId(moduleId)) { p.log.error( `Invalid module id "${moduleId}". Ids must be alphanumeric segments ` + diff --git a/apps/cli/src/commands/commands.test.ts b/apps/cli/src/commands/commands.test.ts index 12de5e0..6c7c06c 100644 --- a/apps/cli/src/commands/commands.test.ts +++ b/apps/cli/src/commands/commands.test.ts @@ -1,12 +1,15 @@ +import { execFileSync } from "node:child_process"; import fs from "node:fs"; import { createServer, type Server } from "node:http"; import os from "node:os"; import path from "node:path"; +import { fileURLToPath } from "node:url"; -import { CATEGORIES } from "@stanza/registry"; -import { buildRegistry } from "@stanza/registry/build"; +import { CATEGORIES } from "@withstanza/schema"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vite-plus/test"; +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../.."); + import { loadRegistries } from "../lib/registry-loader"; import { cmdAdd } from "./add"; import { cmdDoctor } from "./doctor"; @@ -23,10 +26,17 @@ let prevExitCode: typeof process.exitCode; let fixtureRoot: string; let fixtureMain: string; -beforeAll(async () => { +beforeAll(() => { fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "stanza-reg-")); - await buildRegistry({ outBase: fixtureRoot }); - fixtureMain = path.join(fixtureRoot, "registry", "index.json"); + // Build the real first-party registry via the standalone build script — the + // same entrypoint CI and the web prebuild use — so the test exercises the + // production build path rather than an in-process import. + execFileSync( + path.join(repoRoot, "node_modules/.bin/jiti"), + ["scripts/compile-registry.ts", fixtureRoot], + { cwd: repoRoot, stdio: "inherit" }, + ); + fixtureMain = path.join(fixtureRoot, "index.json"); }); afterAll(() => { diff --git a/apps/cli/src/commands/doctor.ts b/apps/cli/src/commands/doctor.ts index 220469e..4c91015 100644 --- a/apps/cli/src/commands/doctor.ts +++ b/apps/cli/src/commands/doctor.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import * as p from "@clack/prompts"; -import { PACKAGE_DIRS } from "@stanza/registry"; +import { PACKAGE_DIRS } from "@withstanza/schema"; import { defineCommand } from "citty"; import pc from "picocolors"; diff --git a/apps/cli/src/commands/init.ts b/apps/cli/src/commands/init.ts index 8838bcd..6b9e47b 100644 --- a/apps/cli/src/commands/init.ts +++ b/apps/cli/src/commands/init.ts @@ -2,21 +2,24 @@ import fs from "node:fs"; import path from "node:path"; import * as p from "@clack/prompts"; -import type { AppSpec, CategoryId, Module, Resolved, ResolvedEntry } from "@stanza/registry"; +import type { Resolved, ResolvedEntry } from "@withstanza/registry"; import { appPackageJsonBase, - categoryHome, categoryOrder, - DEFAULT_NAMESPACE, ENV_EXAMPLE_HEADER, - KNOWN_CATEGORIES, - PEER_CATEGORIES, pmRun, PM_FLOOR_VERSION, resolveAdapter, rootPackageJson, synthesizeReadme, -} from "@stanza/registry"; +} from "@withstanza/registry"; +import type { AppSpec, CategoryId, Module, PackageManager } from "@withstanza/schema"; +import { + categoryHome, + DEFAULT_NAMESPACE, + KNOWN_CATEGORIES, + PEER_CATEGORIES, +} from "@withstanza/schema"; import { type ArgsDef, defineCommand } from "citty"; import pc from "picocolors"; @@ -228,7 +231,7 @@ export async function cmdInit(args: CliArgs): Promise { async function bootstrapShell( projectRoot: string, - opts: { name: string; packageManager: "pnpm" | "bun" | "npm"; apps: AppSpec[] }, + opts: { name: string; packageManager: PackageManager; apps: AppSpec[] }, ) { // Pin `packageManager` to the latest released version that still satisfies // the floor (Corepack requires an exact version, so the modifier is stripped). diff --git a/apps/cli/src/commands/list.ts b/apps/cli/src/commands/list.ts index 43123e7..a84a7f7 100644 --- a/apps/cli/src/commands/list.ts +++ b/apps/cli/src/commands/list.ts @@ -1,5 +1,6 @@ import * as p from "@clack/prompts"; -import { categoryOrder, DEFAULT_NAMESPACE, isMulti, selectedAll } from "@stanza/registry"; +import { categoryOrder } from "@withstanza/registry"; +import { DEFAULT_NAMESPACE, isMulti, selectedAll } from "@withstanza/schema"; import { defineCommand } from "citty"; import pc from "picocolors"; diff --git a/apps/cli/src/commands/remove.ts b/apps/cli/src/commands/remove.ts index 2c112b1..070f004 100644 --- a/apps/cli/src/commands/remove.ts +++ b/apps/cli/src/commands/remove.ts @@ -2,8 +2,8 @@ import fs from "node:fs"; import path from "node:path"; import * as p from "@clack/prompts"; -import { removePackageDependency, removeEnvVar } from "@stanza/codemods"; -import type { AppSpec, StanzaManifest, StanzaModuleRecord } from "@stanza/registry"; +import { removePackageDependency, removeEnvVar } from "@withstanza/codemods"; +import type { AppSpec, StanzaManifest, StanzaModuleRecord } from "@withstanza/schema"; import { appsForRecord, categoryHome, @@ -15,7 +15,7 @@ import { PACKAGE_DIRS, parseModuleSpec, selectedAll, -} from "@stanza/registry"; +} from "@withstanza/schema"; import { defineCommand } from "citty"; import pc from "picocolors"; diff --git a/apps/cli/src/commands/search.ts b/apps/cli/src/commands/search.ts index 5ddfed8..e3dd2cc 100644 --- a/apps/cli/src/commands/search.ts +++ b/apps/cli/src/commands/search.ts @@ -1,4 +1,4 @@ -import { DEFAULT_NAMESPACE, parseModuleSpec, type StanzaManifest } from "@stanza/registry"; +import { DEFAULT_NAMESPACE, parseModuleSpec, type StanzaManifest } from "@withstanza/schema"; import { defineCommand } from "citty"; import pc from "picocolors"; diff --git a/apps/cli/src/lib/codemod-runner.test.ts b/apps/cli/src/lib/codemod-runner.test.ts index 54ee032..d6a82c9 100644 --- a/apps/cli/src/lib/codemod-runner.test.ts +++ b/apps/cli/src/lib/codemod-runner.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { defineModule, emptyManifest, type Module } from "@stanza/registry"; +import { defineModule, type Module, emptyManifest } from "@withstanza/schema"; import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; import { planSlotPackageBootstrap, recordFor, writeDepKeepingHigher } from "./codemod-runner"; diff --git a/apps/cli/src/lib/codemod-runner.ts b/apps/cli/src/lib/codemod-runner.ts index 81ee1f1..0114980 100644 --- a/apps/cli/src/lib/codemod-runner.ts +++ b/apps/cli/src/lib/codemod-runner.ts @@ -1,10 +1,19 @@ import fs from "node:fs"; import path from "node:path"; -import { openProject } from "@stanza/codemods"; -import { addPackageDependency, addPackageScript, addEnvVar } from "@stanza/codemods"; -import type { CodemodContext, Project } from "@stanza/codemods"; -import { CODEMOD_CATALOG } from "@stanza/codemods/builtins"; +import { openProject } from "@withstanza/codemods"; +import { addPackageDependency, addPackageScript, addEnvVar } from "@withstanza/codemods"; +import type { CodemodContext, Project } from "@withstanza/codemods"; +import { CODEMOD_CATALOG } from "@withstanza/codemods/builtins"; +import type { TemplateContext } from "@withstanza/registry"; +import { + activePeerIds, + buildRenderContext, + installPackageJsonTargets, + mergeInstallFields, + renderTemplate, + slotPackageJsonBase, +} from "@withstanza/registry"; import type { AppSpec, CategoryId, @@ -12,21 +21,10 @@ import type { Module, ModuleAdapter, StanzaManifest, - TemplateContext, TemplateRef, -} from "@stanza/registry"; -import { - activePeerIds, - assertSafeRelativePath, - buildRenderContext, - categoryHome, - declaredEnvNames, - installPackageJsonTargets, - mergeInstallFields, - PACKAGE_DIRS, - renderTemplate, - slotPackageJsonBase, -} from "@stanza/registry"; +} from "@withstanza/schema"; +import { categoryHome, declaredEnvNames, PACKAGE_DIRS } from "@withstanza/schema"; +import { assertSafeRelativePath } from "@withstanza/utils"; import semver from "semver"; import { FileTx } from "./file-tx"; @@ -117,7 +115,7 @@ export async function applyModule(args: { // on conflicts; env merges by `name`. const installFields = mergeInstallFields(module, adapter); - // `categoryHome` (in @stanza/registry) is the single decision point for where + // `categoryHome` (in @withstanza/schema) is the single decision point for where // a module's templates/deps/scripts land — package (`packages//`), repo // root, or each targeted app. const packageDir = home.kind === "package" ? home.dir : null; diff --git a/apps/cli/src/lib/manifest.ts b/apps/cli/src/lib/manifest.ts index 14a9baa..836b6b5 100644 --- a/apps/cli/src/lib/manifest.ts +++ b/apps/cli/src/lib/manifest.ts @@ -8,7 +8,7 @@ import { MANIFEST_SCHEMA_URL, StanzaManifestSchema, type StanzaManifest, -} from "@stanza/registry"; +} from "@withstanza/schema"; const MANIFEST_FILENAME = "stanza.json"; diff --git a/apps/cli/src/lib/readme.ts b/apps/cli/src/lib/readme.ts index afd53aa..af9a671 100644 --- a/apps/cli/src/lib/readme.ts +++ b/apps/cli/src/lib/readme.ts @@ -2,8 +2,9 @@ import { createHash } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; -import type { Resolved, ResolvedEntry, StanzaManifest } from "@stanza/registry"; -import { activePeerIds, categoryOrder, synthesizeReadme } from "@stanza/registry"; +import type { Resolved, ResolvedEntry } from "@withstanza/registry"; +import { activePeerIds, categoryOrder, synthesizeReadme } from "@withstanza/registry"; +import type { StanzaManifest } from "@withstanza/schema"; import type { Registries } from "./registry-loader"; diff --git a/apps/cli/src/lib/region-tracker.ts b/apps/cli/src/lib/region-tracker.ts index 16ce191..bc50bd8 100644 --- a/apps/cli/src/lib/region-tracker.ts +++ b/apps/cli/src/lib/region-tracker.ts @@ -1,4 +1,4 @@ -import type { StanzaManifest } from "@stanza/registry"; +import type { StanzaManifest } from "@withstanza/schema"; /** * Region ownership operations on a manifest. Pure functions that return a diff --git a/apps/cli/src/lib/registry-loader.ts b/apps/cli/src/lib/registry-loader.ts index 500bcdc..73187b6 100644 --- a/apps/cli/src/lib/registry-loader.ts +++ b/apps/cli/src/lib/registry-loader.ts @@ -2,8 +2,13 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import type { Module, RegistryConfig, RegistryIndex, StanzaManifest } from "@stanza/registry"; -import { DEFAULT_NAMESPACE, expandEnv, ModuleSchema, RegistryIndexSchema } from "@stanza/registry"; +import type { Module, RegistryConfig, RegistryIndex, StanzaManifest } from "@withstanza/schema"; +import { + DEFAULT_NAMESPACE, + expandEnv, + ModuleSchema, + RegistryIndexSchema, +} from "@withstanza/schema"; /** * The published Stanza website hosts the canonical first-party registry. A diff --git a/apps/cli/src/lib/wizard.ts b/apps/cli/src/lib/wizard.ts index ff517a8..b1e8d77 100644 --- a/apps/cli/src/lib/wizard.ts +++ b/apps/cli/src/lib/wizard.ts @@ -2,17 +2,21 @@ import fs from "node:fs"; import path from "node:path"; import * as p from "@clack/prompts"; -import type { AppSpec, CategoryId, Module, PackageManager, RegistryIndex } from "@stanza/registry"; +import { categoryOrder, resolveAdapter, validateProjectName } from "@withstanza/registry"; +import type { + AppSpec, + CategoryId, + Module, + PackageManager, + RegistryIndex, +} from "@withstanza/schema"; import { categoryLabel, - categoryOrder, defaultWebApp, emptyManifest, isMulti, KNOWN_CATEGORIES, - resolveAdapter, - validateProjectName, -} from "@stanza/registry"; +} from "@withstanza/schema"; import pc from "picocolors"; import type { Registries } from "./registry-loader"; @@ -25,7 +29,7 @@ export type WizardResult = { * multi-app-shaped. */ apps: AppSpec[]; - packageManager: "pnpm" | "bun" | "npm"; + packageManager: PackageManager; /** Chosen modules, keyed by category. Single-choice categories hold one. */ selections: Partial>; }; @@ -37,7 +41,7 @@ export type WizardResult = { */ export type WizardOverrides = { name?: string; - packageManager?: "pnpm" | "bun" | "npm"; + packageManager?: PackageManager; /** Category → module ids (comma-separated on the CLI). Missing = skipped. */ selections: Partial>; }; diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..5c03162 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,2 @@ +public/registry/ +public/schema.json diff --git a/apps/web/content/docs/authoring.mdx b/apps/web/content/docs/authoring.mdx index 7080fe8..777a74a 100644 --- a/apps/web/content/docs/authoring.mdx +++ b/apps/web/content/docs/authoring.mdx @@ -18,7 +18,7 @@ A module lives in a directory named `-/` and exports a default `de ```ts // registry/modules/testing-vitest/module.ts -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "vitest", @@ -151,7 +151,7 @@ Conditional blocks use Handlebars' [built-ins](https://handlebarsjs.com/guide/bu The render context is rebound per target app, so a package-home module shipped into both a web and a native app renders each correctly. -**Template bodies are inlined by the build.** [`packages/registry/src/build.ts`](https://github.com/jakejarvis/stanza/blob/main/packages/registry/src/build.ts) reads every template file and bakes its contents onto `tpl.content` in the per-module JSON. HTTP-loaded modules are self-contained — the CLI never makes follow-up requests for template files. +**Template bodies are inlined by the build.** [`scripts/compile-registry.ts`](https://github.com/jakejarvis/stanza/blob/main/scripts/compile-registry.ts) reads every template file and bakes its contents onto `tpl.content` in the per-module JSON. HTTP-loaded modules are self-contained — the CLI never makes follow-up requests for template files. ## Codemod invocations @@ -286,31 +286,29 @@ Drop these next to `module.ts`: | `logo-light.svg` + `logo-dark.svg` | Theme pair — inlined as `mod.logo = { light, dark }`. Takes precedence over a single `logo.svg`. | | `readme.md` | Markdown contribution to the generated project's README. Renders with the same Handlebars context as templates. | -The build runs SVGO on logos and prefixes every `id` so multiple module logos on the same page can't collide. First-party logos generally come from [svgl.app](https://svgl.app). +The build runs [SVGO](https://svgo.dev/) on logos and prefixes every `id` so multiple module logos on the same page can't collide. First-party logos generally come from [svgl.app](https://svgl.app). ## Validating Module authors don't run a separate validation step — the same Zod schemas the CLI uses are the source of truth: -- `defineModule` (in [`packages/registry/src/module.ts`](https://github.com/jakejarvis/stanza/blob/main/packages/registry/src/module.ts)) throws at runtime on illegal field combinations. +- `defineModule` (in [`packages/schema/src/module.ts`](https://github.com/jakejarvis/stanza/blob/main/packages/schema/src/module.ts)) throws at runtime on illegal field combinations. - `ModuleSchema.parse` runs on every HTTP fetch and rejects malformed manifests with structured errors. - `vp test` runs the whole repo's schema + resolver tests; add a fixture under `registry/modules//` and the existing suite picks it up. - `vp check` does a type-aware lint pass that catches most authoring mistakes statically. -For a third-party registry, the same `ModuleSchema` ships from `@stanza/registry`. The simplest test loop is to build your registry and point the consumer's `STANZA_REGISTRY` at the built main file (`STANZA_REGISTRY=./out/registry/index.json stanza search`) and watch for parse errors. +For a third-party registry, the same `ModuleSchema` ships from `@withstanza/schema` (published to npm). The simplest test loop is to build your registry and point the consumer's `STANZA_REGISTRY` at the built main file (`STANZA_REGISTRY=./out/index.json stanza search`) and watch for parse errors. ## Building a registry -The build script ([`packages/registry/src/build.ts`](https://github.com/jakejarvis/stanza/blob/main/packages/registry/src/build.ts)) scans `registry/modules/*` and emits: +The build script ([`scripts/compile-registry.ts`](https://github.com/jakejarvis/stanza/blob/main/scripts/compile-registry.ts)) scans `registry/modules/*` and writes the main file plus one JSON per module directly under the output directory: ``` / -├── registry/ -│ ├── index.json # main file: categories + per-module summaries (each with a `path`) -│ └── modules/ -│ ├── -.json # one file per module, templates inlined -│ └── … -└── schema.json # JSON Schema for stanza.json +├── index.json # main file: categories + per-module summaries (each with a `path`) +└── modules/ + ├── -.json # one file per module, templates inlined + └── … ``` Each per-module JSON is self-contained: template bodies, deps, env vars, codemod invocations, the optimized logo SVG, and the rendered readme are all in one document. The CLI never makes follow-up requests for module assets. @@ -318,14 +316,14 @@ Each per-module JSON is self-contained: template bodies, deps, env vars, codemod To run the build: ```sh -# Default output: /dist -jiti packages/registry/src/build.ts +# Default output: /dist (writes dist/index.json + dist/modules/*.json) +jiti scripts/compile-registry.ts # Or point it elsewhere (this is what the web app's prebuild does): -jiti packages/registry/src/build.ts apps/web/public +jiti scripts/compile-registry.ts apps/web/public/registry ``` -For a third-party registry, fork the build script (or call it as a library) against your own `registry/modules/` tree. The output shape is identical. +The script is a thin wrapper over `@withstanza/schema` (`CATEGORIES` + the contract types) and SVGO — for a third-party registry, fork it (or copy it) against your own `registry/modules/` tree. `compileRegistry({ outDir })` is also exported for in-process callers. The output shape is identical. ## Hosting a registry @@ -405,7 +403,7 @@ A minimal third-party module from scratch. **1. Author `module.ts`** at `registry/modules/testing-cosmos/module.ts` in your registry repo: ```ts -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "cosmos", @@ -423,13 +421,13 @@ export default defineModule({ **2. Build the registry** with the script above. Output: ``` -out/registry/ +out/ ├── index.json └── modules/ └── testing-cosmos.json ``` -**3. Host it.** Push `out/registry/` (or its parent) to wherever you serve static files. Say the main file ends up at `https://reg.acme.example/index.json`. +**3. Host it.** Push `out/` to wherever you serve static files. Say the main file ends up at `https://reg.acme.example/index.json`. **4. Tell users how to install.** Their `stanza.json`: diff --git a/apps/web/package.json b/apps/web/package.json index fbf3315..f813e26 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,5 +1,5 @@ { - "name": "@stanza/web", + "name": "@withstanza/web", "version": "0.0.0", "private": true, "license": "MIT", @@ -16,7 +16,6 @@ "@number-flow/react": "^0.6.0", "@orama/orama": "^3.1.18", "@pierre/trees": "1.0.0-beta.4", - "@stanza/registry": "workspace:*", "@tabler/icons-react": "^3.44.0", "@tailwindcss/vite": "^4.3.0", "@takumi-rs/image-response": "^1.6.0", @@ -28,6 +27,8 @@ "@tanstack/react-start": "^1.168.18", "@vercel/analytics": "^2.0.1", "@vercel/functions": "^3.6.1", + "@withstanza/registry": "workspace:*", + "@withstanza/schema": "workspace:*", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "fumadocs-core": "^16.9.3", diff --git a/apps/web/src/components/builder/index.tsx b/apps/web/src/components/builder/index.tsx index cbf9783..925ce67 100644 --- a/apps/web/src/components/builder/index.tsx +++ b/apps/web/src/components/builder/index.tsx @@ -1,14 +1,13 @@ -import type { CategoryId, ModuleMetadata } from "@stanza/registry"; -import { isMulti } from "@stanza/registry"; import { useNavigate, useRouterState } from "@tanstack/react-router"; import { track } from "@vercel/analytics"; +import type { CategoryId, ModuleMetadata, PackageManager } from "@withstanza/schema"; +import { isMulti } from "@withstanza/schema"; import { startTransition, useCallback, useMemo, useOptimistic, useRef } from "react"; import { FilePreview } from "@/components/builder/file-preview"; import { ModuleCards } from "@/components/builder/module-cards"; import { ProjectSetup } from "@/components/builder/project-setup"; import { CommandPreview } from "@/components/command-preview"; -import type { PackageManager } from "@/lib/package-manager"; import { type BuilderSearch, type Selections, diff --git a/apps/web/src/components/builder/module-cards.tsx b/apps/web/src/components/builder/module-cards.tsx index 392bc9b..cf6ac41 100644 --- a/apps/web/src/components/builder/module-cards.tsx +++ b/apps/web/src/components/builder/module-cards.tsx @@ -1,14 +1,15 @@ -import type { CategoryId, ModuleMetadata, ResolveError } from "@stanza/registry"; +import { IconCheck, IconInfoCircle } from "@tabler/icons-react"; +import { Link } from "@tanstack/react-router"; +import type { ResolveError } from "@withstanza/registry"; +import { resolveAdapter } from "@withstanza/registry"; +import type { CategoryId, ModuleMetadata } from "@withstanza/schema"; import { categoryLabel, emptyManifest, isMulti, KNOWN_CATEGORIES, PEER_CATEGORIES, - resolveAdapter, -} from "@stanza/registry"; -import { IconCheck, IconInfoCircle } from "@tabler/icons-react"; -import { Link } from "@tanstack/react-router"; +} from "@withstanza/schema"; import { memo, useCallback, useMemo } from "react"; import { ModuleLogo } from "@/components/module-logo"; diff --git a/apps/web/src/components/builder/project-setup.tsx b/apps/web/src/components/builder/project-setup.tsx index 1acb36e..ba583c6 100644 --- a/apps/web/src/components/builder/project-setup.tsx +++ b/apps/web/src/components/builder/project-setup.tsx @@ -1,6 +1,6 @@ -import { validateProjectName } from "@stanza/registry"; import { IconAlertCircle } from "@tabler/icons-react"; import { useDebouncedCallback } from "@tanstack/react-pacer"; +import { validateProjectName } from "@withstanza/registry"; import type { ChangeEvent } from "react"; import { useId, useState } from "react"; diff --git a/apps/web/src/components/command-preview.tsx b/apps/web/src/components/command-preview.tsx index abf5da7..dff68f8 100644 --- a/apps/web/src/components/command-preview.tsx +++ b/apps/web/src/components/command-preview.tsx @@ -1,9 +1,9 @@ import { track } from "@vercel/analytics"; +import type { PackageManager } from "@withstanza/schema"; import { PackageManagerSelect } from "@/components/package-manager-select"; import { CopyableField } from "@/components/ui/copyable-field"; import { selectionProperties } from "@/lib/analytics"; -import type { PackageManager } from "@/lib/package-manager"; import { buildCommand, DEFAULT_NAME, type Selections } from "@/lib/selection"; /** diff --git a/apps/web/src/components/detail/adapter-switcher.tsx b/apps/web/src/components/detail/adapter-switcher.tsx index 12eff82..6edd7cb 100644 --- a/apps/web/src/components/detail/adapter-switcher.tsx +++ b/apps/web/src/components/detail/adapter-switcher.tsx @@ -1,5 +1,5 @@ -import type { CategoryId, ModuleMetadata, RegistryIndex } from "@stanza/registry"; -import { categoryLabel, KNOWN_CATEGORIES } from "@stanza/registry"; +import type { CategoryId, ModuleMetadata, RegistryIndex } from "@withstanza/schema"; +import { categoryLabel, KNOWN_CATEGORIES } from "@withstanza/schema"; import { useMemo } from "react"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; diff --git a/apps/web/src/components/detail/install.tsx b/apps/web/src/components/detail/install.tsx index 7a75976..9ddab94 100644 --- a/apps/web/src/components/detail/install.tsx +++ b/apps/web/src/components/detail/install.tsx @@ -1,8 +1,8 @@ +import { DEFAULT_PACKAGE_MANAGER, type PackageManager } from "@withstanza/schema"; import { useState } from "react"; import { CommandPreview } from "@/components/command-preview"; import { Section } from "@/components/detail/section"; -import { DEFAULT_PACKAGE_MANAGER, type PackageManager } from "@/lib/package-manager"; import type { Selections } from "@/lib/selection"; export function Install({ name, selections }: { name: string; selections: Selections }) { diff --git a/apps/web/src/components/detail/tables.tsx b/apps/web/src/components/detail/tables.tsx index 80176a4..5efd66b 100644 --- a/apps/web/src/components/detail/tables.tsx +++ b/apps/web/src/components/detail/tables.tsx @@ -1,5 +1,5 @@ -import type { EnvVar } from "@stanza/registry"; import { IconExternalLink } from "@tabler/icons-react"; +import type { EnvVar } from "@withstanza/schema"; import { Section, SectionList } from "@/components/detail/section"; import { Badge } from "@/components/ui/badge"; diff --git a/apps/web/src/components/detail/templates-list.tsx b/apps/web/src/components/detail/templates-list.tsx index 5f17581..c526480 100644 --- a/apps/web/src/components/detail/templates-list.tsx +++ b/apps/web/src/components/detail/templates-list.tsx @@ -1,4 +1,4 @@ -import type { TemplateRef } from "@stanza/registry"; +import type { TemplateRef } from "@withstanza/schema"; import { useMemo, useState } from "react"; import { Section, SectionList } from "@/components/detail/section"; diff --git a/apps/web/src/components/module-logo.tsx b/apps/web/src/components/module-logo.tsx index ebba970..c065557 100644 --- a/apps/web/src/components/module-logo.tsx +++ b/apps/web/src/components/module-logo.tsx @@ -1,4 +1,4 @@ -import type { Logo } from "@stanza/registry"; +import type { Logo } from "@withstanza/schema"; import { useMemo } from "react"; import { cn } from "@/lib/utils"; diff --git a/apps/web/src/components/package-manager-select.tsx b/apps/web/src/components/package-manager-select.tsx index 55ab549..ce0a13a 100644 --- a/apps/web/src/components/package-manager-select.tsx +++ b/apps/web/src/components/package-manager-select.tsx @@ -1,4 +1,5 @@ import { IconChevronDown } from "@tabler/icons-react"; +import { isPackageManager, type PackageManager } from "@withstanza/schema"; import type { ComponentProps, ReactNode } from "react"; import { Button } from "@/components/ui/button"; @@ -9,7 +10,7 @@ import { DropdownMenuRadioItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { isPackageManager, PACKAGE_MANAGERS, type PackageManager } from "@/lib/package-manager"; +import { PACKAGE_MANAGERS } from "@/lib/package-manager"; function NpmLogo(props: ComponentProps<"svg">) { return ( diff --git a/apps/web/src/components/search/site-search.tsx b/apps/web/src/components/search/site-search.tsx index 018c882..1731440 100644 --- a/apps/web/src/components/search/site-search.tsx +++ b/apps/web/src/components/search/site-search.tsx @@ -1,11 +1,10 @@ "use client"; -import type { ModuleMetadata, RegistryIndex } from "@stanza/registry"; -import { categoryLabel } from "@stanza/registry"; import { IconBookmark, IconSearch } from "@tabler/icons-react"; import { formatForDisplay, useHotkey } from "@tanstack/react-hotkeys"; import { useDebouncedCallback } from "@tanstack/react-pacer"; import { useNavigate } from "@tanstack/react-router"; +import { categoryLabel, type ModuleMetadata, type RegistryIndex } from "@withstanza/schema"; import type { SortedResult } from "fumadocs-core/search"; import { useDocsSearch } from "fumadocs-core/search/client"; import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; diff --git a/apps/web/src/lib/analytics.ts b/apps/web/src/lib/analytics.ts index f7aa6af..fd664e9 100644 --- a/apps/web/src/lib/analytics.ts +++ b/apps/web/src/lib/analytics.ts @@ -1,4 +1,4 @@ -import { KNOWN_CATEGORIES } from "@stanza/registry"; +import { KNOWN_CATEGORIES } from "@withstanza/schema"; import type { Selections } from "@/lib/selection"; diff --git a/apps/web/src/lib/module-search.ts b/apps/web/src/lib/module-search.ts index dbb2a53..e70b3f3 100644 --- a/apps/web/src/lib/module-search.ts +++ b/apps/web/src/lib/module-search.ts @@ -1,4 +1,4 @@ -import type { CategoryId, ModuleMetadata } from "@stanza/registry"; +import type { CategoryId, ModuleMetadata } from "@withstanza/schema"; export function groupByCategory( modules: ModuleMetadata[], diff --git a/apps/web/src/lib/package-manager.ts b/apps/web/src/lib/package-manager.ts index 11419cf..d057259 100644 --- a/apps/web/src/lib/package-manager.ts +++ b/apps/web/src/lib/package-manager.ts @@ -1,14 +1,8 @@ -export type PackageManager = "pnpm" | "npm" | "bun"; +import type { PackageManager } from "@withstanza/schema"; -/** Ordered list shown in the dropdown — pnpm first (the default). */ +/** Ordered list shown in the dropdown — pnpm first (the default). UI-only. */ export const PACKAGE_MANAGERS: { id: PackageManager; label: string }[] = [ { id: "pnpm", label: "pnpm" }, { id: "npm", label: "npm" }, { id: "bun", label: "bun" }, ]; - -export const DEFAULT_PACKAGE_MANAGER: PackageManager = "pnpm"; - -export function isPackageManager(value: string | null | undefined): value is PackageManager { - return PACKAGE_MANAGERS.some((pm) => pm.id === value); -} diff --git a/apps/web/src/lib/prerender.ts b/apps/web/src/lib/prerender.ts index 2488d18..88a9b40 100644 --- a/apps/web/src/lib/prerender.ts +++ b/apps/web/src/lib/prerender.ts @@ -42,7 +42,7 @@ function listRegistryPaths(): string[] { } catch (error) { throw new Error( `Prerender enumeration failed: ${registryPath} is missing. ` + - `Run \`pnpm --filter @stanza/web prebuild\` (or \`vp run @stanza/web#build\`) first.`, + `Run \`jiti scripts/compile-registry.ts apps/web/public/registry\` first.`, { cause: error }, ); } @@ -67,6 +67,7 @@ export function listPrerenderPages() { "/docs/llms-full.txt", ...registry, "/stats", + "/schema.json", ]; return paths.map((path) => ({ path, prerender: { enabled: true } })); } diff --git a/apps/web/src/lib/selection.ts b/apps/web/src/lib/selection.ts index b841361..aacfd8f 100644 --- a/apps/web/src/lib/selection.ts +++ b/apps/web/src/lib/selection.ts @@ -1,25 +1,20 @@ +import type { Resolved, ResolvedEntry } from "@withstanza/registry"; +import { categoryOrder, resolveAdapter } from "@withstanza/registry"; import type { AppSpec, CategoryId, Module, ModuleMetadata, - Resolved, - ResolvedEntry, -} from "@stanza/registry"; -import { - categoryOrder, - defaultWebApp, - emptyManifest, - KNOWN_CATEGORIES, - PEER_CATEGORIES, - resolveAdapter, -} from "@stanza/registry"; - + PackageManager, +} from "@withstanza/schema"; import { DEFAULT_PACKAGE_MANAGER, + defaultWebApp, + emptyManifest, isPackageManager, - type PackageManager, -} from "@/lib/package-manager"; + KNOWN_CATEGORIES, + PEER_CATEGORIES, +} from "@withstanza/schema"; /** Selected module ids per category. Single-choice categories hold ≤ 1. */ export type Selections = Partial>; diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 1a2b642..6b4dfa9 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -10,6 +10,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as StatsRouteImport } from './routes/stats' +import { Route as SchemaDotjsonRouteImport } from './routes/schema[.]json' import { Route as OgDotwebpRouteImport } from './routes/og[.]webp' import { Route as DocsDotmdRouteImport } from './routes/docs[.]md' import { Route as IndexRouteImport } from './routes/index' @@ -31,6 +32,11 @@ const StatsRoute = StatsRouteImport.update({ path: '/stats', getParentRoute: () => rootRouteImport, } as any) +const SchemaDotjsonRoute = SchemaDotjsonRouteImport.update({ + id: '/schema.json', + path: '/schema.json', + getParentRoute: () => rootRouteImport, +} as any) const OgDotwebpRoute = OgDotwebpRouteImport.update({ id: '/og.webp', path: '/og.webp', @@ -113,6 +119,7 @@ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/docs.md': typeof DocsDotmdRoute '/og.webp': typeof OgDotwebpRoute + '/schema.json': typeof SchemaDotjsonRoute '/stats': typeof StatsRoute '/api/events': typeof ApiEventsRoute '/docs/$': typeof DocsSplatRoute @@ -131,6 +138,7 @@ export interface FileRoutesByTo { '/': typeof IndexRoute '/docs.md': typeof DocsDotmdRoute '/og.webp': typeof OgDotwebpRoute + '/schema.json': typeof SchemaDotjsonRoute '/stats': typeof StatsRoute '/api/events': typeof ApiEventsRoute '/docs/$': typeof DocsSplatRoute @@ -150,6 +158,7 @@ export interface FileRoutesById { '/': typeof IndexRoute '/docs.md': typeof DocsDotmdRoute '/og.webp': typeof OgDotwebpRoute + '/schema.json': typeof SchemaDotjsonRoute '/stats': typeof StatsRoute '/api/events': typeof ApiEventsRoute '/docs/$': typeof DocsSplatRoute @@ -170,6 +179,7 @@ export interface FileRouteTypes { | '/' | '/docs.md' | '/og.webp' + | '/schema.json' | '/stats' | '/api/events' | '/docs/$' @@ -188,6 +198,7 @@ export interface FileRouteTypes { | '/' | '/docs.md' | '/og.webp' + | '/schema.json' | '/stats' | '/api/events' | '/docs/$' @@ -206,6 +217,7 @@ export interface FileRouteTypes { | '/' | '/docs.md' | '/og.webp' + | '/schema.json' | '/stats' | '/api/events' | '/docs/$' @@ -225,6 +237,7 @@ export interface RootRouteChildren { IndexRoute: typeof IndexRoute DocsDotmdRoute: typeof DocsDotmdRoute OgDotwebpRoute: typeof OgDotwebpRoute + SchemaDotjsonRoute: typeof SchemaDotjsonRoute StatsRoute: typeof StatsRoute ApiEventsRoute: typeof ApiEventsRoute DocsSplatRoute: typeof DocsSplatRoute @@ -249,6 +262,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof StatsRouteImport parentRoute: typeof rootRouteImport } + '/schema.json': { + id: '/schema.json' + path: '/schema.json' + fullPath: '/schema.json' + preLoaderRoute: typeof SchemaDotjsonRouteImport + parentRoute: typeof rootRouteImport + } '/og.webp': { id: '/og.webp' path: '/og.webp' @@ -361,6 +381,7 @@ const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, DocsDotmdRoute: DocsDotmdRoute, OgDotwebpRoute: OgDotwebpRoute, + SchemaDotjsonRoute: SchemaDotjsonRoute, StatsRoute: StatsRoute, ApiEventsRoute: ApiEventsRoute, DocsSplatRoute: DocsSplatRoute, diff --git a/apps/web/src/routes/api.search.modules.ts b/apps/web/src/routes/api.search.modules.ts index dabd09c..e1abd6d 100644 --- a/apps/web/src/routes/api.search.modules.ts +++ b/apps/web/src/routes/api.search.modules.ts @@ -1,6 +1,6 @@ import { create, insert, search } from "@orama/orama"; -import type { Module, ModuleMetadata, RegistryIndex } from "@stanza/registry"; import { createFileRoute } from "@tanstack/react-router"; +import type { Module, ModuleMetadata, RegistryIndex } from "@withstanza/schema"; import { cache } from "react"; import { loadRegistryFile } from "@/server/registry-base.server"; diff --git a/apps/web/src/routes/docs.$.tsx b/apps/web/src/routes/docs.$.tsx index 6e72b8b..63d37fd 100644 --- a/apps/web/src/routes/docs.$.tsx +++ b/apps/web/src/routes/docs.$.tsx @@ -86,7 +86,7 @@ export const Route = createFileRoute("/docs/$")({ title, description, path, - ogImage: loaderData ? `/og${loaderData.url}.webp` : undefined, + ogImage: loaderData ? `/og${path}.webp` : undefined, markdownPath: loaderData ? `${path}.md` : undefined, jsonLd: loaderData && title diff --git a/apps/web/src/routes/og.registry.$category.{$id}[.]webp.ts b/apps/web/src/routes/og.registry.$category.{$id}[.]webp.ts index 2d0702d..cba3c78 100644 --- a/apps/web/src/routes/og.registry.$category.{$id}[.]webp.ts +++ b/apps/web/src/routes/og.registry.$category.{$id}[.]webp.ts @@ -1,6 +1,6 @@ -import type { RegistryIndex } from "@stanza/registry"; import { ImageResponse } from "@takumi-rs/image-response"; import { createFileRoute } from "@tanstack/react-router"; +import type { RegistryIndex } from "@withstanza/schema"; import { OgCard } from "@/server/og-card.server"; import { loadRegistryFile } from "@/server/registry-base.server"; diff --git a/apps/web/src/routes/registry.$category.$id.tsx b/apps/web/src/routes/registry.$category.$id.tsx index 0af30f2..661450d 100644 --- a/apps/web/src/routes/registry.$category.$id.tsx +++ b/apps/web/src/routes/registry.$category.$id.tsx @@ -1,7 +1,7 @@ -import type { CategoryId } from "@stanza/registry"; -import { categoryLabel, KNOWN_CATEGORIES, PEER_CATEGORIES } from "@stanza/registry"; import { IconArrowLeft, IconExternalLink } from "@tabler/icons-react"; import { Link, createFileRoute, notFound, useNavigate } from "@tanstack/react-router"; +import type { CategoryId } from "@withstanza/schema"; +import { categoryLabel, KNOWN_CATEGORIES, PEER_CATEGORIES } from "@withstanza/schema"; import { useCallback, useMemo } from "react"; import { AdapterSwitcher } from "@/components/detail/adapter-switcher"; diff --git a/apps/web/src/routes/registry.$category.index.tsx b/apps/web/src/routes/registry.$category.index.tsx index 07b12fe..c3a2c21 100644 --- a/apps/web/src/routes/registry.$category.index.tsx +++ b/apps/web/src/routes/registry.$category.index.tsx @@ -1,6 +1,6 @@ -import { categoryDescription, categoryLabel, isCategoryId } from "@stanza/registry"; import { IconArrowLeft } from "@tabler/icons-react"; import { Link, createFileRoute, notFound, useLoaderData } from "@tanstack/react-router"; +import { categoryDescription, categoryLabel, isCategoryId } from "@withstanza/schema"; import { useMemo } from "react"; import { Section, SectionList } from "@/components/detail/section"; diff --git a/apps/web/src/routes/registry.index.tsx b/apps/web/src/routes/registry.index.tsx index 29cac48..2dd8cc7 100644 --- a/apps/web/src/routes/registry.index.tsx +++ b/apps/web/src/routes/registry.index.tsx @@ -1,6 +1,6 @@ -import type { Category, ModuleMetadata } from "@stanza/registry"; import { IconArrowLeft } from "@tabler/icons-react"; import { Link, createFileRoute, useLoaderData } from "@tanstack/react-router"; +import type { Category, ModuleMetadata } from "@withstanza/schema"; import { useMemo } from "react"; import { SectionList } from "@/components/detail/section"; diff --git a/apps/web/src/routes/schema[.]json.ts b/apps/web/src/routes/schema[.]json.ts new file mode 100644 index 0000000..18b3684 --- /dev/null +++ b/apps/web/src/routes/schema[.]json.ts @@ -0,0 +1,24 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { StanzaManifestSchema } from "@withstanza/schema"; +import { z } from "zod"; + +export const Route = createFileRoute("/schema.json")({ + server: { + handlers: { + GET: () => { + const compiled = { + $id: "https://stanza.tools/schema.json", + title: "Stanza manifest", + description: "Schema for stanza.json — a Stanza monorepo manifest.", + ...z.toJSONSchema(StanzaManifestSchema), + }; + + return new Response(`${JSON.stringify(compiled, null, 2)}\n`, { + headers: { + "content-type": "application/json; charset=utf-8", + }, + }); + }, + }, + }, +}); diff --git a/apps/web/src/routes/stats.tsx b/apps/web/src/routes/stats.tsx index fa113ec..bc06b0a 100644 --- a/apps/web/src/routes/stats.tsx +++ b/apps/web/src/routes/stats.tsx @@ -1,7 +1,7 @@ import NumberFlow from "@number-flow/react"; -import type { CategoryId, ModuleMetadata } from "@stanza/registry"; -import { categoryLabel, KNOWN_CATEGORIES } from "@stanza/registry"; import { createFileRoute, Link, useLoaderData } from "@tanstack/react-router"; +import type { CategoryId, ModuleMetadata } from "@withstanza/schema"; +import { categoryLabel, KNOWN_CATEGORIES } from "@withstanza/schema"; import { lazy, Suspense, useEffect, useState } from "react"; import { ModuleLogo } from "@/components/module-logo"; diff --git a/apps/web/src/server/builder-state.functions.ts b/apps/web/src/server/builder-state.functions.ts index ef0ff21..5af7200 100644 --- a/apps/web/src/server/builder-state.functions.ts +++ b/apps/web/src/server/builder-state.functions.ts @@ -1,11 +1,11 @@ +import { createServerFn } from "@tanstack/react-start"; import { synthesizeEnvExample, synthesizeManifest, synthesizePackageJsons, synthesizeReadme, synthesizeTemplates, -} from "@stanza/registry"; -import { createServerFn } from "@tanstack/react-start"; +} from "@withstanza/registry"; import { DEFAULT_BUILDER_APPS, diff --git a/apps/web/src/server/module-detail.functions.ts b/apps/web/src/server/module-detail.functions.ts index 742fba1..dad3757 100644 --- a/apps/web/src/server/module-detail.functions.ts +++ b/apps/web/src/server/module-detail.functions.ts @@ -1,3 +1,5 @@ +import { createServerFn } from "@tanstack/react-start"; +import { mergeInstallFields, resolveAdapter } from "@withstanza/registry"; import type { CategoryId, EnvVar, @@ -6,17 +8,14 @@ import type { ModuleMetadata, PeerRequirement, RegistryIndex, -} from "@stanza/registry"; +} from "@withstanza/schema"; import { emptyManifest, isCategoryId, isValidModuleId, KNOWN_CATEGORIES, - mergeInstallFields, PEER_CATEGORIES, - resolveAdapter, -} from "@stanza/registry"; -import { createServerFn } from "@tanstack/react-start"; +} from "@withstanza/schema"; import type { Preview } from "@/server/highlighter"; import { renderPreview } from "@/server/highlighter.server"; diff --git a/apps/web/src/server/og-card.server.tsx b/apps/web/src/server/og-card.server.tsx index 748e047..42bd8d9 100644 --- a/apps/web/src/server/og-card.server.tsx +++ b/apps/web/src/server/og-card.server.tsx @@ -1,5 +1,4 @@ -import type { ModuleMetadata } from "@stanza/registry"; -import { categoryLabel } from "@stanza/registry"; +import { categoryLabel, type ModuleMetadata } from "@withstanza/schema"; import type { CSSProperties, ReactElement } from "react"; /** diff --git a/apps/web/src/server/registry-base.server.ts b/apps/web/src/server/registry-base.server.ts index 906b6ee..9a2f926 100644 --- a/apps/web/src/server/registry-base.server.ts +++ b/apps/web/src/server/registry-base.server.ts @@ -29,7 +29,7 @@ export async function loadRegistryFile(relativePath: string): Promise { return parsed; } catch { throw new Error( - `Registry asset not found: ${filePath} (run \`pnpm --filter @stanza/web prebuild\` to populate public/registry/)`, + `Registry asset not found: ${filePath} (run \`pnpm --filter @withstanza/web prebuild\` to populate public/registry/)`, ); } } diff --git a/apps/web/src/server/registry-index.functions.ts b/apps/web/src/server/registry-index.functions.ts index d3a418d..db4c4db 100644 --- a/apps/web/src/server/registry-index.functions.ts +++ b/apps/web/src/server/registry-index.functions.ts @@ -1,5 +1,5 @@ -import type { RegistryIndex } from "@stanza/registry"; import { createServerFn } from "@tanstack/react-start"; +import type { RegistryIndex } from "@withstanza/schema"; import { loadRegistryFile } from "@/server/registry-base.server"; diff --git a/apps/web/src/server/registry-modules.server.ts b/apps/web/src/server/registry-modules.server.ts index e656b77..cc8bd87 100644 --- a/apps/web/src/server/registry-modules.server.ts +++ b/apps/web/src/server/registry-modules.server.ts @@ -1,4 +1,4 @@ -import type { Module, RegistryIndex } from "@stanza/registry"; +import type { Module, RegistryIndex } from "@withstanza/schema"; import { loadRegistryFile } from "@/server/registry-base.server"; @@ -7,7 +7,7 @@ let modulesPromise: Promise> | undefined; /** * Per-process cache for the full module catalog. Registry data is immutable * per deployment, so cache lifetime = process lifetime (restart the dev server - * after `vp run @stanza/web#prebuild` to pick up local edits). Per-module + * after `vp run @withstanza/web#prebuild` to pick up local edits). Per-module * failures are isolated — the failing module is dropped and reported. */ export function getAllModules(): Promise> { diff --git a/apps/web/src/server/stats.functions.ts b/apps/web/src/server/stats.functions.ts index 1201e57..cf66c2e 100644 --- a/apps/web/src/server/stats.functions.ts +++ b/apps/web/src/server/stats.functions.ts @@ -1,7 +1,7 @@ -import type { CategoryId } from "@stanza/registry"; -import { KNOWN_CATEGORIES } from "@stanza/registry"; import { createServerFn } from "@tanstack/react-start"; import { getCache, waitUntil } from "@vercel/functions"; +import type { CategoryId } from "@withstanza/schema"; +import { KNOWN_CATEGORIES } from "@withstanza/schema"; import { getQueryConfig, runQuery } from "@/server/posthog-query.server"; diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 4c9b538..9486727 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -55,12 +55,9 @@ export default defineConfig({ tasks: { "compile-registry": { cwd: "../..", - command: "jiti packages/registry/src/build.ts apps/web/public", - input: [ - { pattern: "registry/modules/**", base: "workspace" }, - { pattern: "packages/registry/src/**", base: "workspace" }, - ], - output: ["apps/web/public/registry/**", "apps/web/public/schema.json"], + command: "jiti scripts/compile-registry.ts apps/web/public/registry", + input: [{ pattern: "registry/modules/**", base: "workspace" }], + output: [{ pattern: "public/registry/**", base: "package" }], }, }, }, diff --git a/package.json b/package.json index d9801f1..531b7ae 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,12 @@ "private": true, "description": "Modular monorepo template CLI — shadcn for stacks.", "license": "MIT", - "author": "Jake Jarvis ", + "author": "Jake Jarvis ", "type": "module", "scripts": { "build": "vp run -r build", - "build:cli": "vp run stanza-cli#build && vp run create-stanza#build", - "build:web": "vp run @stanza/web#build", + "build:cli": "vp run @withstanza/schema#build && vp run stanza-cli#build && vp run create-stanza#build", + "build:web": "vp run @withstanza/web#build", "dev": "vp run -r dev", "test": "vp test", "lint": "vp lint", @@ -25,7 +25,9 @@ "@changesets/changelog-github": "^0.7.0", "@changesets/cli": "^2.31.0", "@types/node": "^25.9.1", + "@withstanza/schema": "workspace:*", "jiti": "^2.7.0", + "svgo": "^4.0.1", "typescript": "^6.0.3", "vite-plus": "catalog:" }, diff --git a/packages/codemods/package.json b/packages/codemods/package.json index d4308c7..9dfba8c 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,5 +1,5 @@ { - "name": "@stanza/codemods", + "name": "@withstanza/codemods", "version": "0.0.0", "private": true, "license": "MIT", @@ -11,7 +11,8 @@ "./builtins": "./src/builtins/index.ts" }, "dependencies": { - "@stanza/registry": "workspace:*", + "@withstanza/schema": "workspace:*", + "@withstanza/utils": "workspace:*", "jsonc-parser": "^3.3.1", "ts-morph": "^28.0.0" }, diff --git a/packages/codemods/src/builtins/add-array-entry-in-call.test.ts b/packages/codemods/src/builtins/add-array-entry-in-call.test.ts index d2ee3e8..33ea38e 100644 --- a/packages/codemods/src/builtins/add-array-entry-in-call.test.ts +++ b/packages/codemods/src/builtins/add-array-entry-in-call.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { emptyManifest } from "@stanza/registry"; +import { emptyManifest } from "@withstanza/schema"; import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; import { type CodemodContext, openProject } from "../index"; diff --git a/packages/codemods/src/builtins/add-array-entry-in-call.ts b/packages/codemods/src/builtins/add-array-entry-in-call.ts index a5879c1..058a769 100644 --- a/packages/codemods/src/builtins/add-array-entry-in-call.ts +++ b/packages/codemods/src/builtins/add-array-entry-in-call.ts @@ -1,6 +1,6 @@ import path from "node:path"; -import { assertSafeRelativePath } from "@stanza/registry"; +import { assertSafeRelativePath } from "@withstanza/utils"; import type { ArrayLiteralExpression, CallExpression, ObjectLiteralExpression } from "ts-morph"; import { diff --git a/packages/codemods/src/builtins/add-jsx-child.test.ts b/packages/codemods/src/builtins/add-jsx-child.test.ts index 1b47bd3..49e0198 100644 --- a/packages/codemods/src/builtins/add-jsx-child.test.ts +++ b/packages/codemods/src/builtins/add-jsx-child.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { emptyManifest } from "@stanza/registry"; +import { emptyManifest } from "@withstanza/schema"; import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; import { type CodemodContext, openProject } from "../index"; diff --git a/packages/codemods/src/builtins/add-jsx-child.ts b/packages/codemods/src/builtins/add-jsx-child.ts index bdec1f4..6096412 100644 --- a/packages/codemods/src/builtins/add-jsx-child.ts +++ b/packages/codemods/src/builtins/add-jsx-child.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; -import { assertSafeRelativePath } from "@stanza/registry"; +import { assertSafeRelativePath } from "@withstanza/utils"; import { addDefaultImport, diff --git a/packages/codemods/src/builtins/add-package-dep.test.ts b/packages/codemods/src/builtins/add-package-dep.test.ts index 41cb170..8d74781 100644 --- a/packages/codemods/src/builtins/add-package-dep.test.ts +++ b/packages/codemods/src/builtins/add-package-dep.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { emptyManifest } from "@stanza/registry"; +import { emptyManifest } from "@withstanza/schema"; import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; import { openProject, type CodemodContext } from "../index"; diff --git a/packages/codemods/src/builtins/add-plugin-to-call.test.ts b/packages/codemods/src/builtins/add-plugin-to-call.test.ts index e312d35..fc48235 100644 --- a/packages/codemods/src/builtins/add-plugin-to-call.test.ts +++ b/packages/codemods/src/builtins/add-plugin-to-call.test.ts @@ -1,4 +1,4 @@ -import { emptyManifest } from "@stanza/registry"; +import { emptyManifest } from "@withstanza/schema"; import { describe, expect, it } from "vite-plus/test"; import { openProject, type CodemodContext, type Project } from "../index"; diff --git a/packages/codemods/src/builtins/add-plugin-to-call.ts b/packages/codemods/src/builtins/add-plugin-to-call.ts index acf195e..ba0e7ed 100644 --- a/packages/codemods/src/builtins/add-plugin-to-call.ts +++ b/packages/codemods/src/builtins/add-plugin-to-call.ts @@ -1,6 +1,6 @@ import path from "node:path"; -import { assertSafeRelativePath } from "@stanza/registry"; +import { assertSafeRelativePath } from "@withstanza/utils"; import type { ArrayLiteralExpression } from "ts-morph"; import { diff --git a/packages/codemods/src/builtins/append-to-file.test.ts b/packages/codemods/src/builtins/append-to-file.test.ts index ff63a5c..71c598d 100644 --- a/packages/codemods/src/builtins/append-to-file.test.ts +++ b/packages/codemods/src/builtins/append-to-file.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { emptyManifest } from "@stanza/registry"; +import { emptyManifest } from "@withstanza/schema"; import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; import { openProject, type CodemodContext, type Project } from "../index"; diff --git a/packages/codemods/src/builtins/append-to-file.ts b/packages/codemods/src/builtins/append-to-file.ts index cf0a317..251908f 100644 --- a/packages/codemods/src/builtins/append-to-file.ts +++ b/packages/codemods/src/builtins/append-to-file.ts @@ -1,9 +1,9 @@ import fs from "node:fs"; import path from "node:path"; -import { assertSafeRelativePath } from "@stanza/registry"; +import { assertSafeRelativePath } from "@withstanza/utils"; -import type { Codemod } from "../index"; +import { type Codemod } from "../index"; /** Comment-line shape used to wrap inserted blocks for idempotency + revert. */ export type CommentStyle = "line" | "hash" | "block"; diff --git a/packages/codemods/src/builtins/index.ts b/packages/codemods/src/builtins/index.ts index a6cff04..3488076 100644 --- a/packages/codemods/src/builtins/index.ts +++ b/packages/codemods/src/builtins/index.ts @@ -15,7 +15,7 @@ * doesn't fit an existing generic codemod, design a new generic codemod * with the right argument surface and add it to the catalog. * - * The catalog lives in `@stanza/codemods/builtins` (this file) so it sits + * The catalog lives in `@withstanza/codemods/builtins` (this file) so it sits * next to the helper primitives it uses; consumers import via the package's * subpath export. */ diff --git a/packages/codemods/src/builtins/re-export.test.ts b/packages/codemods/src/builtins/re-export.test.ts index d03179c..6074f32 100644 --- a/packages/codemods/src/builtins/re-export.test.ts +++ b/packages/codemods/src/builtins/re-export.test.ts @@ -1,4 +1,4 @@ -import { emptyManifest } from "@stanza/registry"; +import { emptyManifest } from "@withstanza/schema"; import { describe, expect, it } from "vite-plus/test"; import { openProject, type CodemodContext, type Project } from "../index"; diff --git a/packages/codemods/src/builtins/re-export.ts b/packages/codemods/src/builtins/re-export.ts index e72e4b4..3772601 100644 --- a/packages/codemods/src/builtins/re-export.ts +++ b/packages/codemods/src/builtins/re-export.ts @@ -1,6 +1,6 @@ import path from "node:path"; -import { assertSafeRelativePath } from "@stanza/registry"; +import { assertSafeRelativePath } from "@withstanza/utils"; import { type Codemod, diff --git a/packages/codemods/src/builtins/replace-import.test.ts b/packages/codemods/src/builtins/replace-import.test.ts index 94448a5..99cc524 100644 --- a/packages/codemods/src/builtins/replace-import.test.ts +++ b/packages/codemods/src/builtins/replace-import.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { emptyManifest } from "@stanza/registry"; +import { emptyManifest } from "@withstanza/schema"; import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; import { type CodemodContext, openProject } from "../index"; diff --git a/packages/codemods/src/builtins/replace-import.ts b/packages/codemods/src/builtins/replace-import.ts index effe1e6..17f0a6f 100644 --- a/packages/codemods/src/builtins/replace-import.ts +++ b/packages/codemods/src/builtins/replace-import.ts @@ -1,6 +1,6 @@ import path from "node:path"; -import { assertSafeRelativePath } from "@stanza/registry"; +import { assertSafeRelativePath } from "@withstanza/utils"; import { type Codemod, type ImportDeclaration } from "../index"; diff --git a/packages/codemods/src/builtins/set-html-attributes.test.ts b/packages/codemods/src/builtins/set-html-attributes.test.ts index 007f95a..8278833 100644 --- a/packages/codemods/src/builtins/set-html-attributes.test.ts +++ b/packages/codemods/src/builtins/set-html-attributes.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { emptyManifest } from "@stanza/registry"; +import { emptyManifest } from "@withstanza/schema"; import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; import { type CodemodContext, openProject } from "../index"; diff --git a/packages/codemods/src/builtins/set-html-attributes.ts b/packages/codemods/src/builtins/set-html-attributes.ts index ec38f00..cf6ff32 100644 --- a/packages/codemods/src/builtins/set-html-attributes.ts +++ b/packages/codemods/src/builtins/set-html-attributes.ts @@ -1,6 +1,6 @@ import path from "node:path"; -import { assertSafeRelativePath } from "@stanza/registry"; +import { assertSafeRelativePath } from "@withstanza/utils"; import { type Codemod, diff --git a/packages/codemods/src/builtins/set-tsconfig-paths.test.ts b/packages/codemods/src/builtins/set-tsconfig-paths.test.ts index 0f54dfe..afdcbb8 100644 --- a/packages/codemods/src/builtins/set-tsconfig-paths.test.ts +++ b/packages/codemods/src/builtins/set-tsconfig-paths.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { emptyManifest } from "@stanza/registry"; +import { emptyManifest } from "@withstanza/schema"; import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; import { type CodemodContext, openProject } from "../index"; diff --git a/packages/codemods/src/builtins/set-tsconfig-paths.ts b/packages/codemods/src/builtins/set-tsconfig-paths.ts index fa2e7b5..b10622f 100644 --- a/packages/codemods/src/builtins/set-tsconfig-paths.ts +++ b/packages/codemods/src/builtins/set-tsconfig-paths.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; -import { assertSafeRelativePath } from "@stanza/registry"; +import { assertSafeRelativePath } from "@withstanza/utils"; import { type Codemod, diff --git a/packages/codemods/src/builtins/wrap-root-layout.test.ts b/packages/codemods/src/builtins/wrap-root-layout.test.ts index f8d93ea..c1f542f 100644 --- a/packages/codemods/src/builtins/wrap-root-layout.test.ts +++ b/packages/codemods/src/builtins/wrap-root-layout.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { emptyManifest, type StanzaManifest } from "@stanza/registry"; +import { emptyManifest, type StanzaManifest } from "@withstanza/schema"; import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; import { type CodemodContext, openProject } from "../index"; diff --git a/packages/codemods/src/builtins/wrap-root-layout.ts b/packages/codemods/src/builtins/wrap-root-layout.ts index 162d804..1c90daf 100644 --- a/packages/codemods/src/builtins/wrap-root-layout.ts +++ b/packages/codemods/src/builtins/wrap-root-layout.ts @@ -1,6 +1,6 @@ import path from "node:path"; -import { selectedOne } from "@stanza/registry"; +import { selectedOne } from "@withstanza/schema"; import { addDefaultImport, diff --git a/packages/codemods/src/env.ts b/packages/codemods/src/env.ts index 400327e..5dff7e8 100644 --- a/packages/codemods/src/env.ts +++ b/packages/codemods/src/env.ts @@ -1,12 +1,12 @@ import fs from "node:fs"; -import { appendEnvVar } from "@stanza/registry"; +import { appendEnvVar } from "@withstanza/utils"; /** * Idempotently append an env var to a .env.example-style file. Preserves * existing entries; updates the example value in-place if the var already * exists; adds a leading comment if `description` is supplied. Formatting is - * delegated to `appendEnvVar` (pure, in `@stanza/registry`) so the CLI and the + * delegated to `appendEnvVar` (pure, in `@withstanza/utils`) so the CLI and the * web builder's preview produce identical files. */ export function addEnvVar( diff --git a/packages/codemods/src/types.ts b/packages/codemods/src/types.ts index b342286..c1187d5 100644 --- a/packages/codemods/src/types.ts +++ b/packages/codemods/src/types.ts @@ -1,4 +1,4 @@ -import type { AppSpec, CategoryId, ModuleId, StanzaManifest } from "@stanza/registry"; +import type { AppSpec, CategoryId, ModuleId, StanzaManifest } from "@withstanza/schema"; import type { Project } from "ts-morph"; export type CodemodContext = { diff --git a/packages/registry/package.json b/packages/registry/package.json index 89c2ad9..7adc2ba 100644 --- a/packages/registry/package.json +++ b/packages/registry/package.json @@ -1,5 +1,5 @@ { - "name": "@stanza/registry", + "name": "@withstanza/registry", "version": "0.0.0", "private": true, "license": "MIT", @@ -8,19 +8,16 @@ "types": "./src/index.ts", "exports": { ".": "./src/index.ts", - "./build": "./src/build.ts", - "./manifest": "./src/manifest.ts", - "./module": "./src/module.ts", "./resolver": "./src/resolver.ts" }, "dependencies": { + "@withstanza/schema": "workspace:*", + "@withstanza/utils": "workspace:*", "handlebars": "^4.7.9", - "validate-npm-package-name": "^8.0.0", - "zod": "^4.4.3" + "validate-npm-package-name": "^8.0.0" }, "devDependencies": { "@types/validate-npm-package-name": "^4.0.2", - "svgo": "^4.0.1", "typescript": "^6.0.3", "vite-plus": "catalog:", "vitest": "catalog:" diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index 8a5a269..53717f2 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -1,78 +1,7 @@ -export type { - AppKind, - Category, - CategoryId, - Cardinality, - InstallHome, - ModuleId, - PeerRequirement, - Module, - ModuleAdapter, - ModuleMetadata, - RegistryIndex, - TemplateRef, - EnvVar, - Logo, - CodemodInvocation, - JsonValue, -} from "./module"; -export { - APP_KINDS, - defineModule, - CATEGORIES, - KNOWN_CATEGORIES, - isCategoryId, - PEER_CATEGORIES, - PACKAGE_DIRS, - categoryLabel, - categoryDescription, - categoryHome, - categoryCardinality, - isMulti, - ModuleSchema, - ModuleMetadataSchema, - RegistryIndexSchema, -} from "./module"; - -export type { - AppSpec, - StanzaManifest, - StanzaModuleRecord, - RegionMap, - RegionOwnership, -} from "./manifest"; -export { - StanzaManifestSchema, - CURRENT_MANIFEST_VERSION, - SUPPORTED_MANIFEST_VERSIONS, - MANIFEST_SCHEMA_URL, - declaredEnvNames, - defaultWebApp, - emptyManifest, - getApp, - appsForRecord, - selectedOne, - selectedAll, - manifestJsonSchema, -} from "./manifest"; - -export type { RegistryConfig } from "./registry-config"; -export { - DEFAULT_NAMESPACE, - expandEnv, - isLikelyNamespaceTypo, - isNamespace, - isValidModuleId, - parseModuleSpec, - RegistriesSchema, - RegistryConfigSchema, -} from "./registry-config"; - export type { ResolveContext, ResolveResult, ResolveError } from "./resolver"; export { resolveAdapter, isCompatible, categoryOrder, activePeerIds } from "./resolver"; export type { - PackageManager, PackageJson, MergedInstallFields, ResolvedEntry, @@ -91,7 +20,6 @@ export { export { ENV_EXAMPLE_HEADER, - appendEnvVar, synthesizeEnvExample, synthesizeManifest, synthesizeReadme, @@ -103,5 +31,3 @@ export { renderTemplate, buildRenderContext, pmRun, pmRecursive } from "./templa export type { ProjectNameValidation } from "./project-name"; export { validateProjectName } from "./project-name"; - -export { safeRelativePath, assertSafeRelativePath } from "./safe-path"; diff --git a/packages/registry/src/package-json.test.ts b/packages/registry/src/package-json.test.ts index e4de4f9..0cdc24d 100644 --- a/packages/registry/src/package-json.test.ts +++ b/packages/registry/src/package-json.test.ts @@ -1,6 +1,6 @@ +import { defineModule, type Module, ModuleSchema } from "@withstanza/schema"; import { describe, expect, it } from "vite-plus/test"; -import { defineModule, type Module, ModuleSchema } from "./module"; import { mergeInstallFields, type Resolved, diff --git a/packages/registry/src/package-json.ts b/packages/registry/src/package-json.ts index fda3187..f9f8a9e 100644 --- a/packages/registry/src/package-json.ts +++ b/packages/registry/src/package-json.ts @@ -1,18 +1,19 @@ -import { type AppSpec, defaultWebApp } from "./manifest"; import { + type AppSpec, categoryHome, type CategoryId, + defaultWebApp, type EnvVar, type InstallHome, type Module, type ModuleAdapter, PACKAGE_DIRS, -} from "./module"; + type PackageManager, +} from "@withstanza/schema"; + import { categoryOrder } from "./resolver"; import { pmRecursive } from "./template"; -export type PackageManager = "pnpm" | "bun" | "npm"; - /** Minimal package.json shape we author. Field order here is the emit order. */ export type PackageJson = { name?: string; diff --git a/packages/registry/src/resolver.test.ts b/packages/registry/src/resolver.test.ts index 7f7f562..a46a3a6 100644 --- a/packages/registry/src/resolver.test.ts +++ b/packages/registry/src/resolver.test.ts @@ -1,7 +1,6 @@ +import { defineModule, emptyManifest, type Module } from "@withstanza/schema"; import { assert, describe, expect, it } from "vite-plus/test"; -import { emptyManifest } from "./manifest"; -import { defineModule, type Module } from "./module"; import { activePeerIds, resolveAdapter } from "./resolver"; const drizzle: Module = defineModule({ diff --git a/packages/registry/src/resolver.ts b/packages/registry/src/resolver.ts index 17e4267..a01d950 100644 --- a/packages/registry/src/resolver.ts +++ b/packages/registry/src/resolver.ts @@ -1,11 +1,12 @@ -import { type StanzaManifest, selectedOne } from "./manifest"; import { type CategoryId, KNOWN_CATEGORIES, type ModuleAdapter, PEER_CATEGORIES, type PeerRequirement, -} from "./module"; + selectedOne, + type StanzaManifest, +} from "@withstanza/schema"; /** * Topological order categories are processed in (derived from `CATEGORIES`): diff --git a/packages/registry/src/synthesize.test.ts b/packages/registry/src/synthesize.test.ts index 575ee6b..fd738d5 100644 --- a/packages/registry/src/synthesize.test.ts +++ b/packages/registry/src/synthesize.test.ts @@ -1,6 +1,6 @@ +import { defineModule, type Module } from "@withstanza/schema"; import { describe, expect, it } from "vite-plus/test"; -import { defineModule, type Module } from "./module"; import type { Resolved } from "./package-json"; import { ENV_EXAMPLE_HEADER, synthesizeEnvExample, synthesizeReadme } from "./synthesize"; diff --git a/packages/registry/src/synthesize.ts b/packages/registry/src/synthesize.ts index 501e71d..6b684a8 100644 --- a/packages/registry/src/synthesize.ts +++ b/packages/registry/src/synthesize.ts @@ -1,22 +1,22 @@ import { type AppSpec, - defaultWebApp, - emptyManifest, - type StanzaManifest, - type StanzaModuleRecord, -} from "./manifest"; -import { categoryHome, type CategoryId, categoryLabel, + defaultWebApp, + emptyManifest, type InstallHome, isMulti, type ModuleId, + type PackageManager, + type StanzaManifest, + type StanzaModuleRecord, type TemplateRef, -} from "./module"; +} from "@withstanza/schema"; +import { appendEnvVar } from "@withstanza/utils"; + import { mergeInstallFields, - type PackageManager, type Resolved, type ResolvedEntry, type SynthesizeEntry, @@ -27,39 +27,6 @@ import { buildRenderContext, pmRun, renderTemplate } from "./template"; /** Header `stanza init` writes at the top of `.env.example`. */ export const ENV_EXAMPLE_HEADER = "# Stanza-managed environment variables.\n"; -/** - * Idempotently append an env var to `.env.example`-style text, returning the - * new contents. Pure (no fs) so it backs both the CLI's `addEnvVar` and the - * web builder's preview synthesis — the single source of truth for env-file - * formatting. Updates an existing var in place; otherwise appends with a blank - * line separator and an optional leading `# description` comment. - */ -export function appendEnvVar( - contents: string, - name: string, - example: string, - description?: string, -): string { - const lines = contents.split("\n"); - const existingIdx = lines.findIndex((line) => line.replace(/^#\s*/, "").startsWith(`${name}=`)); - const entry = description ? `# ${description}\n${name}=${example}` : `${name}=${example}`; - - if (existingIdx >= 0) { - const prev = lines[existingIdx - 1]; - if (description && prev?.startsWith("#")) { - lines.splice(existingIdx - 1, 2, ...entry.split("\n")); - } else { - lines.splice(existingIdx, 1, ...entry.split("\n")); - } - } else { - if (contents.length > 0 && !contents.endsWith("\n")) lines.push(""); - if (lines.length > 0 && lines[lines.length - 1] !== "") lines.push(""); - lines.push(...entry.split("\n")); - } - - return lines.join("\n").replace(/\n+$/, "\n"); -} - /** * Compute the `.env.example` stanza would write for a resolved selection: * the managed header followed by every module's env vars, in `categoryOrder`. diff --git a/packages/registry/src/template.test.ts b/packages/registry/src/template.test.ts index fa24001..ebee75c 100644 --- a/packages/registry/src/template.test.ts +++ b/packages/registry/src/template.test.ts @@ -1,8 +1,6 @@ +import { type AppSpec, defaultWebApp, defineModule, type Module } from "@withstanza/schema"; import { describe, expect, it } from "vite-plus/test"; -import { defaultWebApp } from "./manifest"; -import type { AppSpec } from "./manifest"; -import { defineModule, type Module } from "./module"; import type { Resolved } from "./package-json"; import { synthesizeTemplates } from "./synthesize"; import { buildRenderContext, renderTemplate } from "./template"; diff --git a/packages/registry/src/template.ts b/packages/registry/src/template.ts index cc4bdd5..4f6a6ce 100644 --- a/packages/registry/src/template.ts +++ b/packages/registry/src/template.ts @@ -1,9 +1,12 @@ +import { + type AppSpec, + type CategoryId, + PACKAGE_DIRS, + type PackageManager, + PEER_CATEGORIES, +} from "@withstanza/schema"; import Handlebars from "handlebars"; -import type { AppSpec } from "./manifest"; -import { type CategoryId, PACKAGE_DIRS, PEER_CATEGORIES } from "./module"; -import type { PackageManager } from "./package-json"; - /** * Handlebars context consumed by {@link renderTemplate}. The shape is the * public contract template authors write against: diff --git a/packages/schema/README.md b/packages/schema/README.md new file mode 100644 index 0000000..da117fd --- /dev/null +++ b/packages/schema/README.md @@ -0,0 +1,48 @@ +# @withstanza/schema + +The contract types and [Zod](https://zod.dev) schemas behind [Stanza](https://stanza.tools) — the shape of a `stanza.json` manifest, a registry module, and the registry index, in one place. + +`StanzaManifestSchema` is the single source of truth for the JSON Schema published at **** (the value a manifest's `$schema` points at), so editors, the CLI, and third-party registry authors all validate against the same definitions. + +> [!WARNING] +> **Major work in progress.** The schema is pre-1.0 and still evolving — see the [registry roadmap](https://stanza.tools/docs/registry). + +## Install + +```sh +npm install @withstanza/schema +``` + +## What's in it + +- **`defineModule(...)`** — identity helper for authoring a registry module's `module.ts`; validates illegal field combinations at runtime. +- **`ModuleSchema` / `ModuleMetadataSchema` / `RegistryIndexSchema`** — runtime validation for fetched registry JSON. +- **`StanzaManifestSchema`** + manifest helpers (`emptyManifest`, `selectedOne`, `selectedAll`, `appsForRecord`, `declaredEnvNames`, …) and `CURRENT_MANIFEST_VERSION` / `MANIFEST_SCHEMA_URL`. +- **`CATEGORIES`** and the category helpers (`categoryHome`, `categoryLabel`, `categoryCardinality`, `isMulti`, `PEER_CATEGORIES`, `PACKAGE_DIRS`, …) — the canonical category list. +- **`RegistriesSchema` / `parseModuleSpec` / `expandEnv`** — third-party registry config and `@/` parsing. +- **`PackageManagerSchema`** and the full set of contract types (`Module`, `ModuleAdapter`, `StanzaManifest`, `Category`, `CategoryId`, `TemplateRef`, `EnvVar`, …). + +## Usage + +```ts +import { defineModule, StanzaManifestSchema, CATEGORIES } from "@withstanza/schema"; + +// Author a module (in a registry's module.ts): +export default defineModule({ + id: "cosmos", + category: "testing", + label: "Cosmos", + description: "Visual component sandbox.", + version: "1.0.0", + adapters: [{ key: "default", match: {} }], +}); + +// Validate a stanza.json you loaded yourself: +const manifest = StanzaManifestSchema.parse(JSON.parse(raw)); +``` + +Most consumers reach this through [`stanza-cli`](https://www.npmjs.com/package/stanza-cli) rather than directly. Full guides — including [module authoring](https://stanza.tools/docs/authoring) — live at **[stanza.tools/docs](https://stanza.tools/docs)**. + +## License + +[MIT](https://github.com/jakejarvis/stanza/blob/main/LICENSE) diff --git a/packages/schema/package.json b/packages/schema/package.json new file mode 100644 index 0000000..d186b06 --- /dev/null +++ b/packages/schema/package.json @@ -0,0 +1,52 @@ +{ + "name": "@withstanza/schema", + "version": "0.0.0", + "description": "JSON Schema and contract types for the Stanza manifest (stanza.json).", + "keywords": [ + "json-schema", + "monorepo", + "stanza" + ], + "homepage": "https://stanza.tools", + "bugs": "https://github.com/jakejarvis/stanza/issues", + "license": "MIT", + "author": "Jake Jarvis ", + "repository": { + "type": "git", + "url": "git+https://github.com/jakejarvis/stanza.git", + "directory": "packages/schema" + }, + "files": [ + "dist" + ], + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "publishConfig": { + "main": "./dist/index.mjs", + "types": "./dist/index.d.mts", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + } + }, + "access": "public" + }, + "scripts": { + "build": "vp pack" + }, + "dependencies": { + "zod": "^4.4.3" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "@withstanza/utils": "workspace:*", + "typescript": "^6.0.3", + "vite-plus": "catalog:", + "vitest": "catalog:" + } +} diff --git a/packages/schema/src/category.ts b/packages/schema/src/category.ts new file mode 100644 index 0000000..9d79a50 --- /dev/null +++ b/packages/schema/src/category.ts @@ -0,0 +1,200 @@ +/** + * Kind of app a framework module targets. The closed enum lets the runtime + * validate "you can't install Next.js into a `kind: \"native\"` app" — add new + * kinds here as they're introduced (and the schema picks them up). + */ +export const APP_KINDS = ["web", "native"] as const; +export type AppKind = (typeof APP_KINDS)[number]; + +/** Where a category's install fields and `scope`-derived templates land. */ +export type InstallHome = + | { kind: "app" } // an app entry in manifest.apps[] (selected per install) + | { kind: "repo" } // monorepo root + | { kind: "package"; dir: string }; // packages//, named @/ + +/** How many modules a category holds: a single choice, or several coexisting. */ +export type Cardinality = "one" | "many"; + +export type Category = { + id: CategoryId; + label: string; + description: string; + /** `"one"` → single-choice (framework, auth); `"many"` → coexist (testing). */ + cardinality: Cardinality; + /** Install destination for this category's modules. */ + home: InstallHome; +}; + +/** + * Single source of truth for categories — `CategoryId` and `KNOWN_CATEGORIES` + * derive from this, so adding one is a one-place edit. Order is topological: + * a category appears after every category it can peer on, and `many` (leaf) + * categories come last. It's also the wizard prompt + processing order. + * + * Constraint-bearing is emergent: a category is a peer candidate iff some + * module declares `peers`/`match` against it. The resolver only treats + * `cardinality: "one"` categories as peers (`PEER_CATEGORIES`). + */ +export const CATEGORIES = [ + { + id: "framework", + label: "Framework", + description: "Web and native app frameworks.", + cardinality: "one", + home: { kind: "app" }, + }, + { + id: "ui", + label: "UI", + description: "Styling systems and component primitives.", + cardinality: "one", + home: { kind: "package", dir: "ui" }, + }, + { + id: "api", + label: "API", + description: "Typed RPC layer between the framework and your services.", + cardinality: "one", + home: { kind: "package", dir: "api" }, + }, + { + id: "db", + label: "Database", + description: "Database engines.", + cardinality: "one", + home: { kind: "package", dir: "db" }, + }, + { + id: "orm", + label: "ORM", + description: "Typed query layers over your database.", + cardinality: "one", + home: { kind: "package", dir: "db" }, + }, + { + id: "auth", + label: "Auth", + description: "Authentication providers and session handling.", + cardinality: "one", + home: { kind: "package", dir: "auth" }, + }, + { + id: "payments", + label: "Payments", + description: "Checkout, customer portal, and webhooks.", + cardinality: "one", + home: { kind: "package", dir: "payments" }, + }, + { + id: "email", + label: "Email", + description: "Transactional email providers and templates.", + cardinality: "one", + home: { kind: "package", dir: "email" }, + }, + { + id: "ai", + label: "AI", + description: "AI SDK and provider wiring.", + cardinality: "one", + home: { kind: "package", dir: "ai" }, + }, + { + id: "tooling", + label: "Tooling", + description: "Linter and formatter toolchains.", + cardinality: "one", + home: { kind: "repo" }, + }, + { + id: "testing", + label: "Testing", + description: "Test runners — unit and end-to-end.", + cardinality: "many", + home: { kind: "app" }, + }, + { + id: "deploy", + label: "Deploy", + description: "Deploy targets.", + cardinality: "many", + home: { kind: "repo" }, + }, + { + id: "monorepo", + label: "Monorepo", + description: "Workspace task orchestrators.", + cardinality: "one", + home: { kind: "repo" }, + }, + // Inline shape (not `Category`) so `CategoryId` derives without a cycle. +] as const satisfies readonly { + id: string; + label: string; + description: string; + cardinality: Cardinality; + home: InstallHome; +}[]; + +/** Legal category ids, derived from `CATEGORIES`. */ +export type CategoryId = (typeof CATEGORIES)[number]["id"]; + +/** Category ids as a non-empty tuple — the shape `z.enum` needs. Also the processing order. */ +const [firstCategory, ...restCategories] = CATEGORIES; +export const KNOWN_CATEGORIES: [CategoryId, ...CategoryId[]] = [ + firstCategory.id, + ...restCategories.map((c) => c.id), +]; + +/** Runtime guard narrowing an arbitrary string to a known `CategoryId`. */ +export function isCategoryId(value: string): value is CategoryId { + return KNOWN_CATEGORIES.some((id) => id === value); +} + +// `Object.fromEntries` widens keys to `string`; CATEGORIES covers every +// CategoryId by construction, so narrowing to the exhaustive record is sound. +// oxlint-disable-next-line typescript/no-unsafe-type-assertion +const CATEGORY_BY_ID = Object.fromEntries(CATEGORIES.map((c) => [c.id, c])) as Record< + CategoryId, + Category +>; + +/** Display name — used by the wizard, summary, and CLI/web list output. */ +export function categoryLabel(id: CategoryId): string { + return CATEGORY_BY_ID[id].label; +} + +/** One-line blurb — used as the category landing-page subtitle. */ +export function categoryDescription(id: CategoryId): string { + return CATEGORY_BY_ID[id].description; +} + +/** Install destination for a category's modules. */ +export function categoryHome(id: CategoryId): InstallHome { + return CATEGORY_BY_ID[id].home; +} + +export function categoryCardinality(id: CategoryId): Cardinality { + return CATEGORY_BY_ID[id].cardinality; +} + +/** True for multi-choice categories (testing, deploy, …). */ +export function isMulti(id: CategoryId): boolean { + return CATEGORY_BY_ID[id].cardinality === "many"; +} + +/** + * Categories that can be peers — only `cardinality: "one"` ones, since you + * dispatch on "the framework", not on a set. The resolver iterates these. + */ +export const PEER_CATEGORIES = CATEGORIES.filter((c) => c.cardinality === "one").map( + (c) => c.id, +) as CategoryId[]; + +/** Unique package dirs across all categories (for cross-package wiring + sweep). */ +export const PACKAGE_DIRS: Set = new Set( + CATEGORIES.flatMap((c) => (c.home.kind === "package" ? [c.home.dir] : [])), +); + +/** A module id — interned into manifests and registry URLs. */ +export type ModuleId = string; diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts new file mode 100644 index 0000000..f1104d4 --- /dev/null +++ b/packages/schema/src/index.ts @@ -0,0 +1,64 @@ +export type { AppKind, Cardinality, Category, CategoryId, InstallHome, ModuleId } from "./category"; +export { + APP_KINDS, + CATEGORIES, + categoryCardinality, + categoryDescription, + categoryHome, + categoryLabel, + isCategoryId, + isMulti, + KNOWN_CATEGORIES, + PACKAGE_DIRS, + PEER_CATEGORIES, +} from "./category"; + +export type { + AppSpec, + RegionMap, + RegionOwnership, + StanzaManifest, + StanzaModuleRecord, +} from "./manifest"; +export { + appsForRecord, + CURRENT_MANIFEST_VERSION, + declaredEnvNames, + defaultWebApp, + emptyManifest, + getApp, + MANIFEST_SCHEMA_URL, + selectedAll, + selectedOne, + StanzaManifestSchema, + SUPPORTED_MANIFEST_VERSIONS, +} from "./manifest"; + +export type { PackageManager } from "./package-manager"; +export { DEFAULT_PACKAGE_MANAGER, isPackageManager, PackageManagerSchema } from "./package-manager"; + +export type { RegistryConfig } from "./registry-config"; +export { + DEFAULT_NAMESPACE, + expandEnv, + isLikelyNamespaceTypo, + isNamespace, + isValidModuleId, + parseModuleSpec, + RegistriesSchema, + RegistryConfigSchema, +} from "./registry-config"; + +export type { + CodemodInvocation, + EnvVar, + JsonValue, + Logo, + Module, + ModuleAdapter, + ModuleMetadata, + PeerRequirement, + RegistryIndex, + TemplateRef, +} from "./module"; +export { defineModule, ModuleMetadataSchema, ModuleSchema, RegistryIndexSchema } from "./module"; diff --git a/packages/registry/src/manifest.test.ts b/packages/schema/src/manifest.test.ts similarity index 100% rename from packages/registry/src/manifest.test.ts rename to packages/schema/src/manifest.test.ts diff --git a/packages/registry/src/manifest.ts b/packages/schema/src/manifest.ts similarity index 94% rename from packages/registry/src/manifest.ts rename to packages/schema/src/manifest.ts index 327fc45..b458351 100644 --- a/packages/registry/src/manifest.ts +++ b/packages/schema/src/manifest.ts @@ -8,7 +8,8 @@ import { type CategoryId, KNOWN_CATEGORIES, type ModuleId, -} from "./module"; +} from "./category"; +import { type PackageManager, PackageManagerSchema } from "./package-manager"; import { type RegistryConfig, RegistriesSchema } from "./registry-config"; export const CURRENT_MANIFEST_VERSION = "0.4" as const; @@ -97,7 +98,7 @@ export type StanzaManifest = { */ version: (typeof SUPPORTED_MANIFEST_VERSIONS)[number]; projectShape: "monorepo"; - packageManager: "pnpm" | "bun" | "npm"; + packageManager: PackageManager; /** Display name; usually the repo root name. */ name: string; /** @@ -144,7 +145,7 @@ export const StanzaManifestSchema = z // CLI re-stamps to CURRENT_MANIFEST_VERSION on the next write. version: z.enum(SUPPORTED_MANIFEST_VERSIONS), projectShape: z.literal("monorepo"), - packageManager: z.enum(["pnpm", "bun", "npm"]), + packageManager: PackageManagerSchema, name: z.string(), apps: z.array(appSpecSchema).min(1), // Zod 4: partialRecord because not every category is filled. Every category @@ -219,7 +220,7 @@ export function defaultWebApp(): AppSpec { export function emptyManifest(input: { name: string; apps?: AppSpec[]; - packageManager?: StanzaManifest["packageManager"]; + packageManager?: PackageManager; }): StanzaManifest { return { $schema: MANIFEST_SCHEMA_URL, @@ -297,17 +298,3 @@ export function declaredEnvNames(manifest: StanzaManifest): string[] { if (!envRegions) return []; return Object.keys(envRegions).toSorted(); } - -/** - * JSON Schema for `stanza.json`, derived from the single Zod source of truth. - * Published at {@link MANIFEST_SCHEMA_URL} so editors can validate and - * autocomplete the manifest. - */ -export function manifestJsonSchema(): Record { - return { - $id: MANIFEST_SCHEMA_URL, - title: "Stanza manifest", - description: "Schema for stanza.json — a Stanza monorepo manifest.", - ...z.toJSONSchema(StanzaManifestSchema), - }; -} diff --git a/packages/registry/src/module.ts b/packages/schema/src/module.ts similarity index 66% rename from packages/registry/src/module.ts rename to packages/schema/src/module.ts index 137f556..f655a3d 100644 --- a/packages/registry/src/module.ts +++ b/packages/schema/src/module.ts @@ -1,206 +1,15 @@ +import { safeRelativePath } from "@withstanza/utils"; import { z } from "zod"; -import { safeRelativePath } from "./safe-path"; - -/** - * Kind of app a framework module targets. The closed enum lets the runtime - * validate "you can't install Next.js into a `kind: \"native\"` app" — add new - * kinds here as they're introduced (and the schema picks them up). - */ -export const APP_KINDS = ["web", "native"] as const; -export type AppKind = (typeof APP_KINDS)[number]; - -/** Where a category's install fields and `scope`-derived templates land. */ -export type InstallHome = - | { kind: "app" } // an app entry in manifest.apps[] (selected per install) - | { kind: "repo" } // monorepo root - | { kind: "package"; dir: string }; // packages//, named @/ - -/** How many modules a category holds: a single choice, or several coexisting. */ -export type Cardinality = "one" | "many"; - -export type Category = { - id: CategoryId; - label: string; - description: string; - /** `"one"` → single-choice (framework, auth); `"many"` → coexist (testing). */ - cardinality: Cardinality; - /** Install destination for this category's modules. */ - home: InstallHome; -}; - -/** - * Single source of truth for categories — `CategoryId` and `KNOWN_CATEGORIES` - * derive from this, so adding one is a one-place edit. Order is topological: - * a category appears after every category it can peer on, and `many` (leaf) - * categories come last. It's also the wizard prompt + processing order. - * - * Constraint-bearing is emergent: a category is a peer candidate iff some - * module declares `peers`/`match` against it. The resolver only treats - * `cardinality: "one"` categories as peers (`PEER_CATEGORIES`). - */ -export const CATEGORIES = [ - { - id: "framework", - label: "Framework", - description: "Web and native app frameworks.", - cardinality: "one", - home: { kind: "app" }, - }, - { - id: "ui", - label: "UI", - description: "Styling systems and component primitives.", - cardinality: "one", - home: { kind: "package", dir: "ui" }, - }, - { - id: "api", - label: "API", - description: "Typed RPC layer between the framework and your services.", - cardinality: "one", - home: { kind: "package", dir: "api" }, - }, - { - id: "db", - label: "Database", - description: "Database engines.", - cardinality: "one", - home: { kind: "package", dir: "db" }, - }, - { - id: "orm", - label: "ORM", - description: "Typed query layers over your database.", - cardinality: "one", - home: { kind: "package", dir: "db" }, - }, - { - id: "auth", - label: "Auth", - description: "Authentication providers and session handling.", - cardinality: "one", - home: { kind: "package", dir: "auth" }, - }, - { - id: "payments", - label: "Payments", - description: "Checkout, customer portal, and webhooks.", - cardinality: "one", - home: { kind: "package", dir: "payments" }, - }, - { - id: "email", - label: "Email", - description: "Transactional email providers and templates.", - cardinality: "one", - home: { kind: "package", dir: "email" }, - }, - { - id: "ai", - label: "AI", - description: "AI SDK and provider wiring.", - cardinality: "one", - home: { kind: "package", dir: "ai" }, - }, - { - id: "tooling", - label: "Tooling", - description: "Linter and formatter toolchains.", - cardinality: "one", - home: { kind: "repo" }, - }, - { - id: "testing", - label: "Testing", - description: "Test runners — unit and end-to-end.", - cardinality: "many", - home: { kind: "app" }, - }, - { - id: "deploy", - label: "Deploy", - description: "Deploy targets.", - cardinality: "many", - home: { kind: "repo" }, - }, - { - id: "monorepo", - label: "Monorepo", - description: "Workspace task orchestrators.", - cardinality: "one", - home: { kind: "repo" }, - }, - // Inline shape (not `Category`) so `CategoryId` derives without a cycle. -] as const satisfies readonly { - id: string; - label: string; - description: string; - cardinality: Cardinality; - home: InstallHome; -}[]; - -/** Legal category ids, derived from `CATEGORIES`. */ -export type CategoryId = (typeof CATEGORIES)[number]["id"]; - -/** Category ids as a non-empty tuple — the shape `z.enum` needs. Also the processing order. */ -const [firstCategory, ...restCategories] = CATEGORIES; -export const KNOWN_CATEGORIES: [CategoryId, ...CategoryId[]] = [ - firstCategory.id, - ...restCategories.map((c) => c.id), -]; - -/** Runtime guard narrowing an arbitrary string to a known `CategoryId`. */ -export function isCategoryId(value: string): value is CategoryId { - return KNOWN_CATEGORIES.some((id) => id === value); -} - -// `Object.fromEntries` widens keys to `string`; CATEGORIES covers every -// CategoryId by construction, so narrowing to the exhaustive record is sound. -// oxlint-disable-next-line typescript/no-unsafe-type-assertion -const CATEGORY_BY_ID = Object.fromEntries(CATEGORIES.map((c) => [c.id, c])) as Record< - CategoryId, - Category ->; - -/** Display name — used by the wizard, summary, and CLI/web list output. */ -export function categoryLabel(id: CategoryId): string { - return CATEGORY_BY_ID[id].label; -} - -/** One-line blurb — used as the category landing-page subtitle. */ -export function categoryDescription(id: CategoryId): string { - return CATEGORY_BY_ID[id].description; -} - -/** Install destination for a category's modules. */ -export function categoryHome(id: CategoryId): InstallHome { - return CATEGORY_BY_ID[id].home; -} - -export function categoryCardinality(id: CategoryId): Cardinality { - return CATEGORY_BY_ID[id].cardinality; -} - -/** True for multi-choice categories (testing, deploy, …). */ -export function isMulti(id: CategoryId): boolean { - return CATEGORY_BY_ID[id].cardinality === "many"; -} - -/** - * Categories that can be peers — only `cardinality: "one"` ones, since you - * dispatch on "the framework", not on a set. The resolver iterates these. - */ -export const PEER_CATEGORIES = CATEGORIES.filter((c) => c.cardinality === "one").map( - (c) => c.id, -) as CategoryId[]; - -/** Unique package dirs across all categories (for cross-package wiring + sweep). */ -export const PACKAGE_DIRS: Set = new Set( - CATEGORIES.flatMap((c) => (c.home.kind === "package" ? [c.home.dir] : [])), -); - -export type ModuleId = string; +import { + APP_KINDS, + type AppKind, + type Category, + type CategoryId, + categoryHome, + KNOWN_CATEGORIES, + type ModuleId, +} from "./category"; export type PeerRequirement = { [K in CategoryId]?: ModuleId[] | "any"; diff --git a/packages/schema/src/package-manager.ts b/packages/schema/src/package-manager.ts new file mode 100644 index 0000000..3848d1a --- /dev/null +++ b/packages/schema/src/package-manager.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +/** + * Package managers Stanza scaffolds for. Persisted in `stanza.json` + * (`packageManager`) and surfaced to templates as `{{pm}}`; pnpm is the + * default. Closed enum so a manifest naming an unknown pm fails validation. + */ +export const PackageManagerSchema = z.enum(["pnpm", "bun", "npm"]); +export type PackageManager = z.infer; + +/** Default when the caller doesn't pick one. */ +export const DEFAULT_PACKAGE_MANAGER: PackageManager = "pnpm"; + +/** Narrowing guard for untrusted input (URL search params, env vars). */ +export function isPackageManager(value: unknown): value is PackageManager { + return PackageManagerSchema.safeParse(value).success; +} diff --git a/packages/registry/src/registry-config.test.ts b/packages/schema/src/registry-config.test.ts similarity index 100% rename from packages/registry/src/registry-config.test.ts rename to packages/schema/src/registry-config.test.ts diff --git a/packages/registry/src/registry-config.ts b/packages/schema/src/registry-config.ts similarity index 100% rename from packages/registry/src/registry-config.ts rename to packages/schema/src/registry-config.ts diff --git a/packages/schema/tsconfig.json b/packages/schema/tsconfig.json new file mode 100644 index 0000000..9e1aad4 --- /dev/null +++ b/packages/schema/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 0000000..e2ad511 --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,18 @@ +{ + "name": "@withstanza/utils", + "version": "0.0.0", + "private": true, + "license": "MIT", + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "typescript": "^6.0.3", + "vite-plus": "catalog:", + "vitest": "catalog:" + } +} diff --git a/packages/utils/src/env.ts b/packages/utils/src/env.ts new file mode 100644 index 0000000..a8009e6 --- /dev/null +++ b/packages/utils/src/env.ts @@ -0,0 +1,32 @@ +/** + * Idempotently append an env var to `.env.example`-style text, returning the + * new contents. Pure (no fs) so it backs both the CLI's `addEnvVar` and the + * web builder's preview synthesis — the single source of truth for env-file + * formatting. Updates an existing var in place; otherwise appends with a blank + * line separator and an optional leading `# description` comment. + */ +export function appendEnvVar( + contents: string, + name: string, + example: string, + description?: string, +): string { + const lines = contents.split("\n"); + const existingIdx = lines.findIndex((line) => line.replace(/^#\s*/, "").startsWith(`${name}=`)); + const entry = description ? `# ${description}\n${name}=${example}` : `${name}=${example}`; + + if (existingIdx >= 0) { + const prev = lines[existingIdx - 1]; + if (description && prev?.startsWith("#")) { + lines.splice(existingIdx - 1, 2, ...entry.split("\n")); + } else { + lines.splice(existingIdx, 1, ...entry.split("\n")); + } + } else { + if (contents.length > 0 && !contents.endsWith("\n")) lines.push(""); + if (lines.length > 0 && lines[lines.length - 1] !== "") lines.push(""); + lines.push(...entry.split("\n")); + } + + return lines.join("\n").replace(/\n+$/, "\n"); +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts new file mode 100644 index 0000000..b4d6e5f --- /dev/null +++ b/packages/utils/src/index.ts @@ -0,0 +1,2 @@ +export { safeRelativePath, assertSafeRelativePath } from "./safe-path"; +export { appendEnvVar } from "./env"; diff --git a/packages/registry/src/safe-path.test.ts b/packages/utils/src/safe-path.test.ts similarity index 100% rename from packages/registry/src/safe-path.test.ts rename to packages/utils/src/safe-path.test.ts diff --git a/packages/registry/src/safe-path.ts b/packages/utils/src/safe-path.ts similarity index 100% rename from packages/registry/src/safe-path.ts rename to packages/utils/src/safe-path.ts diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 0000000..7918f33 --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06a55a1..2aa353b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,9 +27,15 @@ importers: '@types/node': specifier: ^25.9.1 version: 25.9.1 + '@withstanza/schema': + specifier: workspace:* + version: link:packages/schema jiti: specifier: ^2.7.0 version: 2.7.0 + svgo: + specifier: ^4.0.1 + version: 4.0.1 typescript: specifier: ^6.0.3 version: 6.0.3 @@ -64,18 +70,24 @@ importers: specifier: ^4.4.3 version: 4.4.3 devDependencies: - '@stanza/codemods': - specifier: workspace:* - version: link:../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../packages/registry '@types/node': specifier: ^25.9.1 version: 25.9.1 '@types/semver': specifier: ^7.7.1 version: 7.7.1 + '@withstanza/codemods': + specifier: workspace:* + version: link:../../packages/codemods + '@withstanza/registry': + specifier: workspace:* + version: link:../../packages/registry + '@withstanza/schema': + specifier: workspace:* + version: link:../../packages/schema + '@withstanza/utils': + specifier: workspace:* + version: link:../../packages/utils typescript: specifier: ^6.0.3 version: 6.0.3 @@ -106,9 +118,6 @@ importers: '@pierre/trees': specifier: 1.0.0-beta.4 version: 1.0.0-beta.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@stanza/registry': - specifier: workspace:* - version: link:../../packages/registry '@tabler/icons-react': specifier: ^3.44.0 version: 3.44.0(react@19.2.6) @@ -142,6 +151,12 @@ importers: '@vercel/functions': specifier: ^3.6.1 version: 3.6.1 + '@withstanza/registry': + specifier: workspace:* + version: link:../../packages/registry + '@withstanza/schema': + specifier: workspace:* + version: link:../../packages/schema class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -165,7 +180,7 @@ importers: version: 12.40.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) nitro: specifier: ^3.0.260522-beta - version: 3.0.260522-beta(@vercel/functions@3.6.1)(@voidzero-dev/vite-plus-core@0.1.23(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3))(chokidar@5.0.0)(dotenv@17.4.2)(jiti@2.7.0)(lru-cache@11.5.1) + version: 3.0.260522-beta(@vercel/blob@2.4.0)(@vercel/functions@3.6.1)(@voidzero-dev/vite-plus-core@0.1.23(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3))(chokidar@5.0.0)(dotenv@17.4.2)(jiti@2.7.0)(lru-cache@11.5.1) posthog-node: specifier: ^5.35.6 version: 5.35.6 @@ -251,9 +266,12 @@ importers: packages/codemods: dependencies: - '@stanza/registry': + '@withstanza/schema': specifier: workspace:* - version: link:../registry + version: link:../schema + '@withstanza/utils': + specifier: workspace:* + version: link:../utils jsonc-parser: specifier: ^3.3.1 version: 3.3.1 @@ -292,22 +310,59 @@ importers: packages/registry: dependencies: + '@withstanza/schema': + specifier: workspace:* + version: link:../schema + '@withstanza/utils': + specifier: workspace:* + version: link:../utils handlebars: specifier: ^4.7.9 version: 4.7.9 validate-npm-package-name: specifier: ^8.0.0 version: 8.0.0 - zod: - specifier: ^4.4.3 - version: 4.4.3 devDependencies: '@types/validate-npm-package-name': specifier: ^4.0.2 version: 4.0.2 - svgo: - specifier: ^4.0.1 - version: 4.0.1 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite-plus: + specifier: 'catalog:' + version: 0.1.23(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + vitest: + specifier: npm:@voidzero-dev/vite-plus-test@^0.1.23 + version: '@voidzero-dev/vite-plus-test@0.1.23(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))' + + packages/schema: + dependencies: + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@types/node': + specifier: ^25.9.1 + version: 25.9.1 + '@withstanza/utils': + specifier: workspace:* + version: link:../utils + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite-plus: + specifier: 'catalog:' + version: 0.1.23(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + vitest: + specifier: npm:@voidzero-dev/vite-plus-test@^0.1.23 + version: '@voidzero-dev/vite-plus-test@0.1.23(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))' + + packages/utils: + devDependencies: + '@types/node': + specifier: ^25.9.1 + version: 25.9.1 typescript: specifier: ^6.0.3 version: 6.0.3 @@ -320,39 +375,27 @@ importers: registry/modules/ai-tanstack-ai: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/ai-vercel-ai-sdk: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/auth-better-auth: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/auth-clerk: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema devDependencies: '@types/node': specifier: ^25.9.1 @@ -360,156 +403,111 @@ importers: registry/modules/db-postgres: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/db-sqlite: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/email-resend: dependencies: - '@stanza/registry': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/framework-next: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/framework-tanstack-start: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/monorepo-turbo: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/orm-drizzle: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/orm-prisma: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/payments-polar: dependencies: - '@stanza/registry': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/payments-stripe: dependencies: - '@stanza/registry': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/testing-playwright: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/testing-vitest: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/tooling-biome: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/tooling-eslint-prettier: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/tooling-oxlint-oxfmt: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/ui-shadcn-base: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/ui-shadcn-radix: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema registry/modules/ui-tailwind: dependencies: - '@stanza/codemods': + '@withstanza/schema': specifier: workspace:* - version: link:../../../packages/codemods - '@stanza/registry': - specifier: workspace:* - version: link:../../../packages/registry + version: link:../../../packages/schema packages: @@ -2468,6 +2466,10 @@ packages: vue-router: optional: true + '@vercel/blob@2.4.0': + resolution: {integrity: sha512-ncQ8CRb6XoEAYJwjOTRGpACRT6h/AeY+/33gLyeVxG5BIes27OPm1jmqreF+JHjcTmGhClTP+kBpmyLfbV0xew==} + engines: {node: '>=20.0.0'} + '@vercel/functions@3.6.1': resolution: {integrity: sha512-xz+zZvj/XE6KDHZ8kYNO25Axpjng/kTJd/87SwONa6hzlq94K69OdqH1ZC/CehbtwWknpeyHqUHmpnfoaCbLZQ==} engines: {node: '>= 20'} @@ -2729,6 +2731,9 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + babel-dead-code-elimination@1.0.12: resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} @@ -3720,6 +3725,10 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -4785,6 +4794,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + rettime@0.11.11: resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} @@ -5042,6 +5055,10 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -5139,6 +5156,10 @@ packages: undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + undici@6.26.0: + resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==} + engines: {node: '>=18.17'} + undici@7.26.0: resolution: {integrity: sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==} engines: {node: '>=20.18.1'} @@ -7309,6 +7330,15 @@ snapshots: optionalDependencies: react: 19.2.6 + '@vercel/blob@2.4.0': + dependencies: + async-retry: 1.3.3 + is-buffer: 2.0.5 + is-node-process: 1.2.0 + throttleit: 2.1.0 + undici: 6.26.0 + optional: true + '@vercel/functions@3.6.1': dependencies: '@vercel/oidc': 3.5.0 @@ -7511,6 +7541,11 @@ snapshots: astring@1.9.0: {} + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + optional: true + babel-dead-code-elimination@1.0.12: dependencies: '@babel/core': 7.29.7 @@ -8509,6 +8544,9 @@ snapshots: is-arrayish@0.2.1: {} + is-buffer@2.0.5: + optional: true + is-decimal@2.0.1: {} is-docker@3.0.0: {} @@ -9249,7 +9287,7 @@ snapshots: nf3@0.3.17: {} - nitro@3.0.260522-beta(@vercel/functions@3.6.1)(@voidzero-dev/vite-plus-core@0.1.23(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3))(chokidar@5.0.0)(dotenv@17.4.2)(jiti@2.7.0)(lru-cache@11.5.1): + nitro@3.0.260522-beta(@vercel/blob@2.4.0)(@vercel/functions@3.6.1)(@voidzero-dev/vite-plus-core@0.1.23(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3))(chokidar@5.0.0)(dotenv@17.4.2)(jiti@2.7.0)(lru-cache@11.5.1): dependencies: consola: 3.4.2 crossws: 0.4.5(srvx@0.11.16) @@ -9264,7 +9302,7 @@ snapshots: rolldown: 1.0.3 srvx: 0.11.16 unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.7(@vercel/functions@3.6.1)(chokidar@5.0.0)(db0@0.3.4)(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3) + unstorage: 2.0.0-alpha.7(@vercel/blob@2.4.0)(@vercel/functions@3.6.1)(chokidar@5.0.0)(db0@0.3.4)(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3) optionalDependencies: dotenv: 17.4.2 jiti: 2.7.0 @@ -9902,6 +9940,9 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry@0.13.1: + optional: true + rettime@0.11.11: {} reusify@1.1.0: {} @@ -10239,6 +10280,9 @@ snapshots: term-size@2.2.1: {} + throttleit@2.1.0: + optional: true + tiny-invariant@1.3.3: {} tinybench@2.9.0: {} @@ -10321,6 +10365,9 @@ snapshots: undici-types@7.24.6: {} + undici@6.26.0: + optional: true + undici@7.26.0: {} unenv@2.0.0-rc.24: @@ -10383,8 +10430,9 @@ snapshots: picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 - unstorage@2.0.0-alpha.7(@vercel/functions@3.6.1)(chokidar@5.0.0)(db0@0.3.4)(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3): + unstorage@2.0.0-alpha.7(@vercel/blob@2.4.0)(@vercel/functions@3.6.1)(chokidar@5.0.0)(db0@0.3.4)(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3): optionalDependencies: + '@vercel/blob': 2.4.0 '@vercel/functions': 3.6.1 chokidar: 5.0.0 db0: 0.3.4 diff --git a/registry/modules/ai-tanstack-ai/module.ts b/registry/modules/ai-tanstack-ai/module.ts index 9f98956..e37ff94 100644 --- a/registry/modules/ai-tanstack-ai/module.ts +++ b/registry/modules/ai-tanstack-ai/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "tanstack-ai", diff --git a/registry/modules/ai-tanstack-ai/package.json b/registry/modules/ai-tanstack-ai/package.json index 9498fc2..e7c8e5b 100644 --- a/registry/modules/ai-tanstack-ai/package.json +++ b/registry/modules/ai-tanstack-ai/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/ai-vercel-ai-sdk/module.ts b/registry/modules/ai-vercel-ai-sdk/module.ts index 223c284..1120413 100644 --- a/registry/modules/ai-vercel-ai-sdk/module.ts +++ b/registry/modules/ai-vercel-ai-sdk/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "vercel-ai-sdk", diff --git a/registry/modules/ai-vercel-ai-sdk/package.json b/registry/modules/ai-vercel-ai-sdk/package.json index c7a90d1..59ef045 100644 --- a/registry/modules/ai-vercel-ai-sdk/package.json +++ b/registry/modules/ai-vercel-ai-sdk/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/auth-better-auth/module.ts b/registry/modules/auth-better-auth/module.ts index 22f06a3..213a7d2 100644 --- a/registry/modules/auth-better-auth/module.ts +++ b/registry/modules/auth-better-auth/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "better-auth", diff --git a/registry/modules/auth-better-auth/package.json b/registry/modules/auth-better-auth/package.json index f518aa4..afe3608 100644 --- a/registry/modules/auth-better-auth/package.json +++ b/registry/modules/auth-better-auth/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/auth-clerk/module.ts b/registry/modules/auth-clerk/module.ts index 4b517e8..9e646a2 100644 --- a/registry/modules/auth-clerk/module.ts +++ b/registry/modules/auth-clerk/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; /** * Clerk ships its own hosted UI and session store; it bypasses the user-owned diff --git a/registry/modules/auth-clerk/package.json b/registry/modules/auth-clerk/package.json index ef983dd..3ce82bb 100644 --- a/registry/modules/auth-clerk/package.json +++ b/registry/modules/auth-clerk/package.json @@ -6,8 +6,7 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" }, "devDependencies": { "@types/node": "^25.9.1" diff --git a/registry/modules/db-postgres/module.ts b/registry/modules/db-postgres/module.ts index ac992fd..e4b85b6 100644 --- a/registry/modules/db-postgres/module.ts +++ b/registry/modules/db-postgres/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "postgres", diff --git a/registry/modules/db-postgres/package.json b/registry/modules/db-postgres/package.json index 39b10bf..f256ef5 100644 --- a/registry/modules/db-postgres/package.json +++ b/registry/modules/db-postgres/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/db-sqlite/module.ts b/registry/modules/db-sqlite/module.ts index 54e0e79..9338d72 100644 --- a/registry/modules/db-sqlite/module.ts +++ b/registry/modules/db-sqlite/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "sqlite", diff --git a/registry/modules/db-sqlite/package.json b/registry/modules/db-sqlite/package.json index cf59544..0f56c20 100644 --- a/registry/modules/db-sqlite/package.json +++ b/registry/modules/db-sqlite/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/email-resend/module.ts b/registry/modules/email-resend/module.ts index 63338bc..d774460 100644 --- a/registry/modules/email-resend/module.ts +++ b/registry/modules/email-resend/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; // Ships the Resend SDK client + a sample React Email template alongside it in // `packages/email/`. Per-framework adapters add the delivery-event webhook diff --git a/registry/modules/email-resend/package.json b/registry/modules/email-resend/package.json index f9a17ca..702c1e3 100644 --- a/registry/modules/email-resend/package.json +++ b/registry/modules/email-resend/package.json @@ -6,6 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/framework-next/module.ts b/registry/modules/framework-next/module.ts index bc8b27c..c862ed1 100644 --- a/registry/modules/framework-next/module.ts +++ b/registry/modules/framework-next/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "next", diff --git a/registry/modules/framework-next/package.json b/registry/modules/framework-next/package.json index 8b32388..0093aea 100644 --- a/registry/modules/framework-next/package.json +++ b/registry/modules/framework-next/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/framework-tanstack-start/module.ts b/registry/modules/framework-tanstack-start/module.ts index 0703db2..0211809 100644 --- a/registry/modules/framework-tanstack-start/module.ts +++ b/registry/modules/framework-tanstack-start/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "tanstack-start", diff --git a/registry/modules/framework-tanstack-start/package.json b/registry/modules/framework-tanstack-start/package.json index 649c7e1..6ebf60c 100644 --- a/registry/modules/framework-tanstack-start/package.json +++ b/registry/modules/framework-tanstack-start/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/monorepo-turbo/module.ts b/registry/modules/monorepo-turbo/module.ts index 331fca4..7526d62 100644 --- a/registry/modules/monorepo-turbo/module.ts +++ b/registry/modules/monorepo-turbo/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "turbo", diff --git a/registry/modules/monorepo-turbo/package.json b/registry/modules/monorepo-turbo/package.json index aabe8b6..6d7363a 100644 --- a/registry/modules/monorepo-turbo/package.json +++ b/registry/modules/monorepo-turbo/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/orm-drizzle/module.ts b/registry/modules/orm-drizzle/module.ts index febcb27..77940fe 100644 --- a/registry/modules/orm-drizzle/module.ts +++ b/registry/modules/orm-drizzle/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "drizzle", diff --git a/registry/modules/orm-drizzle/package.json b/registry/modules/orm-drizzle/package.json index 82afd53..3ffa123 100644 --- a/registry/modules/orm-drizzle/package.json +++ b/registry/modules/orm-drizzle/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/orm-prisma/module.ts b/registry/modules/orm-prisma/module.ts index 4527c16..c8f2195 100644 --- a/registry/modules/orm-prisma/module.ts +++ b/registry/modules/orm-prisma/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "prisma", diff --git a/registry/modules/orm-prisma/package.json b/registry/modules/orm-prisma/package.json index 146b1b6..b76cc97 100644 --- a/registry/modules/orm-prisma/package.json +++ b/registry/modules/orm-prisma/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/payments-polar/module.ts b/registry/modules/payments-polar/module.ts index edde826..a64e742 100644 --- a/registry/modules/payments-polar/module.ts +++ b/registry/modules/payments-polar/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; // SDK is auth-agnostic. `+better-auth` adapters additionally wire the // `@polar-sh/better-auth` plugin into `packages/auth/src/auth.ts` so the diff --git a/registry/modules/payments-polar/package.json b/registry/modules/payments-polar/package.json index 1e49eaa..1c683ab 100644 --- a/registry/modules/payments-polar/package.json +++ b/registry/modules/payments-polar/package.json @@ -6,6 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/payments-stripe/module.ts b/registry/modules/payments-stripe/module.ts index 52d8599..37ac313 100644 --- a/registry/modules/payments-stripe/module.ts +++ b/registry/modules/payments-stripe/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; // SDK is auth-agnostic. `+better-auth` adapters additionally wire the // `@better-auth/stripe` plugin into `packages/auth/src/auth.ts` so the auth diff --git a/registry/modules/payments-stripe/package.json b/registry/modules/payments-stripe/package.json index 9817ab5..608d28a 100644 --- a/registry/modules/payments-stripe/package.json +++ b/registry/modules/payments-stripe/package.json @@ -6,6 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/testing-playwright/module.ts b/registry/modules/testing-playwright/module.ts index 57bec5e..53b765e 100644 --- a/registry/modules/testing-playwright/module.ts +++ b/registry/modules/testing-playwright/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "playwright", diff --git a/registry/modules/testing-playwright/package.json b/registry/modules/testing-playwright/package.json index c373fe9..f576bbc 100644 --- a/registry/modules/testing-playwright/package.json +++ b/registry/modules/testing-playwright/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/testing-vitest/module.ts b/registry/modules/testing-vitest/module.ts index 3b2b6b4..a8be99b 100644 --- a/registry/modules/testing-vitest/module.ts +++ b/registry/modules/testing-vitest/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "vitest", diff --git a/registry/modules/testing-vitest/package.json b/registry/modules/testing-vitest/package.json index 9b2f6c8..81af58b 100644 --- a/registry/modules/testing-vitest/package.json +++ b/registry/modules/testing-vitest/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/tooling-biome/module.ts b/registry/modules/tooling-biome/module.ts index 7647062..465b369 100644 --- a/registry/modules/tooling-biome/module.ts +++ b/registry/modules/tooling-biome/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "biome", diff --git a/registry/modules/tooling-biome/package.json b/registry/modules/tooling-biome/package.json index 679b9b2..af492be 100644 --- a/registry/modules/tooling-biome/package.json +++ b/registry/modules/tooling-biome/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/tooling-eslint-prettier/module.ts b/registry/modules/tooling-eslint-prettier/module.ts index 8f28b91..b94cb74 100644 --- a/registry/modules/tooling-eslint-prettier/module.ts +++ b/registry/modules/tooling-eslint-prettier/module.ts @@ -1,4 +1,4 @@ -import { defineModule, type TemplateRef } from "@stanza/registry"; +import { defineModule, type TemplateRef } from "@withstanza/schema"; // Same templates regardless of which framework adapter wins. The eslint config // is a single Handlebars template that branches on `{{peers.framework}}`, so diff --git a/registry/modules/tooling-eslint-prettier/package.json b/registry/modules/tooling-eslint-prettier/package.json index 95192f3..e61dadb 100644 --- a/registry/modules/tooling-eslint-prettier/package.json +++ b/registry/modules/tooling-eslint-prettier/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/tooling-oxlint-oxfmt/module.ts b/registry/modules/tooling-oxlint-oxfmt/module.ts index fe5fa9e..80d2bb2 100644 --- a/registry/modules/tooling-oxlint-oxfmt/module.ts +++ b/registry/modules/tooling-oxlint-oxfmt/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "oxlint-oxfmt", diff --git a/registry/modules/tooling-oxlint-oxfmt/package.json b/registry/modules/tooling-oxlint-oxfmt/package.json index 8914f9e..0f5aafe 100644 --- a/registry/modules/tooling-oxlint-oxfmt/package.json +++ b/registry/modules/tooling-oxlint-oxfmt/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/ui-shadcn-base/module.ts b/registry/modules/ui-shadcn-base/module.ts index 17806c9..64a0d51 100644 --- a/registry/modules/ui-shadcn-base/module.ts +++ b/registry/modules/ui-shadcn-base/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "shadcn-base", diff --git a/registry/modules/ui-shadcn-base/package.json b/registry/modules/ui-shadcn-base/package.json index c7aeca0..a9a172c 100644 --- a/registry/modules/ui-shadcn-base/package.json +++ b/registry/modules/ui-shadcn-base/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/ui-shadcn-radix/module.ts b/registry/modules/ui-shadcn-radix/module.ts index 944ffe2..f4ac913 100644 --- a/registry/modules/ui-shadcn-radix/module.ts +++ b/registry/modules/ui-shadcn-radix/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "shadcn-radix", diff --git a/registry/modules/ui-shadcn-radix/package.json b/registry/modules/ui-shadcn-radix/package.json index a214b2a..8210ba4 100644 --- a/registry/modules/ui-shadcn-radix/package.json +++ b/registry/modules/ui-shadcn-radix/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/registry/modules/ui-tailwind/module.ts b/registry/modules/ui-tailwind/module.ts index 27cfd8b..c29b630 100644 --- a/registry/modules/ui-tailwind/module.ts +++ b/registry/modules/ui-tailwind/module.ts @@ -1,4 +1,4 @@ -import { defineModule } from "@stanza/registry"; +import { defineModule } from "@withstanza/schema"; export default defineModule({ id: "tailwind", diff --git a/registry/modules/ui-tailwind/package.json b/registry/modules/ui-tailwind/package.json index b9ced3a..5e656bd 100644 --- a/registry/modules/ui-tailwind/package.json +++ b/registry/modules/ui-tailwind/package.json @@ -6,7 +6,6 @@ "type": "module", "main": "./module.ts", "dependencies": { - "@stanza/codemods": "workspace:*", - "@stanza/registry": "workspace:*" + "@withstanza/schema": "workspace:*" } } diff --git a/packages/registry/src/build.ts b/scripts/compile-registry.ts similarity index 75% rename from packages/registry/src/build.ts rename to scripts/compile-registry.ts index 35be773..c91502c 100644 --- a/packages/registry/src/build.ts +++ b/scripts/compile-registry.ts @@ -1,45 +1,42 @@ /** * Static registry build. Scans `registry/modules/*`, imports each module's * default export, writes: - * - /registry/index.json — the main file (per-module + * - /index.json — the main file (per-module * metadata, each carrying `path`) - * - /registry/modules/-.json — per-module full manifests - * - /schema.json — JSON Schema for stanza.json + * - /modules/-.json — per-module full manifests * * The output base defaults to `/dist`. Pass a positional arg to - * redirect — e.g. the web app's prebuild points it at `apps/web/public/` so - * the registry lands directly under `public/registry/`. + * redirect — e.g. the web app's `compile-registry` task points it at + * `apps/web/public/registry`. * - * Invoked via `jiti packages/registry/src/build.ts [outBase]`. Also exported as - * `buildRegistry()` so the CLI test harness can build a fixture registry. + * A standalone build tool (not part of any package): the web prebuild, the + * Blob upload in CI, and the CLI test harness all invoke it via + * `jiti scripts/compile-registry.ts [outDir]`. `compileRegistry()` is also + * exported for any in-process caller. */ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { CATEGORIES, type Logo, type Module, type RegistryIndex } from "@withstanza/schema"; import { optimize } from "svgo"; -import { manifestJsonSchema } from "./manifest"; -import { CATEGORIES, type Logo, type Module, type RegistryIndex } from "./module"; - /** - * Build the static registry into `/registry/` (+ `/schema.json`). - * `modulesDir` defaults to the repo's `registry/modules`; tests can point it - * elsewhere. Returns the module count and resolved output base. + * Build the static registry into ``. `modulesDir` defaults to the + * repo's `registry/modules`; tests can point it elsewhere. Returns the final + * module count. */ -export async function buildRegistry(opts: { - outBase: string; +export async function compileRegistry(opts: { modulesDir?: string; -}): Promise<{ count: number; outBase: string }> { + outDir: string; +}): Promise<{ count: number }> { const here = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = findRepoRoot(here); const modulesDir = opts.modulesDir ?? path.join(repoRoot, "registry", "modules"); - const outBase = path.resolve(opts.outBase); - const registryDir = path.join(outBase, "registry"); // Wipe + recreate so a renamed module's stale JSON doesn't linger and // ghost-serve from the CDN. - const modulesOut = path.join(registryDir, "modules"); + const modulesOut = path.join(opts.outDir, "modules"); fs.rmSync(modulesOut, { recursive: true, force: true }); fs.mkdirSync(modulesOut, { recursive: true }); @@ -80,7 +77,7 @@ export async function buildRegistry(opts: { // filename. (The physical layout is the build's choice — the contract is // the explicit `path`.) const modulePath = `modules/${mod.category}-${mod.id}.json`; - fs.writeFileSync(path.join(registryDir, modulePath), JSON.stringify(inlined, null, 2)); + fs.writeFileSync(path.join(opts.outDir, modulePath), JSON.stringify(inlined, null, 2)); // The index keeps lightweight metadata — no template `content`, no // per-adapter payloads — but it DOES carry top-level fields like `logo` @@ -100,26 +97,18 @@ export async function buildRegistry(opts: { modules: metadata, }; - fs.writeFileSync(path.join(registryDir, "index.json"), JSON.stringify(index, null, 2)); + fs.writeFileSync(path.join(opts.outDir, "index.json"), JSON.stringify(index, null, 2)); - // The stanza.json JSON Schema is served at the web root (not under /registry/), - // so it lands at /schema.json rather than /registry/schema.json. - fs.writeFileSync( - path.join(outBase, "schema.json"), - JSON.stringify(manifestJsonSchema(), null, 2), - ); - - return { count: metadata.length, outBase }; + return { count: metadata.length }; } -// Direct invocation: `jiti packages/registry/src/build.ts [outBase]`. Skipped -// when imported (e.g. by the CLI test harness), so importing has no side effect. +// Direct invocation: `jiti scripts/compile-registry.ts [outDir]`. Skipped when +// imported (e.g. by test harnesses), so importing has no side effect. if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { const repoRoot = findRepoRoot(path.dirname(fileURLToPath(import.meta.url))); - const { count, outBase } = await buildRegistry({ - outBase: process.argv[2] ?? path.join(repoRoot, "dist"), - }); - console.log(`Wrote ${count} modules to ${outBase}`); + const outDir = process.argv[2] ?? path.join(repoRoot, "dist"); + const { count } = await compileRegistry({ outDir }); + console.log(`Wrote ${count} modules to ${outDir}`); } /** diff --git a/vite.config.ts b/vite.config.ts index 008851b..be078fb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -29,7 +29,6 @@ export default defineConfig({ "**/routeTree.gen.ts", "**/coverage/**", "apps/web/public/registry/**", - "apps/web/public/schema.json", "registry/modules/*/logo*.svg", "registry/modules/*/templates/**", ],