From b74ffb9d1af04615486d552a02494c1d0d96ad29 Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Wed, 20 May 2026 11:52:53 -0400 Subject: [PATCH] feat: initial commit --- .gitignore | 31 + .npmrc | 4 + .oxfmtrc.json | 4 + .oxlintrc.json | 42 + CLAUDE.md | 70 + LICENSE | 21 + README.md | 31 + REGISTRY.md | 114 + TODO.md | 83 + apps/cli/package.json | 41 + apps/cli/src/bin.ts | 16 + apps/cli/src/codemod-runner.ts | 191 ++ apps/cli/src/commands/add.ts | 108 + apps/cli/src/commands/init.ts | 159 + apps/cli/src/commands/list.ts | 24 + apps/cli/src/commands/remove.ts | 100 + apps/cli/src/commands/search.ts | 29 + apps/cli/src/index.ts | 1 + apps/cli/src/manifest.ts | 54 + apps/cli/src/region-tracker.ts | 69 + apps/cli/src/registry-loader.ts | 134 + apps/cli/src/run.ts | 66 + apps/cli/src/wizard.ts | 122 + apps/cli/tsconfig.json | 8 + apps/web/package.json | 27 + apps/web/src/components/builder.tsx | 110 + apps/web/src/routeTree.gen.ts | 68 + apps/web/src/router.tsx | 16 + apps/web/src/routes/__root.tsx | 27 + apps/web/src/routes/index.tsx | 29 + apps/web/tsconfig.json | 16 + apps/web/vite.config.ts | 13 + package.json | 31 + packages/codemods/package.json | 29 + packages/codemods/src/arrays.ts | 63 + packages/codemods/src/env.test.ts | 50 + packages/codemods/src/env.ts | 53 + packages/codemods/src/imports.test.ts | 62 + packages/codemods/src/imports.ts | 84 + packages/codemods/src/index.ts | 26 + packages/codemods/src/json.test.ts | 75 + packages/codemods/src/json.ts | 106 + packages/codemods/src/project.ts | 31 + packages/codemods/src/template.ts | 33 + packages/codemods/src/types.ts | 34 + packages/codemods/tsconfig.json | 9 + packages/create-stanza/package.json | 26 + packages/create-stanza/src/bin.ts | 23 + packages/create-stanza/tsconfig.json | 8 + packages/registry-build/package.json | 19 + packages/registry-build/src/build.ts | 73 + packages/registry-build/tsconfig.json | 8 + packages/registry/package.json | 30 + packages/registry/src/index.ts | 18 + packages/registry/src/manifest.ts | 75 + packages/registry/src/module.ts | 162 + packages/registry/src/resolver.test.ts | 94 + packages/registry/src/resolver.ts | 99 + packages/registry/tsconfig.json | 8 + pnpm-lock.yaml | 2774 +++++++++++++++++ pnpm-workspace.yaml | 10 + registry/modules/auth-better-auth/module.ts | 111 + .../modules/auth-better-auth/package.json | 12 + .../templates/next/auth-client.ts | 5 + .../templates/next/auth.drizzle.ts | 8 + .../templates/next/auth.prisma.ts | 8 + .../auth-better-auth/templates/next/route.ts | 4 + .../templates/shared/auth-schema.drizzle.ts | 51 + .../templates/tanstack/api.ts | 7 + .../templates/tanstack/auth-client.ts | 5 + .../templates/tanstack/auth.drizzle.ts | 8 + .../templates/tanstack/auth.prisma.ts | 8 + .../modules/auth-better-auth/tsconfig.json | 5 + registry/modules/auth-clerk/module.ts | 42 + registry/modules/auth-clerk/package.json | 12 + .../auth-clerk/templates/layout-wrapper.tsx | 6 + .../auth-clerk/templates/middleware.ts | 10 + registry/modules/auth-clerk/tsconfig.json | 5 + registry/modules/db-postgres/module.ts | 25 + registry/modules/db-postgres/package.json | 12 + registry/modules/db-postgres/tsconfig.json | 5 + registry/modules/db-sqlite/module.ts | 26 + registry/modules/db-sqlite/package.json | 12 + registry/modules/db-sqlite/tsconfig.json | 5 + registry/modules/framework-next/module.ts | 41 + registry/modules/framework-next/package.json | 12 + .../framework-next/templates/app/globals.css | 15 + .../framework-next/templates/app/layout.tsx | 15 + .../framework-next/templates/app/page.tsx | 10 + .../framework-next/templates/next-env.d.ts | 2 + .../framework-next/templates/next.config.ts | 7 + .../framework-next/templates/tsconfig.json | 24 + registry/modules/framework-next/tsconfig.json | 5 + .../framework-tanstack-start/module.ts | 42 + .../framework-tanstack-start/package.json | 12 + .../templates/src/router.tsx | 12 + .../templates/src/routes/__root.tsx | 11 + .../templates/src/routes/index.tsx | 12 + .../templates/tsconfig.json | 22 + .../templates/vite.config.ts | 12 + .../framework-tanstack-start/tsconfig.json | 5 + registry/modules/orm-drizzle/module.ts | 45 + registry/modules/orm-drizzle/package.json | 12 + .../templates/drizzle.config.postgres.ts | 10 + .../templates/drizzle.config.sqlite.ts | 10 + .../templates/src/db/index.postgres.ts | 7 + .../templates/src/db/index.sqlite.ts | 8 + .../templates/src/db/schema.postgres.ts | 10 + .../templates/src/db/schema.sqlite.ts | 6 + registry/modules/orm-drizzle/tsconfig.json | 5 + registry/modules/orm-prisma/module.ts | 43 + registry/modules/orm-prisma/package.json | 12 + .../templates/prisma/schema.postgres.prisma | 8 + .../templates/prisma/schema.sqlite.prisma | 8 + .../modules/orm-prisma/templates/src/db.ts | 7 + registry/modules/orm-prisma/tsconfig.json | 5 + registry/modules/styling-tailwind/module.ts | 39 + .../modules/styling-tailwind/package.json | 12 + .../styling-tailwind/templates/globals.css | 1 + .../templates/next/postcss.config.mjs | 5 + .../templates/tanstack/vite.config.ts | 14 + .../modules/styling-tailwind/tsconfig.json | 5 + tsconfig.base.json | 32 + turbo.json | 29 + 124 files changed, 6925 insertions(+) create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .oxfmtrc.json create mode 100644 .oxlintrc.json create mode 100644 CLAUDE.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 REGISTRY.md create mode 100644 TODO.md create mode 100644 apps/cli/package.json create mode 100755 apps/cli/src/bin.ts create mode 100644 apps/cli/src/codemod-runner.ts create mode 100644 apps/cli/src/commands/add.ts create mode 100644 apps/cli/src/commands/init.ts create mode 100644 apps/cli/src/commands/list.ts create mode 100644 apps/cli/src/commands/remove.ts create mode 100644 apps/cli/src/commands/search.ts create mode 100644 apps/cli/src/index.ts create mode 100644 apps/cli/src/manifest.ts create mode 100644 apps/cli/src/region-tracker.ts create mode 100644 apps/cli/src/registry-loader.ts create mode 100644 apps/cli/src/run.ts create mode 100644 apps/cli/src/wizard.ts create mode 100644 apps/cli/tsconfig.json create mode 100644 apps/web/package.json create mode 100644 apps/web/src/components/builder.tsx create mode 100644 apps/web/src/routeTree.gen.ts create mode 100644 apps/web/src/router.tsx create mode 100644 apps/web/src/routes/__root.tsx create mode 100644 apps/web/src/routes/index.tsx create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/vite.config.ts create mode 100644 package.json create mode 100644 packages/codemods/package.json create mode 100644 packages/codemods/src/arrays.ts create mode 100644 packages/codemods/src/env.test.ts create mode 100644 packages/codemods/src/env.ts create mode 100644 packages/codemods/src/imports.test.ts create mode 100644 packages/codemods/src/imports.ts create mode 100644 packages/codemods/src/index.ts create mode 100644 packages/codemods/src/json.test.ts create mode 100644 packages/codemods/src/json.ts create mode 100644 packages/codemods/src/project.ts create mode 100644 packages/codemods/src/template.ts create mode 100644 packages/codemods/src/types.ts create mode 100644 packages/codemods/tsconfig.json create mode 100644 packages/create-stanza/package.json create mode 100644 packages/create-stanza/src/bin.ts create mode 100644 packages/create-stanza/tsconfig.json create mode 100644 packages/registry-build/package.json create mode 100644 packages/registry-build/src/build.ts create mode 100644 packages/registry-build/tsconfig.json create mode 100644 packages/registry/package.json create mode 100644 packages/registry/src/index.ts create mode 100644 packages/registry/src/manifest.ts create mode 100644 packages/registry/src/module.ts create mode 100644 packages/registry/src/resolver.test.ts create mode 100644 packages/registry/src/resolver.ts create mode 100644 packages/registry/tsconfig.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 registry/modules/auth-better-auth/module.ts create mode 100644 registry/modules/auth-better-auth/package.json create mode 100644 registry/modules/auth-better-auth/templates/next/auth-client.ts create mode 100644 registry/modules/auth-better-auth/templates/next/auth.drizzle.ts create mode 100644 registry/modules/auth-better-auth/templates/next/auth.prisma.ts create mode 100644 registry/modules/auth-better-auth/templates/next/route.ts create mode 100644 registry/modules/auth-better-auth/templates/shared/auth-schema.drizzle.ts create mode 100644 registry/modules/auth-better-auth/templates/tanstack/api.ts create mode 100644 registry/modules/auth-better-auth/templates/tanstack/auth-client.ts create mode 100644 registry/modules/auth-better-auth/templates/tanstack/auth.drizzle.ts create mode 100644 registry/modules/auth-better-auth/templates/tanstack/auth.prisma.ts create mode 100644 registry/modules/auth-better-auth/tsconfig.json create mode 100644 registry/modules/auth-clerk/module.ts create mode 100644 registry/modules/auth-clerk/package.json create mode 100644 registry/modules/auth-clerk/templates/layout-wrapper.tsx create mode 100644 registry/modules/auth-clerk/templates/middleware.ts create mode 100644 registry/modules/auth-clerk/tsconfig.json create mode 100644 registry/modules/db-postgres/module.ts create mode 100644 registry/modules/db-postgres/package.json create mode 100644 registry/modules/db-postgres/tsconfig.json create mode 100644 registry/modules/db-sqlite/module.ts create mode 100644 registry/modules/db-sqlite/package.json create mode 100644 registry/modules/db-sqlite/tsconfig.json create mode 100644 registry/modules/framework-next/module.ts create mode 100644 registry/modules/framework-next/package.json create mode 100644 registry/modules/framework-next/templates/app/globals.css create mode 100644 registry/modules/framework-next/templates/app/layout.tsx create mode 100644 registry/modules/framework-next/templates/app/page.tsx create mode 100644 registry/modules/framework-next/templates/next-env.d.ts create mode 100644 registry/modules/framework-next/templates/next.config.ts create mode 100644 registry/modules/framework-next/templates/tsconfig.json create mode 100644 registry/modules/framework-next/tsconfig.json create mode 100644 registry/modules/framework-tanstack-start/module.ts create mode 100644 registry/modules/framework-tanstack-start/package.json create mode 100644 registry/modules/framework-tanstack-start/templates/src/router.tsx create mode 100644 registry/modules/framework-tanstack-start/templates/src/routes/__root.tsx create mode 100644 registry/modules/framework-tanstack-start/templates/src/routes/index.tsx create mode 100644 registry/modules/framework-tanstack-start/templates/tsconfig.json create mode 100644 registry/modules/framework-tanstack-start/templates/vite.config.ts create mode 100644 registry/modules/framework-tanstack-start/tsconfig.json create mode 100644 registry/modules/orm-drizzle/module.ts create mode 100644 registry/modules/orm-drizzle/package.json create mode 100644 registry/modules/orm-drizzle/templates/drizzle.config.postgres.ts create mode 100644 registry/modules/orm-drizzle/templates/drizzle.config.sqlite.ts create mode 100644 registry/modules/orm-drizzle/templates/src/db/index.postgres.ts create mode 100644 registry/modules/orm-drizzle/templates/src/db/index.sqlite.ts create mode 100644 registry/modules/orm-drizzle/templates/src/db/schema.postgres.ts create mode 100644 registry/modules/orm-drizzle/templates/src/db/schema.sqlite.ts create mode 100644 registry/modules/orm-drizzle/tsconfig.json create mode 100644 registry/modules/orm-prisma/module.ts create mode 100644 registry/modules/orm-prisma/package.json create mode 100644 registry/modules/orm-prisma/templates/prisma/schema.postgres.prisma create mode 100644 registry/modules/orm-prisma/templates/prisma/schema.sqlite.prisma create mode 100644 registry/modules/orm-prisma/templates/src/db.ts create mode 100644 registry/modules/orm-prisma/tsconfig.json create mode 100644 registry/modules/styling-tailwind/module.ts create mode 100644 registry/modules/styling-tailwind/package.json create mode 100644 registry/modules/styling-tailwind/templates/globals.css create mode 100644 registry/modules/styling-tailwind/templates/next/postcss.config.mjs create mode 100644 registry/modules/styling-tailwind/templates/tanstack/vite.config.ts create mode 100644 registry/modules/styling-tailwind/tsconfig.json create mode 100644 tsconfig.base.json create mode 100644 turbo.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f6c90d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +node_modules/ +dist/ +.output/ +.vercel/ +.turbo/ +*.tsbuildinfo +.tanstack/ + +# IDE +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json +.idea/ + +# Logs +*.log +npm-debug.log* +pnpm-debug.log* + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.local +.env.*.local + +# Test artifacts +coverage/ +__snapshots__/.tmp/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..af544a2 --- /dev/null +++ b/.npmrc @@ -0,0 +1,4 @@ +auto-install-peers=true +strict-peer-dependencies=false +node-linker=isolated +prefer-workspace-packages=true diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..55c15df --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,4 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "ignorePatterns": [] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..327f774 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,42 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "unicorn", "import", "promise"], + "categories": { + "correctness": "error", + "suspicious": "warn", + "perf": "warn" + }, + "rules": { + "no-console": "off", + "no-empty": ["error", { "allowEmptyCatch": true }], + "no-await-in-loop": "off", + "unicorn/no-null": "off", + "unicorn/filename-case": "off", + "unicorn/no-array-reduce": "off" + }, + "ignorePatterns": [ + "**/node_modules/**", + "**/dist/**", + "**/.output/**", + "**/.turbo/**", + "**/.vercel/**", + "**/*.gen.ts", + "**/coverage/**", + "registry/modules/*/templates/**" + ], + "overrides": [ + { + "files": ["**/*.test.ts", "**/*.test.tsx", "**/tests/**"], + "rules": { + "typescript/no-explicit-any": "off" + } + }, + { + "files": ["apps/web/**/*.tsx"], + "plugins": ["typescript", "react", "react-hooks", "jsx-a11y", "unicorn", "import"], + "rules": { + "react/react-in-jsx-scope": "off" + } + } + ] +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..26f0efc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,70 @@ +# Stanza + +Shadcn-style CLI for assembling modular full-stack TS monorepos. Currently +ships `init`, `add`, `remove`, `list`, `search` against five slots: +`framework`, `styling`, `db`, `orm`, `auth`. `swap` + `update` verbs and +additional slots (api/ai/ui/payments/email/tooling/testing/deploy) are +planned — the manifest already reserves the fields they'll need +(`modules[slot].version`, `regions`). See [REGISTRY.md](REGISTRY.md) for the +module roadmap and [TODO.md](TODO.md) for active work. + +Three things differentiate stanza from other scaffolders: + +1. Post-init `stanza add` works on existing projects (manifest-driven, peer-aware). +2. Generated code is vendored verbatim — no `@stanza/runtime` dep. +3. Open registry spec — third parties can host their own static JSON. + +## Layout + +- `apps/cli/` — `@stanza/cli`, Bun entrypoint at `src/bin.ts` +- `apps/web/` — `@stanza/web`, TanStack Start visual builder (Vite-native, no Vinxi) +- `packages/registry/` — shared schema, slot/peer resolver, Zod manifest validator +- `packages/codemods/` — ts-morph helpers (idempotent + reversible) +- `packages/create-stanza/` — `pnpm create stanza` shim +- `packages/registry-build/` — Bun script that emits the static CDN JSON +- `registry/modules/-/` — first-party modules: `module.ts` + `templates/` (+ optional `codemods/`) + +## Commands + +- `bun apps/cli/src/bin.ts ` — run CLI directly without build +- `bun packages/registry-build/src/build.ts` — regenerate `dist/registry/{index,modules/*}.json` +- `pnpm lint` / `pnpm lint:fix` — Oxlint across the whole repo (config: `.oxlintrc.json`) +- `pnpm fmt` / `pnpm fmt:check` — oxfmt across the whole repo (config: `.oxfmtrc.json`) +- `cd packages/ && node_modules/.bin/vitest run` — unit tests (per workspace; no root `vitest` binary) +- `cd && node_modules/.bin/tsc --noEmit` — typecheck (per workspace) +- `cd apps/web && node_modules/.bin/vite build` — generates `src/routeTree.gen.ts` (required before first typecheck) +- E2E smoke: seed `$TMPDIR/x` with `stanza.json` + `apps/web/package.json`, then `bun .../bin.ts add ` + +## Toolchain invariants + +- pnpm 10 + `node-linker: isolated` — each workspace MUST declare `@types/node` in its own devDeps and set `types: ["node"]` in tsconfig (auto-discovery doesn't reach into the isolated `node_modules/@types`) +- TypeScript 6 — `allowImportingTsExtensions: true` + `noEmit: true` is set in `tsconfig.base.json`; build with `bun build`, not `tsc` +- `tsconfig.base.json` excludes `**/templates/**` globally — template files target user projects, not this repo +- Zod 4: use `z.partialRecord(K, V)` for finite-key partial records (`z.record(z.enum, V)` requires exhaustive keys) +- TanStack Start: `verbatimModuleSyntax: false` in `apps/web/tsconfig.json` (server bundles leak otherwise); `tanstackStart()` MUST precede `react()` in vite plugins +- Bun runs the CLI directly from `.ts`; don't add a TS compile step + +## Architecture rules + +- **Modules are vendored**: their templates land in the user's repo verbatim; no `@stanza/runtime` dep +- **Slot taxonomy** is currently `framework | styling | db | orm | auth` (see `KNOWN_SLOTS`); adding a slot is a manifest schema bump — update `KNOWN_SLOTS`, `slotOrder`, and the Zod manifest schema together +- **Adapter keys** encode peer choices (e.g., `next+drizzle`); the resolver picks the most specific match +- **Region ownership** in `stanza.json` is the source of truth for `remove`/future-`swap`; two modules claiming the same region is a hard error (`RegionConflictError`) +- **Declarative beats imperative**: prefer `templates`/`dependencies`/`env`/`scripts` over imperative codemods; the runner applies declarative fields generically +- **Reserved manifest fields**: `modules[slot].version` and `regions` are written today but only fully consumed by the upcoming `swap`/`update` verbs — do not drop them + +## Module authoring + +- `module.ts` exports `defineModule({...})` with at least one adapter (use `match: {}` for "default / no peer") +- Templates go in `templates/`, referenced by `src` path; `scope: "app"` resolves against `manifest.appDir`, `scope: "repo"` against the repo root +- Framework modules MUST NOT ship a `package.json.tpl` — it collides with `addPackageDependency`. Let the runner merge deps into the host's package.json +- Codemod functions (when needed) live in `/codemods/index.ts` as a default-exported map of `{ id: Codemod }` + +## Gotchas + +- Clack spinners (`p.spinner()`) don't auto-stop on promise rejection — wrap awaits in try/catch and call `spinner.stop(...failed)` in the catch +- LSP diagnostics on template files (e.g. "Cannot find module 'react'") are noise; the global exclude works for `tsc` but tsserver still indexes them +- The dev registry is found by walking up from `import.meta.url` looking for `registry/modules/`; `STANZA_REGISTRY` env var overrides (FS path or HTTP URL) +- `pnpm install` says "Already up to date" when only workspace `package.json` files changed without lockfile-affecting bumps — use `pnpm install --force` to re-link +- Oxlint has _most_ but not all ESLint plugin rules — `prevent-abbreviations`, `react/jsx-uses-react`, etc. don't exist. Check `node_modules/oxlint/configuration_schema.json` if a rule name fails +- oxfmt auto-loads `.gitignore` and `.prettierignore`; we use `.oxfmtrc.json`'s `ignorePatterns` instead so we don't masquerade as a Prettier project diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..495db88 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Jake Jarvis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f131808 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Stanza + +Modular monorepo template CLI — shadcn for full-stack TypeScript stacks. + +```sh +pnpm create stanza my-app +``` + +Pick a framework, ORM, database, auth provider, styling — get a clean monorepo with idiomatic code, vendored into your repo. Add modules later with `stanza add`. + +## What's inside + +``` +apps/ + cli/ # @stanza/cli — the CLI binary + web/ # builder.stanza.dev (TanStack Start) +packages/ + registry/ # shared schema, slot/peer/capability resolver + codemods/ # ts-morph helpers for region-aware patching + create-stanza/ # `pnpm create stanza` shim +registry/ + modules/ # first-party modules (framework, orm, db, auth, styling) +``` + +## Status + +Work in progress. See [CLAUDE.md](./CLAUDE.md), [TODO.md](./TODO.md), and [REGISTRY.md](./REGISTRY.md) for current state. + +## License + +MIT — see [LICENSE](./LICENSE). diff --git a/REGISTRY.md b/REGISTRY.md new file mode 100644 index 0000000..ad90656 --- /dev/null +++ b/REGISTRY.md @@ -0,0 +1,114 @@ +# First-party module registry + +This is the canonical roadmap for the first-party modules stanza ships. Each +entry maps to a `registry/modules/-/` directory. Update this file +when a module lands, gets renamed, or is dropped. + +Legend: `[x]` added· `[ ]` planned + +## framework + +- [x] **tanstack-start** — TanStack Start on Vite (no Vinxi). Provides `web`, `react`, `ssr`, `node` +- [x] **next** — Next.js 16 (App Router). Provides `web`, `react`, `ssr`, `rsc`, `node`, `edge` +- [ ] **nuxt** — Vue. Will require relaxing the React-implicit assumption in many peer modules (capability tag `vue`) +- [ ] **svelte** — SvelteKit. Capability `svelte` +- [ ] **solid** — SolidStart. Capability `solid` + +## api + +_New slot._ Optional layer between the framework and the database/services. + +- [ ] **trpc** — tRPC v11; per-framework adapters (next, tanstack-start, nuxt, svelte, solid) +- [ ] **orpc** — oRPC + +## ai + +_New slot._ + +- [ ] **vercel-ai-sdk** — `ai` package + provider sub-recipes +- [ ] **tanstack-ai** — TanStack AI + +## auth + +- [x] **better-auth** — headless, peers `orm: [drizzle, prisma]`, `framework: [next, tanstack-start]` +- [x] **clerk** — hosted UI, peers `framework: [next]` (TanStack Start adapter planned) +- [ ] **workos** — WorkOS AuthKit + +## orm + +- [x] **drizzle** — Drizzle ORM 0.45, peers `db: [postgres, sqlite]` +- [x] **prisma** — Prisma 7, peers `db: [postgres, sqlite]` + +## db + +- [x] **postgres** — `postgres` driver 3.4.x +- [x] **sqlite** — `better-sqlite3` 12.x + +## styling + +- [x] **tailwind** — Tailwind v4, adapters per framework + +## ui + +_New slot._ Layered on top of `styling`; provides component primitives. + +- [ ] **shadcn-radix** — classic shadcn/ui (Radix primitives) +- [ ] **shadcn-base** — shadcn on react-base-ui + +## payments + +_New slot._ + +- [ ] **stripe** — Checkout Sessions + webhooks +- [ ] **polar** — Polar SDK + +## email + +_New slot._ + +- [ ] **resend** — Resend SDK + React Email templates + +## tooling + +_New slot._ Lint/format toolchain. Single-choice slot. + +- [ ] **eslint-prettier** — ESLint + Prettier +- [ ] **biome** — Biome (lint + format) +- [ ] **oxlint-oxfmt** — Oxlint + oxfmt + +## testing + +_New slot._ Multi-choice (unit + e2e are independent). + +- [ ] **vitest** — unit + integration +- [ ] **playwright** — e2e + +## deploy + +_New slot._ + +- [ ] **vercel** — `vercel.json` + framework-specific output +- [ ] **cloudflare** — Workers / Pages adapter per framework +- [ ] **railway** — `railway.toml` + Dockerfile +- [ ] **docker** — generic `Dockerfile` + compose for self-host + +## monorepo + +_Currently hardcoded in `bootstrapShell` as Turborepo. Will become a slot if we ever add a Nx/Moonrepo alternative._ + +- [x] **turborepo** — Turbo 2.x (current default; not yet a configurable slot) + +## packageManager + +_Not a slot — a top-level field in `stanza.json` (`packageManager: "pnpm" | "bun" | "npm"`). Wizard prompts; codemods only touch `package.json`, never lockfiles._ + +- [x] pnpm (default) +- [x] bun +- [x] npm +- [ ] yarn — needs lockfile/workspace handling that differs from the others + +## Slot taxonomy changes required + +The slots `api`, `ai`, `ui`, `payments`, `email`, `tooling`, `testing`, `deploy` don't exist in `KNOWN_SLOTS` yet. Adding them is a manifest-schema bump and a `slotOrder` update in `packages/registry/src/resolver.ts`. Plan the rollout so existing `stanza.json` files don't break — new slots are optional, so adding them is additive. + +`tooling` and `monorepo` are single-choice within their slot; `testing` is multi-choice (a project can have both vitest and playwright). The current resolver assumes single-choice — `testing` will need an array-valued slot or two sub-slots (`testing-unit`, `testing-e2e`). diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..e121e70 --- /dev/null +++ b/TODO.md @@ -0,0 +1,83 @@ +# TODO + +State at end of last session: the core architecture is end-to-end functional. `stanza add` composes modules across slots (verified: `framework next` → `db sqlite` → `orm drizzle` → `auth better-auth` produces a working tree with the right adapter selection). 20 unit tests pass. apps/web is a minimal Vite-native TanStack Start scaffold — needs the actual builder UI. + +## Web app (apps/web) — priority + +The builder is functionally wired but visually unfinished. It's inline-styled radios + a code block. + +- [ ] Add Tailwind 4 to `apps/web` — use the `@tailwindcss/vite` plugin (mirror what the `styling-tailwind` module emits for the `tanstack-start` adapter) +- [ ] Hand-roll a small component layer or pull in shadcn (web variant) for cards, buttons, inputs, code blocks +- [ ] Replace the radio-list `Builder` with a card grid per slot, showing module label + description + homepage link +- [ ] Show _why_ a module is filtered out (e.g. "needs `framework: next`, you picked tanstack-start") instead of just dropping it +- [ ] Deep linking: encode selections in URL search params (`?framework=next&orm=drizzle&...`) so users can share configurations +- [ ] Copy-to-clipboard for the generated `pnpm create stanza ...` command +- [ ] Preview pane: list of files stanza will write (derived from `module.adapters[].templates[].dest` for the selected adapter combo) +- [ ] Show pinned npm versions per selected module (from `adapter.dependencies` / `adapter.devDependencies`) +- [ ] Module detail route at `/m/$slot/$id` — full description, all adapters, deps, env vars, templates list +- [ ] Search route at `/search` backed by the same registry index +- [ ] Layout: header (logo, GitHub link, docs link), footer +- [ ] Dark mode (Tailwind 4 `light-dark()` + system pref) +- [ ] SEO: meta tags via `head()` on routes, OG image, sitemap +- [ ] Host the registry on the same domain — wire `packages/registry-build` output to `apps/web/public/registry/` and serve statically, or via a Vercel route handler +- [ ] Vercel deploy config — `vercel.json` if needed, env vars for `STANZA_REGISTRY` (point dev at local FS, prod at the deployed CDN path) +- [ ] Docs section (could be MDX routes): overview, authoring guide, registry spec + +## CLI (apps/cli) + +The wizard and verbs work but a few things from the plan are stubbed. + +- [ ] Implement opt-out PostHog telemetry — wire `posthog-node` (already a dep), prompt on first run, store a `telemetryId` in `stanza.json`, respect `--no-telemetry` and `DO_NOT_TRACK=1` +- [ ] `--yes` flag for non-interactive `init` — pick defaults via flags (`--framework=next` etc.); essential for CI tests +- [ ] HTTP registry loader path is implemented but unverified — smoke test against the static JSON output +- [ ] `stanza init`: today's `bootstrapShell` doesn't include `tsconfig.json` at the repo root for the generated project, and doesn't emit `turbo.json`. Decide whether stanza ships those or modules do +- [ ] Better error messages for `RegionConflictError` (current message is technical; should suggest `stanza remove ` or manual cleanup) +- [ ] `bun build` the CLI for publish — script exists, not yet exercised +- [ ] Tests for command handlers (`init`, `add`, `remove`) — current coverage is just codemods + resolver + +## Modules + +Functional but a few real issues to fix. + +- [ ] `auth-better-auth` tanstack-start adapter references `import.meta.env.VITE_BETTER_AUTH_URL` but the env var is declared as `BETTER_AUTH_URL` — either add `VITE_` prefix or read it server-side +- [ ] `auth-better-auth` auth schema (`shared/auth-schema.drizzle.ts`) is Postgres-only — needs a SQLite variant (different timestamp/boolean handling) +- [ ] `auth-clerk`: ships `layout-wrapper.tsx` (a `ClerkRootProvider`) but doesn't actually wire it into `app/layout.tsx` — needs an imperative codemod to wrap `` children +- [ ] `styling-tailwind` + `framework-next` adapter writes `app/globals.css` over the framework's own — confirm the merge order produces the right import (`@import "tailwindcss"`) +- [ ] First imperative codemod example to validate the codemod-runner's `loadCodemods()` path — try ClerkProvider wrapping +- [ ] Authoring guide: docs page covering `defineModule`, slot/peer/capability semantics, template vs. codemod choice, region ownership + +## Registry expansion + +The full first-party module roadmap lives in [REGISTRY.md](REGISTRY.md). These are the schema/resolver changes needed before most of those modules can land. + +- [ ] Add new slots to `KNOWN_SLOTS` + `slotOrder`: `api`, `ai`, `ui`, `payments`, `email`, `tooling`, `testing`, `deploy`. Decide ordering for the wizard's topological prompts (deploy last; testing/tooling near the end; api/ai after framework) +- [ ] Multi-choice slot support — `testing` is the first slot where two modules co-exist (vitest + playwright). Either (a) make the slot value `string[]` in `stanza.json` and update the resolver/UI, or (b) split into `testing-unit` + `testing-e2e` sub-slots. Pick before implementing the testing modules +- [ ] Capability tag expansion — current set is `web | native | react | node | edge | ssr | rsc`. Adding Nuxt/Svelte/Solid needs `vue`, `svelte`, `solid` capabilities; existing React-only modules must add `requires: ['react']` so the resolver filters correctly +- [ ] `monorepo` is hardcoded to Turborepo in `bootstrapShell` — promote to a real slot only when a second option lands +- [ ] `packageManager` Yarn support — different workspace/lockfile semantics than pnpm/bun/npm; needs its own bootstrap branch +- [ ] Cross-framework adapter explosion — Better Auth, tRPC, oRPC, the AI SDKs, etc. each need a sub-adapter per framework (next/tanstack-start/nuxt/svelte/solid). Decide whether to ship them all in one module with many adapters, or split per framework. Lean toward many-adapters-per-module to keep the slot count sane + +## Infrastructure + +- [ ] GitHub Actions CI: typecheck (every workspace), unit tests, registry build, lint (add Biome or ESLint) +- [ ] Golden snapshot tests per module combination (per the plan's verification section) — for each valid `(framework, orm, db, auth, styling, pm)` tuple, run `stanza init` headless, snapshot the tree, compare against fixture +- [ ] Integration test for the canonical stack — Docker Postgres + Playwright sign-up flow +- [ ] Registry deploy pipeline — on push to main, build `dist/registry/` and push to Vercel/CF +- [ ] npm publish workflow for `@stanza/cli` + `create-stanza` (Changesets recommended; no changesets config yet) +- [ ] `.env.example` at repo root listing `STANZA_REGISTRY`, PostHog key, etc. + +## Open items from the plan + +- [ ] Domain clearance — confirm `stanza.dev` / `stanza.sh` availability + decide +- [ ] npm scope clearance — `@stanza` scope availability (the CLI assumes `@stanza/cli`); fall back to unscoped `stanza` if taken +- [ ] Better Auth vs Clerk feature parity — Clerk wraps its own UI, Better Auth is headless; document the difference or ship shared UI stubs (`SignInForm`, callback page) that each adapter fills in + +## Out of scope for now + +These are real future work but consciously deferred — don't pull them in opportunistically. + +- `stanza swap ` — manifest already records `version` + `regions` to support it +- `stanza update` — pinned-version 3-way merge +- Third-party registry hosting — the spec exists implicitly; publish it formally later +- React Native / Expo modules — needs the `native` capability + cross-platform framework slot +- Additional first-party modules — full catalog tracked in [REGISTRY.md](REGISTRY.md); land the slot taxonomy changes above first diff --git a/apps/cli/package.json b/apps/cli/package.json new file mode 100644 index 0000000..3fc2e63 --- /dev/null +++ b/apps/cli/package.json @@ -0,0 +1,41 @@ +{ + "name": "@stanza/cli", + "version": "0.1.0", + "description": "Modular monorepo template CLI — shadcn for stacks.", + "license": "MIT", + "bin": { + "stanza": "./src/bin.ts" + }, + "files": [ + "src", + "dist" + ], + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./bin": "./src/bin.ts" + }, + "scripts": { + "dev": "bun run --watch ./src/bin.ts", + "build": "bun build ./src/bin.ts --target=bun --outdir=./dist --minify", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@clack/prompts": "^1.4.0", + "@stanza/codemods": "workspace:*", + "@stanza/registry": "workspace:*", + "kleur": "^4.1.5", + "mri": "^1.2.0", + "posthog-node": "^5.34.7", + "semver": "^7.8.0" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "@types/semver": "^7.7.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } +} diff --git a/apps/cli/src/bin.ts b/apps/cli/src/bin.ts new file mode 100755 index 0000000..fb88873 --- /dev/null +++ b/apps/cli/src/bin.ts @@ -0,0 +1,16 @@ +#!/usr/bin/env bun +import mri from "mri"; +import { run } from "./run.ts"; + +const argv = mri(process.argv.slice(2), { + alias: { h: "help", v: "version" }, + boolean: ["help", "version", "yes", "dry-run", "no-telemetry"], +}); + +run(argv).catch((err: unknown) => { + // Top-level catch — every command should handle its own errors, but this is + // the safety net. We use process.exitCode rather than process.exit so any + // outstanding async work (telemetry flush) can finish. + console.error(err instanceof Error ? err.message : String(err)); + process.exitCode = 1; +}); diff --git a/apps/cli/src/codemod-runner.ts b/apps/cli/src/codemod-runner.ts new file mode 100644 index 0000000..340de09 --- /dev/null +++ b/apps/cli/src/codemod-runner.ts @@ -0,0 +1,191 @@ +import path from "node:path"; +import fs from "node:fs"; +import { openProject } from "@stanza/codemods"; +import { + addPackageDependency, + addPackageScript, + addEnvVar, + writeTemplateFile, +} from "@stanza/codemods"; +import type { Module, ModuleAdapter, StanzaManifest } from "@stanza/registry"; +import type { CodemodContext, Project } from "@stanza/codemods"; +import { claim, RegionConflictError } from "./region-tracker.ts"; +import { writeManifest } from "./manifest.ts"; + +export type RunResult = { + manifest: StanzaManifest; + touchedFiles: string[]; + dryRun: boolean; +}; + +/** + * Apply a module's chosen adapter to the project at projectRoot. Handles + * the declarative parts (templates, env vars, deps, scripts) directly; + * defers anything in `adapter.codemods` to the codemod registry. + * + * This is the only place that writes to disk on `add`. Both the manifest + * update and the file writes share the same dry-run gate. + */ +export async function applyModule(args: { + projectRoot: string; + manifest: StanzaManifest; + module: Module; + adapter: ModuleAdapter; + registryRoot: string; + dryRun: boolean; +}): Promise { + const { projectRoot, module, adapter, registryRoot, dryRun } = args; + let manifest = args.manifest; + const touchedFiles = new Set(); + const appRoot = path.join(projectRoot, manifest.appDir); + + const owner = module.id; + const moduleDir = path.join(registryRoot, "modules", `${module.slot}-${module.id}`); + + // 1. Templates (claim regions per-template-file). + for (const tpl of adapter.templates ?? []) { + const dest = + tpl.scope === "repo" ? path.join(projectRoot, tpl.dest) : path.join(appRoot, tpl.dest); + const rel = path.relative(projectRoot, dest); + + manifest = claim(manifest, rel, "file", owner); + if (!dryRun) { + writeTemplateFile(path.join(moduleDir, "templates", tpl.src), dest, { + appDir: manifest.appDir, + projectName: manifest.name, + }); + } + touchedFiles.add(rel); + } + + // 2. Dependencies on the host package.json (in the active app). + const appPkg = path.join(appRoot, "package.json"); + if ( + (adapter.dependencies || adapter.devDependencies || adapter.scripts) && + fs.existsSync(appPkg) + ) { + for (const [name, range] of Object.entries(adapter.dependencies ?? {})) { + manifest = claim(manifest, path.relative(projectRoot, appPkg), `dependencies.${name}`, owner); + if (!dryRun) addPackageDependency(appPkg, name, range); + } + for (const [name, range] of Object.entries(adapter.devDependencies ?? {})) { + manifest = claim( + manifest, + path.relative(projectRoot, appPkg), + `devDependencies.${name}`, + owner, + ); + if (!dryRun) addPackageDependency(appPkg, name, range, { dev: true }); + } + for (const [name, command] of Object.entries(adapter.scripts ?? {})) { + manifest = claim(manifest, path.relative(projectRoot, appPkg), `scripts.${name}`, owner); + if (!dryRun) addPackageScript(appPkg, name, command); + } + touchedFiles.add(path.relative(projectRoot, appPkg)); + } + + // 3. Env vars in .env.example at repo root. + if (adapter.env && adapter.env.length > 0) { + const envFile = path.join(projectRoot, ".env.example"); + for (const v of adapter.env) { + manifest = claim(manifest, ".env.example", v.name, owner); + if (!dryRun) addEnvVar(envFile, v.name, v.example, v.description); + } + touchedFiles.add(".env.example"); + } + + // 4. Imperative codemods — resolved against the module's local codemod registry. + if (adapter.codemods?.length) { + const codemods = await loadCodemods(moduleDir); + const project = lazyProject(appRoot); + const ctx = buildContext({ + projectRoot, + appRoot, + manifest, + module, + adapter, + project, + touchedFiles, + dryRun, + onClaim: (file, region) => { + manifest = claim(manifest, file, region, owner); + }, + }); + for (const id of adapter.codemods) { + const fn = codemods[id]; + if (!fn) { + throw new Error(`Codemod "${id}" not found in module ${module.id}`); + } + const result = await fn.apply(ctx); + result.touchedFiles.forEach((f) => touchedFiles.add(f)); + } + if (!dryRun) await project.save?.(); + } + + if (!dryRun) { + manifest = { + ...manifest, + modules: { + ...manifest.modules, + [module.slot]: { + id: module.id, + version: module.version, + adapter: adapter.key, + }, + }, + }; + writeManifest(projectRoot, manifest); + } + + return { manifest, touchedFiles: [...touchedFiles], dryRun }; +} + +type CodemodMap = Record; + +async function loadCodemods(moduleDir: string): Promise { + const entry = path.join(moduleDir, "codemods", "index.ts"); + if (!fs.existsSync(entry)) return {}; + const mod = (await import(entry)) as { default?: CodemodMap; codemods?: CodemodMap }; + return mod.default ?? mod.codemods ?? {}; +} + +function lazyProject(appRoot: string): { (): Project; save?: () => Promise } { + let project: Project | undefined; + const fn = (() => { + if (!project) project = openProject(appRoot); + return project; + }) as { (): Project; save?: () => Promise }; + fn.save = async () => { + if (project) await project.save(); + }; + return fn; +} + +function buildContext(args: { + projectRoot: string; + appRoot: string; + manifest: StanzaManifest; + module: Module; + adapter: ModuleAdapter; + project: () => Project; + touchedFiles: Set; + dryRun: boolean; + onClaim: (file: string, region: string) => void; +}): CodemodContext { + return { + projectRoot: args.projectRoot, + appRoot: args.appRoot, + project: args.project, + manifest: args.manifest, + owner: { slot: args.module.slot, module: args.module.id }, + adapter: args.adapter.key, + claimRegion(file, region) { + args.onClaim(file, region); + }, + releaseRegion() { + // No-op during `add`. The remove path uses regionsOwnedBy() to reverse. + }, + }; +} + +export { RegionConflictError }; diff --git a/apps/cli/src/commands/add.ts b/apps/cli/src/commands/add.ts new file mode 100644 index 0000000..f60d8bf --- /dev/null +++ b/apps/cli/src/commands/add.ts @@ -0,0 +1,108 @@ +import type { Argv } from "mri"; +import kleur from "kleur"; +import * as p from "@clack/prompts"; +import path from "node:path"; +import fs from "node:fs"; +import { findProjectRoot, readManifest } from "../manifest.ts"; +import { loadRegistry } from "../registry-loader.ts"; +import { applyModule } from "../codemod-runner.ts"; +import { resolveAdapter, type SlotId, KNOWN_SLOTS } from "@stanza/registry"; + +export async function cmdAdd(args: { + slot?: string; + moduleId?: string; + argv: Argv; +}): Promise { + if (!args.slot || !args.moduleId) { + p.log.error("Usage: stanza add "); + process.exitCode = 1; + return; + } + + if (!(KNOWN_SLOTS as readonly string[]).includes(args.slot)) { + p.log.error(`Unknown slot: ${args.slot}. Known: ${KNOWN_SLOTS.join(", ")}`); + process.exitCode = 1; + return; + } + const slot = args.slot as SlotId; + + const projectRoot = findProjectRoot(); + if (!projectRoot) { + p.log.error("No stanza.json found in this or any parent directory."); + process.exitCode = 1; + return; + } + + const manifest = readManifest(projectRoot); + if (manifest.modules[slot]) { + p.log.error( + `Slot "${slot}" is already filled by "${manifest.modules[slot]!.id}". Run \`stanza remove ${slot}\` first.`, + ); + process.exitCode = 1; + return; + } + + const registry = await loadRegistry(); + const mod = await registry.loadModule(slot, args.moduleId).catch(() => null); + if (!mod) { + p.log.error(`Module not found: ${slot}/${args.moduleId}`); + process.exitCode = 1; + return; + } + + const resolved = resolveAdapter(mod, { manifest, pending: {} }); + if (!resolved.ok) { + p.log.error(`Cannot add ${mod.label}: ${describeResolveError(resolved.error.kind)}`); + process.exitCode = 1; + return; + } + + const dryRun = Boolean(args.argv["dry-run"]); + const spinner = p.spinner(); + spinner.start(`Adding ${mod.label}`); + + const registryRoot = pickRegistryRoot(); + try { + await applyModule({ + projectRoot, + manifest, + module: mod, + adapter: resolved.adapter, + registryRoot, + dryRun, + }); + } catch (err) { + spinner.stop(`${mod.label} ${kleur.red("failed")}`); + throw err; + } + + spinner.stop(`${kleur.green("✓")} ${mod.label} added`); + if (dryRun) p.log.info(kleur.yellow("[dry-run] no files were written")); +} + +function describeResolveError(kind: string): string { + switch (kind) { + case "missing-peer": + return "a required peer module isn't installed."; + case "incompatible-peer": + return "the installed peer module isn't supported."; + case "no-adapter": + return "no adapter matches your current stack."; + default: + return kind; + } +} + +function pickRegistryRoot(): string { + const override = process.env.STANZA_REGISTRY; + if (override && !override.startsWith("http")) return override; + const here = path.dirname(new URL(import.meta.url).pathname); + let dir = here; + for (let i = 0; i < 6; i++) { + if (fs.existsSync(path.join(dir, "registry", "modules"))) { + return path.join(dir, "registry"); + } + dir = path.dirname(dir); + } + throw new Error("Could not locate stanza registry root."); +} diff --git a/apps/cli/src/commands/init.ts b/apps/cli/src/commands/init.ts new file mode 100644 index 0000000..0faea38 --- /dev/null +++ b/apps/cli/src/commands/init.ts @@ -0,0 +1,159 @@ +import path from "node:path"; +import fs from "node:fs"; +import type { Argv } from "mri"; +import kleur from "kleur"; +import * as p from "@clack/prompts"; +import { initManifest } from "../manifest.ts"; +import { loadRegistry } from "../registry-loader.ts"; +import { runInitWizard } from "../wizard.ts"; +import { applyModule } from "../codemod-runner.ts"; +import { resolveAdapter, slotOrder } from "@stanza/registry"; + +export async function cmdInit(args: { name?: string; argv: Argv }): Promise { + const registry = await loadRegistry(); + const defaultName = args.name ?? path.basename(process.cwd()); + + const result = await runInitWizard({ registry, defaultName }); + if (!result) return; + + const projectRoot = path.resolve(process.cwd(), result.name); + + if (fs.existsSync(projectRoot)) { + p.log.error(`Directory already exists: ${projectRoot}`); + process.exitCode = 1; + return; + } + + fs.mkdirSync(projectRoot, { recursive: true }); + + // Bootstrap the empty monorepo shell. + bootstrapShell(projectRoot, { + name: result.name, + packageManager: result.packageManager, + appDir: result.appDir, + }); + + let manifest = initManifest({ + projectRoot, + name: result.name, + appDir: result.appDir, + packageManager: result.packageManager, + }); + + const registryRoot = pickRegistryRoot(); + + const dryRun = Boolean(args.argv["dry-run"]); + if (dryRun) p.log.info(kleur.yellow("[dry-run] no files will be written")); + + const spinner = p.spinner(); + + for (const slot of slotOrder) { + const mod = result.modules[slot]; + if (!mod) continue; + spinner.start(`Installing ${mod.label}`); + const adapter = resolveAdapter(mod, { manifest, pending: {} }); + if (!adapter.ok) { + spinner.stop(`${mod.label} ${kleur.red("failed")}`); + throw new Error(`Could not resolve adapter for ${slot}/${mod.id}: ${adapter.error.kind}`); + } + const r = await applyModule({ + projectRoot, + manifest, + module: mod, + adapter: adapter.adapter, + registryRoot, + dryRun, + }); + manifest = r.manifest; + spinner.stop(`${kleur.green("✓")} ${mod.label}`); + } + + p.outro( + [ + kleur.green("Done."), + "", + ` ${kleur.dim("$")} cd ${result.name}`, + ` ${kleur.dim("$")} ${result.packageManager} install`, + ` ${kleur.dim("$")} ${result.packageManager} dev`, + ].join("\n"), + ); +} + +function bootstrapShell( + projectRoot: string, + opts: { name: string; packageManager: "pnpm" | "bun" | "npm"; appDir: string }, +) { + // Root package.json + fs.writeFileSync( + path.join(projectRoot, "package.json"), + JSON.stringify( + { + name: opts.name, + private: true, + version: "0.1.0", + packageManager: defaultPmVersion(opts.packageManager), + scripts: { + dev: `${opts.packageManager} -r run dev`, + build: `${opts.packageManager} -r run build`, + test: `${opts.packageManager} -r run test`, + }, + }, + null, + 2, + ) + "\n", + ); + + if (opts.packageManager === "pnpm") { + fs.writeFileSync( + path.join(projectRoot, "pnpm-workspace.yaml"), + `packages:\n - "apps/*"\n - "packages/*"\n`, + ); + } else if (opts.packageManager === "bun") { + // Bun reads workspaces from package.json. + const pkgPath = path.join(projectRoot, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + pkg.workspaces = ["apps/*", "packages/*"]; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + } else { + const pkgPath = path.join(projectRoot, "package.json"); + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + pkg.workspaces = ["apps/*", "packages/*"]; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); + } + + fs.writeFileSync( + path.join(projectRoot, ".gitignore"), + "node_modules/\ndist/\n.output/\n.vercel/\n.turbo/\n.env\n.env.local\n.env.*.local\n*.log\n", + ); + + fs.writeFileSync( + path.join(projectRoot, ".env.example"), + `# Stanza-managed environment variables.\n`, + ); + + // App shell — empty but layout-correct. The framework module fills it in. + fs.mkdirSync(path.join(projectRoot, opts.appDir), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, "packages"), { recursive: true }); +} + +function defaultPmVersion(pm: "pnpm" | "bun" | "npm"): string { + return { pnpm: "pnpm@9.12.0", bun: "bun@1.1.34", npm: "npm@10.9.0" }[pm]; +} + +function pickRegistryRoot(): string { + // Same resolution as registry-loader's local path lookup. Kept here so the + // template + codemod files can be sourced from the same place we read the + // index from. + const override = process.env.STANZA_REGISTRY; + if (override && !override.startsWith("http")) return override; + // Walk up from this file. + const here = path.dirname(new URL(import.meta.url).pathname); + let dir = here; + for (let i = 0; i < 6; i++) { + if (fs.existsSync(path.join(dir, "registry", "modules"))) { + return path.join(dir, "registry"); + } + dir = path.dirname(dir); + } + throw new Error("Could not locate stanza registry root."); +} diff --git a/apps/cli/src/commands/list.ts b/apps/cli/src/commands/list.ts new file mode 100644 index 0000000..ea6db65 --- /dev/null +++ b/apps/cli/src/commands/list.ts @@ -0,0 +1,24 @@ +import type { Argv } from "mri"; +import * as p from "@clack/prompts"; +import kleur from "kleur"; +import { findProjectRoot, readManifest } from "../manifest.ts"; +import { slotOrder, type SlotId } from "@stanza/registry"; + +export async function cmdList(_args: { argv: Argv }): Promise { + const projectRoot = findProjectRoot(); + if (!projectRoot) { + p.log.error("No stanza.json found."); + process.exitCode = 1; + return; + } + const manifest = readManifest(projectRoot); + + const rows = slotOrder.map((slot: SlotId) => { + const m = manifest.modules[slot]; + return m + ? `${kleur.cyan(slot.padEnd(10))} ${m.id} ${kleur.dim(`@${m.version}`)} ${kleur.dim(`[${m.adapter}]`)}` + : `${kleur.cyan(slot.padEnd(10))} ${kleur.dim("(empty)")}`; + }); + + console.log(rows.join("\n")); +} diff --git a/apps/cli/src/commands/remove.ts b/apps/cli/src/commands/remove.ts new file mode 100644 index 0000000..f8b61c4 --- /dev/null +++ b/apps/cli/src/commands/remove.ts @@ -0,0 +1,100 @@ +import type { Argv } from "mri"; +import * as p from "@clack/prompts"; +import kleur from "kleur"; +import path from "node:path"; +import fs from "node:fs"; +import { findProjectRoot, readManifest, writeManifest } from "../manifest.ts"; +import { regionsOwnedBy } from "../region-tracker.ts"; +import { removePackageDependency, removeEnvVar } from "@stanza/codemods"; +import { KNOWN_SLOTS, type SlotId } from "@stanza/registry"; + +export async function cmdRemove(args: { slot?: string; argv: Argv }): Promise { + if (!args.slot) { + p.log.error("Usage: stanza remove "); + process.exitCode = 1; + return; + } + if (!(KNOWN_SLOTS as readonly string[]).includes(args.slot)) { + p.log.error(`Unknown slot: ${args.slot}`); + process.exitCode = 1; + return; + } + const slot = args.slot as SlotId; + + const projectRoot = findProjectRoot(); + if (!projectRoot) { + p.log.error("No stanza.json found."); + process.exitCode = 1; + return; + } + + const manifest = readManifest(projectRoot); + const installed = manifest.modules[slot]; + if (!installed) { + p.log.warn(`Slot "${slot}" is not filled.`); + return; + } + + const owned = regionsOwnedBy(manifest, installed.id); + const dryRun = Boolean(args.argv["dry-run"]); + + // Best-effort reversal: deps, env vars, and whole-file templates revert + // cleanly. Regions touched by imperative codemods get reported as "needs + // manual cleanup" until proper inverse codemods land. + const manualCleanup: string[] = []; + + for (const { file, region } of owned) { + const abs = path.join(projectRoot, file); + if (file === ".env.example") { + if (!dryRun) removeEnvVar(abs, region); + continue; + } + if (file.endsWith("package.json")) { + if (region.startsWith("dependencies.") || region.startsWith("devDependencies.")) { + const name = region.split(".").slice(1).join("."); + if (!dryRun) removePackageDependency(abs, name); + continue; + } + if (region.startsWith("scripts.")) { + if (!dryRun) { + const pkg = JSON.parse(fs.readFileSync(abs, "utf8")); + delete pkg.scripts?.[region.slice("scripts.".length)]; + fs.writeFileSync(abs, JSON.stringify(pkg, null, 2) + "\n"); + } + continue; + } + } + if (region === "file") { + // Whole-file template — safe to delete since stanza wrote it. + if (!dryRun && fs.existsSync(abs)) fs.unlinkSync(abs); + continue; + } + manualCleanup.push(`${file}:${region}`); + } + + // Update manifest: drop module + its regions. + const nextRegions = { ...manifest.regions }; + for (const { file, region } of owned) { + if (nextRegions[file]) { + const copy = { ...nextRegions[file] }; + delete copy[region]; + if (Object.keys(copy).length === 0) delete nextRegions[file]; + else nextRegions[file] = copy; + } + } + const nextModules = { ...manifest.modules }; + delete nextModules[slot]; + + if (!dryRun) { + writeManifest(projectRoot, { ...manifest, modules: nextModules, regions: nextRegions }); + } + + p.log.success(`${kleur.green("✓")} Removed ${installed.id} from ${slot}`); + if (manualCleanup.length > 0) { + p.log.warn( + `${manualCleanup.length} region(s) need manual cleanup:\n` + + manualCleanup.map((r) => ` • ${r}`).join("\n"), + ); + } + if (dryRun) p.log.info(kleur.yellow("[dry-run] no files were written")); +} diff --git a/apps/cli/src/commands/search.ts b/apps/cli/src/commands/search.ts new file mode 100644 index 0000000..33f52ae --- /dev/null +++ b/apps/cli/src/commands/search.ts @@ -0,0 +1,29 @@ +import type { Argv } from "mri"; +import kleur from "kleur"; +import { loadRegistry } from "../registry-loader.ts"; + +export async function cmdSearch(args: { query?: string; argv: Argv }): Promise { + const registry = await loadRegistry(); + const q = (args.query ?? "").toLowerCase().trim(); + + const results = registry.index.modules.filter((m) => { + if (!q) return true; + return ( + m.id.toLowerCase().includes(q) || + m.label.toLowerCase().includes(q) || + m.description.toLowerCase().includes(q) || + m.slot.includes(q) + ); + }); + + if (results.length === 0) { + console.log(kleur.dim("No modules found.")); + return; + } + + for (const m of results) { + const head = `${kleur.bold(m.label)} ${kleur.dim(`(${m.slot}/${m.id})`)}`; + const desc = m.description ? ` ${kleur.dim(m.description)}` : ""; + console.log(`${head}\n${desc}`); + } +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts new file mode 100644 index 0000000..58aa318 --- /dev/null +++ b/apps/cli/src/index.ts @@ -0,0 +1 @@ +export { run } from "./run.ts"; diff --git a/apps/cli/src/manifest.ts b/apps/cli/src/manifest.ts new file mode 100644 index 0000000..86c7096 --- /dev/null +++ b/apps/cli/src/manifest.ts @@ -0,0 +1,54 @@ +import fs from "node:fs"; +import path from "node:path"; +import { StanzaManifestSchema, type StanzaManifest, emptyManifest } from "@stanza/registry"; + +const MANIFEST_FILENAME = "stanza.json"; + +export function manifestPath(projectRoot: string): string { + return path.join(projectRoot, MANIFEST_FILENAME); +} + +export function readManifest(projectRoot: string): StanzaManifest { + const file = manifestPath(projectRoot); + if (!fs.existsSync(file)) { + throw new Error(`No stanza.json found in ${projectRoot}. Run \`stanza init\` first.`); + } + const parsed = StanzaManifestSchema.safeParse(JSON.parse(fs.readFileSync(file, "utf8"))); + if (!parsed.success) { + throw new Error( + `Malformed stanza.json:\n${parsed.error.issues + .map((i) => ` • ${i.path.join(".")}: ${i.message}`) + .join("\n")}`, + ); + } + return parsed.data; +} + +export function writeManifest(projectRoot: string, manifest: StanzaManifest): void { + const file = manifestPath(projectRoot); + fs.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf8"); +} + +export function findProjectRoot(cwd: string = process.cwd()): string | undefined { + let dir = path.resolve(cwd); + while (dir !== path.dirname(dir)) { + if (fs.existsSync(path.join(dir, MANIFEST_FILENAME))) return dir; + dir = path.dirname(dir); + } + return undefined; +} + +export function initManifest(input: { + projectRoot: string; + name: string; + appDir?: string; + packageManager?: StanzaManifest["packageManager"]; +}): StanzaManifest { + const m = emptyManifest({ + name: input.name, + appDir: input.appDir, + packageManager: input.packageManager, + }); + writeManifest(input.projectRoot, m); + return m; +} diff --git a/apps/cli/src/region-tracker.ts b/apps/cli/src/region-tracker.ts new file mode 100644 index 0000000..19e0857 --- /dev/null +++ b/apps/cli/src/region-tracker.ts @@ -0,0 +1,69 @@ +import type { StanzaManifest } from "@stanza/registry"; + +/** + * Region ownership operations on a manifest. Pure functions that return a + * new manifest — call sites are responsible for persisting via writeManifest. + */ + +export class RegionConflictError extends Error { + constructor( + public readonly file: string, + public readonly region: string, + public readonly existingOwner: string, + public readonly newOwner: string, + ) { + super( + `Region conflict in ${file}/${region}: already owned by "${existingOwner}", "${newOwner}" tried to claim`, + ); + this.name = "RegionConflictError"; + } +} + +export function claim( + manifest: StanzaManifest, + file: string, + region: string, + owner: string, +): StanzaManifest { + const current = manifest.regions[file]?.[region]; + if (current && current !== owner) { + throw new RegionConflictError(file, region, current, owner); + } + return { + ...manifest, + regions: { + ...manifest.regions, + [file]: { ...manifest.regions[file], [region]: owner }, + }, + }; +} + +export function release(manifest: StanzaManifest, file: string, region: string): StanzaManifest { + const fileRegions = manifest.regions[file]; + if (!fileRegions) return manifest; + + const next: Record = { ...fileRegions }; + delete next[region]; + + const regions = { ...manifest.regions }; + if (Object.keys(next).length === 0) { + delete regions[file]; + } else { + regions[file] = next; + } + + return { ...manifest, regions }; +} + +export function regionsOwnedBy( + manifest: StanzaManifest, + owner: string, +): { file: string; region: string }[] { + const out: { file: string; region: string }[] = []; + for (const [file, regions] of Object.entries(manifest.regions)) { + for (const [region, value] of Object.entries(regions)) { + if (value === owner) out.push({ file, region }); + } + } + return out; +} diff --git a/apps/cli/src/registry-loader.ts b/apps/cli/src/registry-loader.ts new file mode 100644 index 0000000..fd9aa94 --- /dev/null +++ b/apps/cli/src/registry-loader.ts @@ -0,0 +1,134 @@ +import type { Module, RegistryIndex } from "@stanza/registry"; +import path from "node:path"; +import fs from "node:fs"; +import { fileURLToPath } from "node:url"; + +/** + * In dev (when running from the stanza monorepo), modules are imported + * directly from `registry/modules//module.ts`. In production (published + * CLI), the registry is a static JSON endpoint plus per-module bundles. + * + * Resolution order: + * 1. `STANZA_REGISTRY` env var — explicit URL or filesystem path. + * 2. Local workspace at `../../registry` (the stanza monorepo dev case). + * 3. The default CDN URL `https://stanza.dev/registry`. + */ +const DEFAULT_REGISTRY_URL = "https://stanza.dev/registry"; + +export type Registry = { + index: RegistryIndex; + loadModule(slot: string, id: string): Promise; +}; + +export async function loadRegistry(): Promise { + const envOverride = process.env.STANZA_REGISTRY; + if (envOverride) { + return envOverride.startsWith("http") + ? loadHttpRegistry(envOverride) + : loadFsRegistry(envOverride); + } + + const localPath = resolveLocalRegistry(); + if (localPath) return loadFsRegistry(localPath); + + return loadHttpRegistry(DEFAULT_REGISTRY_URL); +} + +function resolveLocalRegistry(): string | undefined { + // When running from `apps/cli/src/bin.ts`, walk up until we find a sibling + // `registry/modules/` dir — the dev-time monorepo layout. + const here = path.dirname(fileURLToPath(import.meta.url)); + let dir = here; + for (let i = 0; i < 6; i++) { + const candidate = path.join(dir, "registry", "modules"); + if (fs.existsSync(candidate)) return path.join(dir, "registry"); + dir = path.dirname(dir); + } + return undefined; +} + +async function loadFsRegistry(rootDir: string): Promise { + const modulesDir = path.join(rootDir, "modules"); + const ids = fs + .readdirSync(modulesDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name); + + // Lazily import each module manifest. We don't keep them all in memory; + // we keep summaries for the index and re-import full modules on demand. + const summaries = await Promise.all( + ids.map(async (name) => { + const mod = await importModule(modulesDir, name); + return summarize(mod); + }), + ); + + const index: RegistryIndex = { + generatedAt: new Date().toISOString(), + schemaVersion: 1, + slots: slotMetadata(), + modules: summaries, + }; + + return { + index, + async loadModule(_slot, id) { + // Modules are stored as `-` directories. We accept either the + // bare id (preferred) or the slot-prefixed dir name. + const dirName = ids.find((d) => d === id || d.endsWith(`-${id}`) || d === `${_slot}-${id}`); + if (!dirName) { + throw new Error(`Module not found in local registry: ${_slot}/${id}`); + } + return importModule(modulesDir, dirName); + }, + }; +} + +async function loadHttpRegistry(baseUrl: string): Promise { + const indexRes = await fetch(`${baseUrl}/index.json`); + if (!indexRes.ok) { + throw new Error(`Failed to load stanza registry from ${baseUrl}: ${indexRes.status}`); + } + const index = (await indexRes.json()) as RegistryIndex; + + return { + index, + async loadModule(slot, id) { + const url = `${baseUrl}/modules/${slot}-${id}.json`; + const res = await fetch(url); + if (!res.ok) throw new Error(`Module fetch failed: ${url} (${res.status})`); + return (await res.json()) as Module; + // NOTE: HTTP-loaded modules can't ship codemod *functions* — they + // contain only the manifest + codemod ids. The CLI ships a bundled + // registry of codemod implementations keyed by id, populated at build + // time from the first-party modules. Custom codemod bundles for + // third-party modules are a future addition (signed JS payloads). + }, + }; +} + +async function importModule(modulesDir: string, dirName: string): Promise { + const entry = path.join(modulesDir, dirName, "module.ts"); + const mod = (await import(entry)) as { default: Module }; + if (!mod.default) { + throw new Error(`Module ${dirName} has no default export at ${entry}`); + } + return mod.default; +} + +function summarize(mod: Module) { + return { + ...mod, + adapters: mod.adapters.map((a) => ({ key: a.key, match: a.match })), + }; +} + +function slotMetadata() { + return [ + { id: "framework" as const, label: "Framework", description: "Web/native app framework." }, + { id: "styling" as const, label: "Styling", description: "CSS / styling system." }, + { id: "db" as const, label: "Database", description: "Database engine." }, + { id: "orm" as const, label: "ORM", description: "Database query layer." }, + { id: "auth" as const, label: "Auth", description: "Authentication provider." }, + ]; +} diff --git a/apps/cli/src/run.ts b/apps/cli/src/run.ts new file mode 100644 index 0000000..fe6025b --- /dev/null +++ b/apps/cli/src/run.ts @@ -0,0 +1,66 @@ +import type { Argv } from "mri"; +import kleur from "kleur"; +import { cmdInit } from "./commands/init.ts"; +import { cmdAdd } from "./commands/add.ts"; +import { cmdRemove } from "./commands/remove.ts"; +import { cmdList } from "./commands/list.ts"; +import { cmdSearch } from "./commands/search.ts"; + +const VERSION = "0.1.0"; + +const HELP = `${kleur.bold("stanza")} — modular monorepo template CLI + +${kleur.bold("Usage")} + stanza [options] + +${kleur.bold("Commands")} + init [name] Scaffold a new monorepo via the interactive wizard. + add Add a module to the current project. + remove Remove the module currently filling a slot. + list List installed modules. + search [query] Search the registry. + +${kleur.bold("Options")} + -h, --help Show this help. + -v, --version Print the CLI version. + --yes Accept all defaults; suppress prompts. + --dry-run Print the actions that would be taken; write nothing. + --no-telemetry Disable telemetry for this invocation. + +${kleur.dim("Docs: https://stanza.dev")} +`; + +export async function run(argv: Argv): Promise { + if (argv.version) { + console.log(VERSION); + return; + } + + const [command, ...rest] = argv._; + if (!command || argv.help) { + console.log(HELP); + return; + } + + switch (command) { + case "init": + await cmdInit({ name: rest[0], argv }); + return; + case "add": + await cmdAdd({ slot: rest[0], moduleId: rest[1], argv }); + return; + case "remove": + await cmdRemove({ slot: rest[0], argv }); + return; + case "list": + await cmdList({ argv }); + return; + case "search": + await cmdSearch({ query: rest.join(" "), argv }); + return; + default: + console.error(kleur.red(`Unknown command: ${command}`)); + console.error(HELP); + process.exitCode = 1; + } +} diff --git a/apps/cli/src/wizard.ts b/apps/cli/src/wizard.ts new file mode 100644 index 0000000..0533d05 --- /dev/null +++ b/apps/cli/src/wizard.ts @@ -0,0 +1,122 @@ +import * as p from "@clack/prompts"; +import kleur from "kleur"; +import type { Module, RegistryIndex, SlotId } from "@stanza/registry"; +import { resolveAdapter, slotOrder } from "@stanza/registry"; +import type { Registry } from "./registry-loader.ts"; +import { emptyManifest } from "@stanza/registry"; + +export type WizardResult = { + name: string; + appDir: string; + packageManager: "pnpm" | "bun" | "npm"; + modules: Partial>; +}; + +/** + * Run the interactive `stanza init` wizard. Topological prompt order (defined + * by slotOrder); slots that don't have any compatible modules given prior + * picks are skipped. Returns the user's selections. + */ +export async function runInitWizard(args: { + registry: Registry; + defaultName: string; +}): Promise { + const { registry, defaultName } = args; + + p.intro(kleur.bold().cyan("stanza")); + + const name = await p.text({ + message: "Project name", + initialValue: defaultName, + validate: (v) => + v && /^[a-z0-9][a-z0-9-]*$/i.test(v) ? undefined : "Use letters, digits, dashes.", + }); + if (p.isCancel(name)) return cancel(); + + const modules: Partial> = {}; + + for (const slot of slotOrder) { + const candidates = candidatesForSlot(registry.index, slot, modules); + if (candidates.length === 0) { + continue; + } + + const choices = candidates.map((m) => ({ + value: m.id, + label: m.label, + hint: m.description, + })); + + const choice = await p.select({ + message: `${slotLabel(slot)}?`, + options: [...choices, { value: "__skip__", label: kleur.dim("Skip this slot") }], + }); + if (p.isCancel(choice)) return cancel(); + if (choice === "__skip__") continue; + + const full = await registry.loadModule(slot, choice as string); + modules[slot] = full; + } + + const pmChoice = await p.select({ + message: "Package manager?", + options: [ + { value: "pnpm", label: "pnpm", hint: "Recommended" }, + { value: "bun", label: "bun" }, + { value: "npm", label: "npm" }, + ], + initialValue: "pnpm", + }); + if (p.isCancel(pmChoice)) return cancel(); + + // Summary screen — what we're about to write. + const summary = [ + `${kleur.bold("Name:")} ${String(name)}`, + `${kleur.bold("Package manager:")} ${String(pmChoice)}`, + "", + ...Object.entries(modules).map( + ([slot, mod]) => + `${kleur.bold(slotLabel(slot as SlotId).padEnd(16))} ${mod!.label} ${kleur.dim(`(${mod!.id})`)}`, + ), + ].join("\n"); + p.note(summary, "Summary"); + + const confirm = await p.confirm({ message: "Scaffold this project?" }); + if (p.isCancel(confirm) || !confirm) return cancel(); + + return { + name: String(name), + appDir: "apps/web", + packageManager: pmChoice as "pnpm" | "bun" | "npm", + modules, + }; +} + +function candidatesForSlot( + index: RegistryIndex, + slot: SlotId, + picked: Partial>, +): RegistryIndex["modules"] { + const manifest = emptyManifest({ name: "tmp" }); + return index.modules + .filter((m) => m.slot === slot) + .filter((m) => { + // Use the resolver with summary adapters — we only need the peer check, + // not the full adapter selection. Build a temp Module with empty adapter + // bodies just to validate peers. + const synthetic: Module = { ...m, adapters: m.adapters.map((a) => ({ ...a })) }; + const result = resolveAdapter(synthetic, { manifest, pending: picked }); + return result.ok; + }); +} + +function slotLabel(slot: SlotId): string { + return { framework: "Framework", styling: "Styling", db: "Database", orm: "ORM", auth: "Auth" }[ + slot + ]; +} + +function cancel(): null { + p.cancel("Cancelled."); + return null; +} diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json new file mode 100644 index 0000000..307afcd --- /dev/null +++ b/apps/cli/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "types": ["node"] + }, + "include": ["src"] +} diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..378c064 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "@stanza/web", + "version": "0.1.0", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "start": "node .output/server/index.mjs", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@stanza/registry": "workspace:*", + "@tanstack/react-router": "^1.170.5", + "@tanstack/react-start": "^1.168.7", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.2", + "typescript": "^6.0.3", + "vite": "^8.0.13" + } +} diff --git a/apps/web/src/components/builder.tsx b/apps/web/src/components/builder.tsx new file mode 100644 index 0000000..821e09c --- /dev/null +++ b/apps/web/src/components/builder.tsx @@ -0,0 +1,110 @@ +import { useMemo, useState } from "react"; +import type { ModuleSummary, RegistryIndex, SlotId } from "@stanza/registry"; +import { resolveAdapter, slotOrder, emptyManifest } from "@stanza/registry"; + +type Selections = Partial>; + +export function Builder({ index }: { index: RegistryIndex }) { + const [selections, setSelections] = useState({}); + const [name, setName] = useState("my-app"); + + const moduleById = useMemo(() => { + const m = new Map(); + for (const mod of index.modules) m.set(`${mod.slot}:${mod.id}`, mod); + return m; + }, [index]); + + const compatibleBySlot = useMemo(() => { + const out: Record = { + framework: [], + styling: [], + db: [], + orm: [], + auth: [], + }; + const pending = pendingFromSelections(selections, moduleById); + for (const slot of slotOrder) { + out[slot] = index.modules + .filter((m) => m.slot === slot) + .filter((m) => { + const synthetic = { ...m, adapters: m.adapters.map((a) => ({ ...a })) }; + return resolveAdapter(synthetic, { + manifest: emptyManifest({ name: "t" }), + pending, + }).ok; + }); + } + return out; + }, [index, selections, moduleById]); + + const command = useMemo(() => buildCommand(name, selections), [name, selections]); + + return ( +
+ + + {slotOrder.map((slot) => ( +
+ {slotLabel(slot)} + {compatibleBySlot[slot].length === 0 ? ( +
No compatible modules with current picks.
+ ) : ( + compatibleBySlot[slot].map((m) => ( + + )) + )} +
+ ))} + +
+        {command}
+      
+
+ ); +} + +function pendingFromSelections(selections: Selections, moduleById: Map) { + const pending: Record = {}; + for (const [slot, id] of Object.entries(selections)) { + if (!id) continue; + const mod = moduleById.get(`${slot}:${id}`); + if (mod) pending[slot] = mod; + } + // The resolver expects full Modules; the empty `adapters` slot bodies don't + // matter for peer-validation, so cast is safe. + return pending as Parameters[1]["pending"]; +} + +function buildCommand(name: string, selections: Selections): string { + const flags = slotOrder + .map((s) => (selections[s] ? `--${s}=${selections[s]}` : null)) + .filter(Boolean) + .join(" "); + return `pnpm create stanza ${name} ${flags}`.trim(); +} + +function slotLabel(slot: SlotId): string { + return { framework: "Framework", styling: "Styling", db: "Database", orm: "ORM", auth: "Auth" }[ + slot + ]; +} diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts new file mode 100644 index 0000000..b747c08 --- /dev/null +++ b/apps/web/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from "./routes/__root"; +import { Route as IndexRouteImport } from "./routes/index"; + +const IndexRoute = IndexRouteImport.update({ + id: "/", + path: "/", + getParentRoute: () => rootRouteImport, +} as any); + +export interface FileRoutesByFullPath { + "/": typeof IndexRoute; +} +export interface FileRoutesByTo { + "/": typeof IndexRoute; +} +export interface FileRoutesById { + __root__: typeof rootRouteImport; + "/": typeof IndexRoute; +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath; + fullPaths: "/"; + fileRoutesByTo: FileRoutesByTo; + to: "/"; + id: "__root__" | "/"; + fileRoutesById: FileRoutesById; +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute; +} + +declare module "@tanstack/react-router" { + interface FileRoutesByPath { + "/": { + id: "/"; + path: "/"; + fullPath: "/"; + preLoaderRoute: typeof IndexRouteImport; + parentRoute: typeof rootRouteImport; + }; + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, +}; +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes(); + +import type { getRouter } from "./router.tsx"; +import type { createStart } from "@tanstack/react-start"; +declare module "@tanstack/react-start" { + interface Register { + ssr: true; + router: Awaited>; + } +} diff --git a/apps/web/src/router.tsx b/apps/web/src/router.tsx new file mode 100644 index 0000000..0cdf139 --- /dev/null +++ b/apps/web/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen"; + +export function getRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + defaultPreload: "intent", + }); +} + +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx new file mode 100644 index 0000000..c280a90 --- /dev/null +++ b/apps/web/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { HeadContent, Outlet, Scripts, createRootRoute } from "@tanstack/react-router"; + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: "utf-8" }, + { name: "viewport", content: "width=device-width, initial-scale=1" }, + { title: "stanza" }, + { name: "description", content: "Modular monorepo template builder." }, + ], + }), + component: RootComponent, +}); + +function RootComponent() { + return ( + + + + + + + + + + ); +} diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx new file mode 100644 index 0000000..70a1f64 --- /dev/null +++ b/apps/web/src/routes/index.tsx @@ -0,0 +1,29 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { createServerFn } from "@tanstack/react-start"; +import type { RegistryIndex } from "@stanza/registry"; +import { Builder } from "~/components/builder.tsx"; + +const getRegistryIndex = createServerFn({ method: "GET" }).handler(async () => { + const url = process.env.STANZA_REGISTRY ?? "https://stanza.dev/registry"; + const res = await fetch(`${url}/index.json`); + if (!res.ok) { + throw new Error(`Failed to load stanza registry: ${res.status}`); + } + return (await res.json()) as RegistryIndex; +}); + +export const Route = createFileRoute("/")({ + loader: () => getRegistryIndex(), + component: Page, +}); + +function Page() { + const index = Route.useLoaderData(); + return ( +
+

stanza

+

Pick your stack. Get an idiomatic monorepo.

+ +
+ ); +} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..81b78ee --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "react", + "lib": ["DOM", "DOM.Iterable", "ES2023"], + "moduleResolution": "Bundler", + "verbatimModuleSyntax": false, + "noEmit": true, + "allowImportingTsExtensions": true, + "paths": { + "~/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"] +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..96c16f1 --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vite"; +import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [ + // tanstackStart() MUST come before react() — the Start plugin generates + // route trees and transforms server functions; the React plugin reads + // the transformed output. + tanstackStart(), + react(), + ], +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..3627bdb --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "stanza", + "version": "0.1.0", + "private": true, + "description": "Modular monorepo template CLI — shadcn for stacks.", + "license": "MIT", + "author": "Jake Jarvis ", + "scripts": { + "build": "turbo run build", + "dev": "turbo run dev", + "test": "turbo run test", + "typecheck": "turbo run typecheck", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "fmt": "oxfmt", + "fmt:check": "oxfmt --check", + "registry:build": "turbo run registry:build --filter=@stanza/registry-build" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "oxfmt": "^0.51.0", + "oxlint": "^1.66.0", + "turbo": "^2.9.14", + "typescript": "^6.0.3" + }, + "engines": { + "node": ">=22", + "pnpm": ">=10" + }, + "packageManager": "pnpm@10.20.0" +} diff --git a/packages/codemods/package.json b/packages/codemods/package.json new file mode 100644 index 0000000..956d548 --- /dev/null +++ b/packages/codemods/package.json @@ -0,0 +1,29 @@ +{ + "name": "@stanza/codemods", + "version": "0.1.0", + "license": "MIT", + "files": [ + "src", + "dist" + ], + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@stanza/registry": "workspace:*", + "ts-morph": "^28.0.0" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } +} diff --git a/packages/codemods/src/arrays.ts b/packages/codemods/src/arrays.ts new file mode 100644 index 0000000..be14497 --- /dev/null +++ b/packages/codemods/src/arrays.ts @@ -0,0 +1,63 @@ +import { type SourceFile, SyntaxKind, type ArrayLiteralExpression } from "ts-morph"; + +/** + * Add an element to an array literal exported (or assigned to) under a + * specific name. Idempotent — won't duplicate identical text. + */ +export function addArrayElement( + sourceFile: SourceFile, + exportedName: string, + elementText: string, +): void { + const arr = findArrayLiteral(sourceFile, exportedName); + if (!arr) { + throw new Error( + `addArrayElement: no array literal named "${exportedName}" found in ${sourceFile.getFilePath()}`, + ); + } + + const existing = arr.getElements().map((e) => e.getText().trim()); + if (existing.includes(elementText.trim())) return; + + arr.addElement(elementText); +} + +export function removeArrayElement( + sourceFile: SourceFile, + exportedName: string, + elementText: string, +): void { + const arr = findArrayLiteral(sourceFile, exportedName); + if (!arr) return; + + const target = elementText.trim(); + arr + .getElements() + .filter((e) => e.getText().trim() === target) + .forEach((e) => arr.removeElement(e)); +} + +function findArrayLiteral( + sourceFile: SourceFile, + name: string, +): ArrayLiteralExpression | undefined { + // export const X = [...] + const v = sourceFile.getVariableDeclaration(name); + if (v) { + const init = v.getInitializer(); + if (init?.isKind(SyntaxKind.ArrayLiteralExpression)) { + return init.asKindOrThrow(SyntaxKind.ArrayLiteralExpression); + } + } + + // export default [...] + if (name === "default") { + const def = sourceFile.getExportAssignment(() => true); + const init = def?.getExpression(); + if (init?.isKind(SyntaxKind.ArrayLiteralExpression)) { + return init.asKindOrThrow(SyntaxKind.ArrayLiteralExpression); + } + } + + return undefined; +} diff --git a/packages/codemods/src/env.test.ts b/packages/codemods/src/env.test.ts new file mode 100644 index 0000000..fd39283 --- /dev/null +++ b/packages/codemods/src/env.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it, beforeEach } from "vitest"; +import fs from "node:fs"; +import path from "node:path"; +import os from "node:os"; +import { addEnvVar, removeEnvVar } from "./env.ts"; + +let tmp: string; +beforeEach(() => { + tmp = fs.mkdtempSync(path.join(os.tmpdir(), "stanza-test-")); +}); + +function envFile(initial: string): string { + const p = path.join(tmp, ".env.example"); + fs.writeFileSync(p, initial); + return p; +} + +describe("addEnvVar", () => { + it("creates the file if missing", () => { + const p = path.join(tmp, ".env.example"); + addEnvVar(p, "DATABASE_URL", "postgres://localhost/db"); + expect(fs.readFileSync(p, "utf8")).toMatch(/DATABASE_URL=postgres/); + }); + + it("appends to an existing file", () => { + const p = envFile("FOO=bar\n"); + addEnvVar(p, "BAZ", "qux"); + const out = fs.readFileSync(p, "utf8"); + expect(out).toContain("FOO=bar"); + expect(out).toContain("BAZ=qux"); + }); + + it("is idempotent (updates in place rather than appending)", () => { + const p = envFile("FOO=bar\n"); + addEnvVar(p, "FOO", "baz"); + const out = fs.readFileSync(p, "utf8"); + expect(out).toBe("FOO=baz\n"); + }); +}); + +describe("removeEnvVar", () => { + it("removes the entry and an attached preceding comment", () => { + const p = envFile("# the foo\nFOO=bar\nKEEP=me\n"); + removeEnvVar(p, "FOO"); + const out = fs.readFileSync(p, "utf8"); + expect(out).not.toContain("FOO"); + expect(out).not.toContain("# the foo"); + expect(out).toContain("KEEP=me"); + }); +}); diff --git a/packages/codemods/src/env.ts b/packages/codemods/src/env.ts new file mode 100644 index 0000000..e65a828 --- /dev/null +++ b/packages/codemods/src/env.ts @@ -0,0 +1,53 @@ +import fs from "node:fs"; + +/** + * 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. + */ +export function addEnvVar( + envFile: string, + name: string, + example: string, + description?: string, +): void { + const contents = fs.existsSync(envFile) ? fs.readFileSync(envFile, "utf8") : ""; + 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) { + // Replace existing line (and a preceding comment if present and matches description). + 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")); + } + + fs.writeFileSync(envFile, lines.join("\n").replace(/\n+$/, "\n"), "utf8"); +} + +export function removeEnvVar(envFile: string, name: string): void { + if (!fs.existsSync(envFile)) return; + const lines = fs.readFileSync(envFile, "utf8").split("\n"); + const idx = lines.findIndex((line) => line.replace(/^#\s*/, "").startsWith(`${name}=`)); + if (idx < 0) return; + + // Remove the var line and a preceding standalone comment if it looks attached. + const prev = lines[idx - 1]; + if (prev?.startsWith("#") && (lines[idx - 2] === "" || idx - 2 < 0)) { + lines.splice(idx - 1, 2); + } else { + lines.splice(idx, 1); + } + + fs.writeFileSync(envFile, lines.join("\n"), "utf8"); +} diff --git a/packages/codemods/src/imports.test.ts b/packages/codemods/src/imports.test.ts new file mode 100644 index 0000000..9fc03eb --- /dev/null +++ b/packages/codemods/src/imports.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from "vitest"; +import { Project } from "ts-morph"; +import { addNamedImport, addDefaultImport, removeImport } from "./imports.ts"; + +function file(src: string) { + const project = new Project({ useInMemoryFileSystem: true }); + return project.createSourceFile("x.ts", src); +} + +describe("addNamedImport", () => { + it("inserts when no import for the module exists", () => { + const sf = file(""); + addNamedImport(sf, "react", "useState"); + expect(sf.getText()).toContain(`import { useState } from "react"`); + }); + + it("merges named imports into an existing declaration", () => { + const sf = file(`import { useState } from "react";\n`); + addNamedImport(sf, "react", ["useEffect", "useMemo"]); + const text = sf.getText(); + expect(text).toContain("useState"); + expect(text).toContain("useEffect"); + expect(text).toContain("useMemo"); + expect(text.match(/from "react"/g)?.length).toBe(1); + }); + + it("is idempotent on the same named import", () => { + const sf = file(`import { useState } from "react";\n`); + addNamedImport(sf, "react", "useState"); + expect(sf.getText().match(/useState/g)?.length).toBe(1); + }); +}); + +describe("addDefaultImport", () => { + it("adds a default import alongside named imports if present", () => { + const sf = file(`import { useState } from "react";\n`); + addDefaultImport(sf, "react", "React"); + expect(sf.getText()).toMatch(/import React, \{ useState \} from "react"/); + }); +}); + +describe("removeImport", () => { + it("removes the whole declaration when named is omitted", () => { + const sf = file(`import { useState } from "react";\n`); + removeImport(sf, "react"); + expect(sf.getText()).not.toContain("react"); + }); + + it("removes only the listed named imports, keeping the rest", () => { + const sf = file(`import { useState, useEffect } from "react";\n`); + removeImport(sf, "react", ["useState"]); + const text = sf.getText(); + expect(text).toContain("useEffect"); + expect(text).not.toContain("useState"); + }); + + it("drops the declaration when all named imports are removed and no default", () => { + const sf = file(`import { useState } from "react";\n`); + removeImport(sf, "react", ["useState"]); + expect(sf.getText()).not.toContain("react"); + }); +}); diff --git a/packages/codemods/src/imports.ts b/packages/codemods/src/imports.ts new file mode 100644 index 0000000..360115b --- /dev/null +++ b/packages/codemods/src/imports.ts @@ -0,0 +1,84 @@ +import type { SourceFile } from "ts-morph"; + +/** + * Idempotently add a named import. If the module specifier is already + * imported, merges the named entries; otherwise inserts a new import after + * the last existing import statement. + */ +export function addNamedImport( + sourceFile: SourceFile, + moduleSpecifier: string, + named: string | string[], +): void { + const names = Array.isArray(named) ? named : [named]; + const existing = sourceFile.getImportDeclaration( + (decl) => decl.getModuleSpecifierValue() === moduleSpecifier, + ); + + if (existing) { + const already = new Set(existing.getNamedImports().map((n) => n.getName())); + const toAdd = names.filter((n) => !already.has(n)); + if (toAdd.length > 0) { + existing.addNamedImports(toAdd.map((name) => ({ name }))); + } + return; + } + + sourceFile.addImportDeclaration({ + moduleSpecifier, + namedImports: names.map((name) => ({ name })), + }); +} + +export function addDefaultImport( + sourceFile: SourceFile, + moduleSpecifier: string, + defaultName: string, +): void { + const existing = sourceFile.getImportDeclaration( + (decl) => decl.getModuleSpecifierValue() === moduleSpecifier, + ); + + if (existing) { + if (!existing.getDefaultImport()) { + existing.setDefaultImport(defaultName); + } + return; + } + + sourceFile.addImportDeclaration({ + moduleSpecifier, + defaultImport: defaultName, + }); +} + +export function removeImport( + sourceFile: SourceFile, + moduleSpecifier: string, + named?: string[], +): void { + const existing = sourceFile.getImportDeclaration( + (decl) => decl.getModuleSpecifierValue() === moduleSpecifier, + ); + if (!existing) return; + + if (!named) { + existing.remove(); + return; + } + + const toRemove = new Set(named); + existing + .getNamedImports() + .filter((n) => toRemove.has(n.getName())) + .forEach((n) => n.remove()); + + // If we removed everything and no default, drop the declaration. + if ( + existing.getNamedImports().length === 0 && + !existing.getDefaultImport() && + !existing.getNamespaceImport() + ) { + existing.remove(); + } +} diff --git a/packages/codemods/src/index.ts b/packages/codemods/src/index.ts new file mode 100644 index 0000000..83fdcb2 --- /dev/null +++ b/packages/codemods/src/index.ts @@ -0,0 +1,26 @@ +export type { CodemodContext, Codemod, CodemodResult } from "./types.ts"; + +// Re-export the ts-morph Project type so consumers don't need a direct +// ts-morph dep just to type a parameter. +export type { Project, SourceFile } from "ts-morph"; + +export { openProject } from "./project.ts"; + +export { addNamedImport, addDefaultImport, removeImport } from "./imports.ts"; + +export { addArrayElement, removeArrayElement } from "./arrays.ts"; + +export { + readJson, + writeJson, + mergeJson, + setJsonPath, + unsetJsonPath, + addPackageDependency, + removePackageDependency, + addPackageScript, +} from "./json.ts"; + +export { addEnvVar, removeEnvVar } from "./env.ts"; + +export { renderTemplate, writeTemplateFile } from "./template.ts"; diff --git a/packages/codemods/src/json.test.ts b/packages/codemods/src/json.test.ts new file mode 100644 index 0000000..5abdee8 --- /dev/null +++ b/packages/codemods/src/json.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it, beforeEach } from "vitest"; +import fs from "node:fs"; +import path from "node:path"; +import os from "node:os"; +import { + addPackageDependency, + addPackageScript, + mergeJson, + removePackageDependency, +} from "./json.ts"; + +let tmp: string; +beforeEach(() => { + tmp = fs.mkdtempSync(path.join(os.tmpdir(), "stanza-test-")); +}); + +function writePkg(contents: object): string { + const p = path.join(tmp, "package.json"); + fs.writeFileSync(p, JSON.stringify(contents, null, 2)); + return p; +} + +describe("addPackageDependency", () => { + it("adds to dependencies by default", () => { + const p = writePkg({ name: "x", dependencies: { react: "^18" } }); + addPackageDependency(p, "better-auth", "^1.0.0"); + const out = JSON.parse(fs.readFileSync(p, "utf8")); + expect(out.dependencies["better-auth"]).toBe("^1.0.0"); + expect(out.dependencies.react).toBe("^18"); + }); + + it("creates devDependencies when dev: true", () => { + const p = writePkg({ name: "x" }); + addPackageDependency(p, "vitest", "^2", { dev: true }); + const out = JSON.parse(fs.readFileSync(p, "utf8")); + expect(out.devDependencies.vitest).toBe("^2"); + }); +}); + +describe("removePackageDependency", () => { + it("removes from both dependencies and devDependencies", () => { + const p = writePkg({ + name: "x", + dependencies: { foo: "1" }, + devDependencies: { foo: "1" }, + }); + removePackageDependency(p, "foo"); + const out = JSON.parse(fs.readFileSync(p, "utf8")); + expect(out.dependencies.foo).toBeUndefined(); + expect(out.devDependencies.foo).toBeUndefined(); + }); +}); + +describe("mergeJson", () => { + it("deep-merges objects and reports touched paths", () => { + const p = writePkg({ name: "x", scripts: { dev: "vite" } }); + const touched = mergeJson(p, { + scripts: { dev: "vite --host", db: "drizzle" }, + }); + expect(touched).toContain("scripts.dev"); + expect(touched).toContain("scripts.db"); + const out = JSON.parse(fs.readFileSync(p, "utf8")); + expect(out.scripts.dev).toBe("vite --host"); + expect(out.scripts.db).toBe("drizzle"); + }); +}); + +describe("addPackageScript", () => { + it("adds a script entry", () => { + const p = writePkg({ name: "x" }); + addPackageScript(p, "db:migrate", "drizzle-kit migrate"); + const out = JSON.parse(fs.readFileSync(p, "utf8")); + expect(out.scripts["db:migrate"]).toBe("drizzle-kit migrate"); + }); +}); diff --git a/packages/codemods/src/json.ts b/packages/codemods/src/json.ts new file mode 100644 index 0000000..47626e1 --- /dev/null +++ b/packages/codemods/src/json.ts @@ -0,0 +1,106 @@ +import fs from "node:fs"; + +export function readJson(path: string): T { + return JSON.parse(fs.readFileSync(path, "utf8")) as T; +} + +export function writeJson(path: string, value: unknown): void { + fs.writeFileSync(path, JSON.stringify(value, null, 2) + "\n", "utf8"); +} + +/** + * Deep-merge an object into the JSON at `path`. Arrays are replaced, not merged. + * Returns the dot-paths that were created or changed (for region claiming). + */ +export function mergeJson(path: string, patch: Record): string[] { + const original = readJson>(path); + const touched: string[] = []; + const merged = mergeRecurse(original, patch, "", touched); + writeJson(path, merged); + return touched; +} + +function mergeRecurse( + target: Record, + source: Record, + prefix: string, + touched: string[], +): Record { + const out: Record = { ...target }; + for (const [k, v] of Object.entries(source)) { + const path = prefix ? `${prefix}.${k}` : k; + if ( + v !== null && + typeof v === "object" && + !Array.isArray(v) && + typeof out[k] === "object" && + out[k] !== null && + !Array.isArray(out[k]) + ) { + out[k] = mergeRecurse( + out[k] as Record, + v as Record, + path, + touched, + ); + } else { + if (out[k] !== v) touched.push(path); + out[k] = v; + } + } + return out; +} + +export function setJsonPath(file: string, dotPath: string, value: unknown): void { + const root = readJson>(file); + setPath(root, dotPath, value); + writeJson(file, root); +} + +export function unsetJsonPath(file: string, dotPath: string): void { + const root = readJson>(file); + unsetPath(root, dotPath); + writeJson(file, root); +} + +function setPath(root: Record, dotPath: string, value: unknown): void { + const parts = dotPath.split("."); + // parts is non-empty by construction (dotPath is non-empty caller contract) + const last = parts.pop() as string; + let node: Record = root; + for (const p of parts) { + if (typeof node[p] !== "object" || node[p] === null) node[p] = {}; + node = node[p] as Record; + } + node[last] = value; +} + +function unsetPath(root: Record, dotPath: string): void { + const parts = dotPath.split("."); + const last = parts.pop() as string; + let node: Record = root; + for (const p of parts) { + if (typeof node[p] !== "object" || node[p] === null) return; + node = node[p] as Record; + } + delete node[last]; +} + +export function addPackageDependency( + packageJsonPath: string, + name: string, + range: string, + options: { dev?: boolean } = {}, +): void { + const key = options.dev ? "devDependencies" : "dependencies"; + setJsonPath(packageJsonPath, `${key}.${name}`, range); +} + +export function removePackageDependency(packageJsonPath: string, name: string): void { + unsetJsonPath(packageJsonPath, `dependencies.${name}`); + unsetJsonPath(packageJsonPath, `devDependencies.${name}`); +} + +export function addPackageScript(packageJsonPath: string, name: string, command: string): void { + setJsonPath(packageJsonPath, `scripts.${name}`, command); +} diff --git a/packages/codemods/src/project.ts b/packages/codemods/src/project.ts new file mode 100644 index 0000000..c68c373 --- /dev/null +++ b/packages/codemods/src/project.ts @@ -0,0 +1,31 @@ +import { Project, ScriptTarget, ModuleKind } from "ts-morph"; +import path from "node:path"; +import fs from "node:fs"; + +/** + * Open (or lazily load) a ts-morph Project rooted at the given directory. + * Prefers the project's tsconfig if present; otherwise falls back to a + * minimal in-memory config sufficient for adding imports / modifying + * exports without resolving the whole graph. + */ +export function openProject(rootDir: string): Project { + const tsconfigPath = path.join(rootDir, "tsconfig.json"); + + if (fs.existsSync(tsconfigPath)) { + return new Project({ + tsConfigFilePath: tsconfigPath, + skipAddingFilesFromTsConfig: true, + skipFileDependencyResolution: true, + }); + } + + return new Project({ + compilerOptions: { + target: ScriptTarget.ES2022, + module: ModuleKind.ESNext, + moduleResolution: 100, // Bundler + allowJs: true, + jsx: 4, // ReactJSX + }, + }); +} diff --git a/packages/codemods/src/template.ts b/packages/codemods/src/template.ts new file mode 100644 index 0000000..82a71bb --- /dev/null +++ b/packages/codemods/src/template.ts @@ -0,0 +1,33 @@ +import fs from "node:fs"; +import path from "node:path"; + +/** + * Minimal mustache-style template renderer — `{{ name }}` only. Intentionally + * no logic helpers; templates that need branching should live as separate + * files and the codemod picks which one to render. + */ +export function renderTemplate(source: string, context: Record): string { + return source.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (_, key: string) => { + const value = context[key]; + if (value === undefined) { + throw new Error(`renderTemplate: missing key "${key}"`); + } + return value; + }); +} + +export function writeTemplateFile( + sourcePath: string, + destPath: string, + context: Record | undefined, + options: { overwrite?: boolean } = {}, +): void { + if (!options.overwrite && fs.existsSync(destPath)) { + throw new Error(`writeTemplateFile: refusing to overwrite existing file: ${destPath}`); + } + + fs.mkdirSync(path.dirname(destPath), { recursive: true }); + const raw = fs.readFileSync(sourcePath, "utf8"); + const rendered = context ? renderTemplate(raw, context) : raw; + fs.writeFileSync(destPath, rendered, "utf8"); +} diff --git a/packages/codemods/src/types.ts b/packages/codemods/src/types.ts new file mode 100644 index 0000000..aad7ba5 --- /dev/null +++ b/packages/codemods/src/types.ts @@ -0,0 +1,34 @@ +import type { Project } from "ts-morph"; +import type { ModuleId, SlotId, StanzaManifest } from "@stanza/registry"; + +export type CodemodContext = { + /** Absolute path to the project root (where stanza.json lives). */ + projectRoot: string; + /** Absolute path to the active app dir (manifest.appDir resolved). */ + appRoot: string; + /** ts-morph Project, lazily opened on first AST-touching codemod. */ + project: () => Project; + /** Current manifest snapshot (read-only inside a codemod). */ + manifest: StanzaManifest; + /** The slot/module this codemod is acting on behalf of. */ + owner: { slot: SlotId; module: ModuleId }; + /** Adapter key the resolver selected — useful for adapter-specific branches. */ + adapter: string; + /** Claim a region in stanza.json. Throws if a different owner already holds it. */ + claimRegion(filePath: string, region: string): void; + /** Release a region (used by remove/inverse codemods). */ + releaseRegion(filePath: string, region: string): void; +}; + +export type CodemodResult = { + /** Files this codemod touched (relative to projectRoot). For dry-run output. */ + touchedFiles: string[]; +}; + +export type Codemod = { + id: string; + description?: string; + apply(ctx: CodemodContext): Promise | CodemodResult; + /** Inverse for `stanza remove`. Ship these where cheap. */ + revert?(ctx: CodemodContext): Promise | CodemodResult; +}; diff --git a/packages/codemods/tsconfig.json b/packages/codemods/tsconfig.json new file mode 100644 index 0000000..eac65a4 --- /dev/null +++ b/packages/codemods/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/create-stanza/package.json b/packages/create-stanza/package.json new file mode 100644 index 0000000..a402cbe --- /dev/null +++ b/packages/create-stanza/package.json @@ -0,0 +1,26 @@ +{ + "name": "create-stanza", + "version": "0.1.0", + "description": "`pnpm create stanza` — the canonical entry point to the stanza wizard.", + "license": "MIT", + "bin": { + "create-stanza": "./src/bin.ts" + }, + "files": [ + "src", + "dist" + ], + "type": "module", + "scripts": { + "build": "bun build ./src/bin.ts --target=bun --outdir=./dist --minify", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@stanza/cli": "workspace:*", + "mri": "^1.2.0" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "typescript": "^6.0.3" + } +} diff --git a/packages/create-stanza/src/bin.ts b/packages/create-stanza/src/bin.ts new file mode 100644 index 0000000..fbbe3a0 --- /dev/null +++ b/packages/create-stanza/src/bin.ts @@ -0,0 +1,23 @@ +#!/usr/bin/env bun +/** + * `pnpm create stanza my-app` lands here. We forward straight to the CLI's + * init command — no extra logic, since the wizard wants to live in one place. + * + * The argv shape from npm's `create-` convention is the same as a normal CLI + * invocation, with the project name as the first positional arg. + */ +import mri from "mri"; +import { run } from "@stanza/cli"; + +const argv = mri(process.argv.slice(2), { + alias: { h: "help", v: "version" }, + boolean: ["help", "version", "yes", "dry-run", "no-telemetry"], +}); + +// Inject the `init` verb so the user-facing command stays terse. +argv._ = ["init", ...argv._]; + +run(argv).catch((err: unknown) => { + console.error(err instanceof Error ? err.message : String(err)); + process.exitCode = 1; +}); diff --git a/packages/create-stanza/tsconfig.json b/packages/create-stanza/tsconfig.json new file mode 100644 index 0000000..307afcd --- /dev/null +++ b/packages/create-stanza/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/registry-build/package.json b/packages/registry-build/package.json new file mode 100644 index 0000000..3e16eb5 --- /dev/null +++ b/packages/registry-build/package.json @@ -0,0 +1,19 @@ +{ + "name": "@stanza/registry-build", + "version": "0.1.0", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "registry:build": "bun run ./src/build.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@stanza/registry": "workspace:*" + }, + "devDependencies": { + "@types/bun": "^1.3.14", + "@types/node": "^25.9.1", + "typescript": "^6.0.3" + } +} diff --git a/packages/registry-build/src/build.ts b/packages/registry-build/src/build.ts new file mode 100644 index 0000000..8a63b94 --- /dev/null +++ b/packages/registry-build/src/build.ts @@ -0,0 +1,73 @@ +#!/usr/bin/env bun +/** + * Static registry build. Scans `registry/modules/*`, imports each module's + * default export, writes: + * - dist/registry/index.json — registry index (slot/module summaries) + * - dist/registry/modules/-.json — per-module full manifests + * + * The output directory is what gets uploaded to the CDN (Vercel) and what + * the CLI's HTTP loader hits at runtime. + */ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import type { Module, RegistryIndex } from "@stanza/registry"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = findRepoRoot(here); +const modulesDir = path.join(repoRoot, "registry", "modules"); +const outDir = path.join(repoRoot, "dist", "registry"); + +await main(); + +async function main() { + fs.mkdirSync(path.join(outDir, "modules"), { recursive: true }); + + const dirs = fs + .readdirSync(modulesDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name); + + const summaries = []; + for (const dir of dirs) { + const entry = path.join(modulesDir, dir, "module.ts"); + const mod = ((await import(entry)) as { default: Module }).default; + if (!mod) throw new Error(`Module ${dir} has no default export.`); + + fs.writeFileSync( + path.join(outDir, "modules", `${mod.slot}-${mod.id}.json`), + JSON.stringify(mod, null, 2), + ); + + summaries.push({ + ...mod, + adapters: mod.adapters.map((a) => ({ key: a.key, match: a.match })), + }); + } + + const index: RegistryIndex = { + generatedAt: new Date().toISOString(), + schemaVersion: 1, + slots: [ + { id: "framework", label: "Framework", description: "Web/native app framework." }, + { id: "styling", label: "Styling", description: "CSS / styling system." }, + { id: "db", label: "Database", description: "Database engine." }, + { id: "orm", label: "ORM", description: "Database query layer." }, + { id: "auth", label: "Auth", description: "Authentication provider." }, + ], + modules: summaries, + }; + + fs.writeFileSync(path.join(outDir, "index.json"), JSON.stringify(index, null, 2)); + + console.log(`Wrote ${summaries.length} modules to ${outDir}`); +} + +function findRepoRoot(start: string): string { + let dir = start; + for (let i = 0; i < 8; i++) { + if (fs.existsSync(path.join(dir, "pnpm-workspace.yaml"))) return dir; + dir = path.dirname(dir); + } + throw new Error("Could not locate repo root from " + start); +} diff --git a/packages/registry-build/tsconfig.json b/packages/registry-build/tsconfig.json new file mode 100644 index 0000000..307afcd --- /dev/null +++ b/packages/registry-build/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/registry/package.json b/packages/registry/package.json new file mode 100644 index 0000000..a7d6b9a --- /dev/null +++ b/packages/registry/package.json @@ -0,0 +1,30 @@ +{ + "name": "@stanza/registry", + "version": "0.1.0", + "license": "MIT", + "files": [ + "src", + "dist" + ], + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./manifest": "./src/manifest.ts", + "./module": "./src/module.ts", + "./resolver": "./src/resolver.ts" + }, + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "zod": "^4.4.3" + }, + "devDependencies": { + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } +} diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts new file mode 100644 index 0000000..11a709d --- /dev/null +++ b/packages/registry/src/index.ts @@ -0,0 +1,18 @@ +export type { + Slot, + SlotId, + ModuleId, + Capability, + PeerRequirement, + Module, + ModuleAdapter, + ModuleSummary, + RegistryIndex, +} from "./module.ts"; +export { defineModule, KNOWN_SLOTS } from "./module.ts"; + +export type { StanzaManifest, StanzaModuleRecord, RegionMap, RegionOwnership } from "./manifest.ts"; +export { StanzaManifestSchema, CURRENT_MANIFEST_VERSION, emptyManifest } from "./manifest.ts"; + +export type { ResolveContext, ResolveResult, ResolveError } from "./resolver.ts"; +export { resolveAdapter, isCompatible, slotOrder } from "./resolver.ts"; diff --git a/packages/registry/src/manifest.ts b/packages/registry/src/manifest.ts new file mode 100644 index 0000000..73509fd --- /dev/null +++ b/packages/registry/src/manifest.ts @@ -0,0 +1,75 @@ +import { z } from "zod"; +import { KNOWN_SLOTS, type ModuleId, type SlotId } from "./module.ts"; + +export const CURRENT_MANIFEST_VERSION = "0.1" as const; + +export type StanzaModuleRecord = { + id: ModuleId; + /** + * Pinned module version at install time. Recorded now so the upcoming + * `swap` and `update` verbs can read it; not consumed yet. + */ + version: string; + /** Adapter key chosen at install time (function of peer slots). */ + adapter: string; +}; + +/** + * Per-file region ownership. Keys are dot-paths inside the file + * (e.g. "imports", "providers", "dependencies.better-auth"). Values are + * the owning module id. Written today; the `swap`/`update` verbs (and the + * deeper `remove` reversal) will use it to scope codemods back to the + * regions a single module owns. + */ +export type RegionMap = Record; +export type RegionOwnership = Record; + +export type StanzaManifest = { + version: typeof CURRENT_MANIFEST_VERSION; + projectShape: "monorepo"; + packageManager: "pnpm" | "bun" | "npm"; + /** Display name; usually the repo root name. */ + name: string; + /** Path of the primary web/native app inside the monorepo. */ + appDir: string; + modules: Partial>; + regions: RegionOwnership; + /** Anonymous client id used by opt-out telemetry. Stored to dedupe events. */ + telemetryId?: string; +}; + +export const StanzaManifestSchema = z.object({ + version: z.literal(CURRENT_MANIFEST_VERSION), + projectShape: z.literal("monorepo"), + packageManager: z.enum(["pnpm", "bun", "npm"]), + name: z.string(), + appDir: z.string(), + // Zod 4: `z.record` over a finite key type requires all keys to be present. + // We want partial — not every slot needs to be filled — so use partialRecord. + modules: z.partialRecord( + z.enum(KNOWN_SLOTS), + z.object({ + id: z.string(), + version: z.string(), + adapter: z.string(), + }), + ), + regions: z.record(z.string(), z.record(z.string(), z.string())), + telemetryId: z.string().optional(), +}) satisfies z.ZodType; + +export function emptyManifest(input: { + name: string; + appDir?: string; + packageManager?: StanzaManifest["packageManager"]; +}): StanzaManifest { + return { + version: CURRENT_MANIFEST_VERSION, + projectShape: "monorepo", + packageManager: input.packageManager ?? "pnpm", + name: input.name, + appDir: input.appDir ?? "apps/web", + modules: {}, + regions: {}, + }; +} diff --git a/packages/registry/src/module.ts b/packages/registry/src/module.ts new file mode 100644 index 0000000..5e53ebc --- /dev/null +++ b/packages/registry/src/module.ts @@ -0,0 +1,162 @@ +import { z } from "zod"; + +export const KNOWN_SLOTS = ["framework", "orm", "db", "auth", "styling"] as const; + +export type SlotId = (typeof KNOWN_SLOTS)[number]; + +export type Slot = { + id: SlotId; + label: string; + description: string; +}; + +export type ModuleId = string; + +export type Capability = "web" | "native" | "react" | "node" | "edge" | "ssr" | "rsc"; + +export type PeerRequirement = { + [K in SlotId]?: ModuleId[] | "any"; +}; + +export type ModuleAdapter = { + /** Stable key — composite of peer choices that selected this adapter. */ + key: string; + /** Which peer module(s) this adapter handles. Empty record means "default / no peer". */ + match: Partial>; + /** Templates copied verbatim into the project (relative dest path → registry-relative source). */ + templates?: TemplateRef[]; + /** + * Codemod IDs to run, in order. The CLI looks these up in the module's + * codemod registry (loaded lazily so the registry index stays JSON-serializable). + */ + codemods?: string[]; + /** npm dependencies this adapter adds (name → semver range). */ + dependencies?: Record; + devDependencies?: Record; + /** Environment variables to add to `.env.example`. */ + env?: EnvVar[]; + /** package.json `scripts` to merge into the host app. */ + scripts?: Record; +}; + +export type TemplateRef = { + /** Path inside the module's templates/ folder. */ + src: string; + /** Path in the generated project (relative to repo root or app root). */ + dest: string; + /** + * Where the dest is resolved against: + * - "repo": repo root + * - "app": the active framework app dir (apps/web by default) + */ + scope?: "repo" | "app"; + /** If true, run as a template (mustache-style) with the manifest as context. */ + template?: boolean; +}; + +export type EnvVar = { + name: string; + /** Example value placed in `.env.example`. */ + example: string; + /** Required for the module to work, vs. nice-to-have. */ + required: boolean; + description?: string; +}; + +export type Module = { + id: ModuleId; + slot: SlotId; + /** Display name in the wizard / web builder. */ + label: string; + description: string; + /** Module schema version — pinned in stanza.json. */ + version: string; + /** Tags this module provides to peers downstream. */ + provides?: Capability[]; + /** Capabilities this module needs from the project. */ + requires?: Capability[]; + /** Peer module slots this module needs filled, with optional allow-list. */ + peers?: PeerRequirement; + /** + * Concrete install recipes keyed by adapter. The resolver picks one based on + * the project's other modules. + */ + adapters: ModuleAdapter[]; + /** Optional homepage / docs URL surfaced in `stanza search`. */ + homepage?: string; + /** Optional maintainer attribution. */ + author?: string; +}; + +/** + * Lightweight summary suitable for the registry index — strips the codemod + * implementations but keeps everything needed for the wizard / search UI. + */ +export type ModuleSummary = Omit & { + adapters: Pick[]; +}; + +export type RegistryIndex = { + generatedAt: string; + schemaVersion: 1; + slots: Slot[]; + modules: ModuleSummary[]; +}; + +/** + * Identity helper for module manifests — gives full inference and IDE + * autocomplete without forcing a class hierarchy. The runtime cost is zero. + */ +export function defineModule(module: Module): Module { + return module; +} + +// Runtime-validatable schema for third-party / fetched manifests. +// +// Zod 4 requires `z.record(K, V)` — the 1-arg form is gone. We use +// `z.string()` for keys everywhere except `peers`, which keys on SlotId. +export const ModuleSchema = z.object({ + id: z.string(), + slot: z.enum(KNOWN_SLOTS), + label: z.string(), + description: z.string(), + version: z.string(), + provides: z.array(z.enum(["web", "native", "react", "node", "edge", "ssr", "rsc"])).optional(), + requires: z.array(z.enum(["web", "native", "react", "node", "edge", "ssr", "rsc"])).optional(), + // Zod 4: partialRecord because not every slot is constrained by a module. + peers: z + .partialRecord(z.enum(KNOWN_SLOTS), z.union([z.literal("any"), z.array(z.string())])) + .optional(), + adapters: z.array( + z.object({ + key: z.string(), + match: z.record(z.string(), z.string()), + templates: z + .array( + z.object({ + src: z.string(), + dest: z.string(), + scope: z.enum(["repo", "app"]).optional(), + template: z.boolean().optional(), + }), + ) + .optional(), + codemods: z.array(z.string()).optional(), + dependencies: z.record(z.string(), z.string()).optional(), + devDependencies: z.record(z.string(), z.string()).optional(), + env: z + .array( + z.object({ + name: z.string(), + example: z.string(), + required: z.boolean(), + description: z.string().optional(), + }), + ) + .optional(), + scripts: z.record(z.string(), z.string()).optional(), + }), + ), + homepage: z.string().optional(), + author: z.string().optional(), +}) satisfies z.ZodType; diff --git a/packages/registry/src/resolver.test.ts b/packages/registry/src/resolver.test.ts new file mode 100644 index 0000000..fd4de3c --- /dev/null +++ b/packages/registry/src/resolver.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from "vitest"; +import { defineModule, type Module } from "./module.ts"; +import { emptyManifest } from "./manifest.ts"; +import { resolveAdapter } from "./resolver.ts"; + +const drizzle: Module = defineModule({ + id: "drizzle", + slot: "orm", + label: "Drizzle", + description: "", + version: "0.1.0", + peers: { db: ["postgres", "sqlite"] }, + adapters: [ + { key: "postgres", match: { db: "postgres" } }, + { key: "sqlite", match: { db: "sqlite" } }, + ], +}); + +const betterAuth: Module = defineModule({ + id: "better-auth", + slot: "auth", + label: "Better Auth", + description: "", + version: "0.1.0", + peers: { orm: ["drizzle", "prisma"] }, + adapters: [ + { key: "drizzle", match: { orm: "drizzle" } }, + { key: "prisma", match: { orm: "prisma" } }, + ], +}); + +describe("resolveAdapter", () => { + it("picks the adapter whose match aligns with active peers", () => { + const result = resolveAdapter(drizzle, { + manifest: emptyManifest({ name: "t" }), + pending: { + db: defineModule({ + id: "postgres", + slot: "db", + label: "", + description: "", + version: "0.1.0", + adapters: [{ key: "default", match: {} }], + }), + }, + }); + expect(result.ok).toBe(true); + if (result.ok) expect(result.adapter.key).toBe("postgres"); + }); + + it("fails fast when a required peer is missing", () => { + const result = resolveAdapter(betterAuth, { + manifest: emptyManifest({ name: "t" }), + pending: {}, + }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error.kind).toBe("missing-peer"); + }); + + it("rejects a peer not on the allow-list", () => { + const result = resolveAdapter(betterAuth, { + manifest: emptyManifest({ name: "t" }), + pending: { + orm: defineModule({ + id: "typeorm", + slot: "orm", + label: "", + description: "", + version: "0.1.0", + adapters: [{ key: "default", match: {} }], + }), + }, + }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error.kind).toBe("incompatible-peer"); + }); + + it("falls back to a default (empty-match) adapter when no peers are required", () => { + const tailwind: Module = defineModule({ + id: "tailwind", + slot: "styling", + label: "", + description: "", + version: "0.1.0", + adapters: [{ key: "default", match: {} }], + }); + const result = resolveAdapter(tailwind, { + manifest: emptyManifest({ name: "t" }), + pending: {}, + }); + expect(result.ok).toBe(true); + if (result.ok) expect(result.adapter.key).toBe("default"); + }); +}); diff --git a/packages/registry/src/resolver.ts b/packages/registry/src/resolver.ts new file mode 100644 index 0000000..b81e020 --- /dev/null +++ b/packages/registry/src/resolver.ts @@ -0,0 +1,99 @@ +import { KNOWN_SLOTS, type Module, type ModuleAdapter, type SlotId } from "./module.ts"; +import type { StanzaManifest } from "./manifest.ts"; + +/** + * Topological order slots are processed in. Earlier slots become peer + * candidates for later ones. Hardcoded for now since the dependency graph + * is small and fixed; can be derived from peer declarations later. + */ +export const slotOrder: readonly SlotId[] = ["framework", "styling", "db", "orm", "auth"]; + +export type ResolveContext = { + /** Manifest state at the moment of resolution (post any pending picks). */ + manifest: StanzaManifest; + /** Modules the user has already chosen this run but not yet committed. */ + pending: Partial>; +}; + +export type ResolveError = + | { kind: "no-adapter"; module: Module; peers: Partial> } + | { kind: "missing-peer"; module: Module; slot: SlotId } + | { kind: "incompatible-peer"; module: Module; slot: SlotId; peer: string }; + +export type ResolveResult = + | { ok: true; adapter: ModuleAdapter } + | { ok: false; error: ResolveError }; + +/** + * Pick the most-specific adapter that matches the active peer choices. + * + * Specificity = number of `match` keys satisfied. The default (empty `match`) + * always wins on tiebreak when no peers are required. + */ +export function resolveAdapter(module: Module, context: ResolveContext): ResolveResult { + const activePeers = activePeerIds(context); + + // Check declared peers are satisfied (id present + on allow-list if specified). + for (const slot of KNOWN_SLOTS) { + const allowed = module.peers?.[slot]; + if (allowed === undefined) continue; + const chosen = activePeers[slot]; + if (!chosen) { + return { ok: false, error: { kind: "missing-peer", module, slot } }; + } + if (allowed !== "any" && !allowed.includes(chosen)) { + return { + ok: false, + error: { kind: "incompatible-peer", module, slot, peer: chosen }, + }; + } + } + + const candidates = module.adapters + .map((adapter) => ({ + adapter, + specificity: matchSpecificity(adapter, activePeers), + })) + .filter((c) => c.specificity >= 0); + + if (candidates.length === 0) { + return { + ok: false, + error: { kind: "no-adapter", module, peers: activePeers }, + }; + } + + candidates.sort((a, b) => b.specificity - a.specificity); + // Non-null asserted: candidates.length > 0 was just checked above. + return { ok: true, adapter: candidates[0]!.adapter }; +} + +export function isCompatible(module: Module, context: ResolveContext): boolean { + return resolveAdapter(module, context).ok; +} + +function activePeerIds(context: ResolveContext): Partial> { + const out: Partial> = {}; + for (const slot of KNOWN_SLOTS) { + const pending = context.pending[slot]?.id; + const installed = context.manifest.modules[slot]?.id; + const chosen = pending ?? installed; + if (chosen) out[slot] = chosen; + } + return out; +} + +/** + * Returns -1 if the adapter is impossible (declares a peer match the active + * peers contradict). Otherwise returns the number of matched constraints. + * Adapters with an empty `match` map are universally applicable (specificity 0). + */ +function matchSpecificity(adapter: ModuleAdapter, peers: Partial>): number { + let score = 0; + for (const [slot, required] of Object.entries(adapter.match) as [SlotId, string][]) { + const actual = peers[slot]; + if (actual !== required) return -1; + score += 1; + } + return score; +} diff --git a/packages/registry/tsconfig.json b/packages/registry/tsconfig.json new file mode 100644 index 0000000..792172f --- /dev/null +++ b/packages/registry/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..234e66c --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2774 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^25.9.1 + version: 25.9.1 + oxfmt: + specifier: ^0.51.0 + version: 0.51.0 + oxlint: + specifier: ^1.66.0 + version: 1.66.0 + turbo: + specifier: ^2.9.14 + version: 2.9.14 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + + apps/cli: + dependencies: + '@clack/prompts': + specifier: ^1.4.0 + version: 1.4.0 + '@stanza/codemods': + specifier: workspace:* + version: link:../../packages/codemods + '@stanza/registry': + specifier: workspace:* + version: link:../../packages/registry + kleur: + specifier: ^4.1.5 + version: 4.1.5 + mri: + specifier: ^1.2.0 + version: 1.2.0 + posthog-node: + specifier: ^5.34.7 + version: 5.34.7 + semver: + specifier: ^7.8.0 + version: 7.8.0 + devDependencies: + '@types/node': + specifier: ^25.9.1 + version: 25.9.1 + '@types/semver': + specifier: ^7.7.1 + version: 7.7.1 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.7 + version: 4.1.7(@types/node@25.9.1)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)) + + apps/web: + dependencies: + '@stanza/registry': + specifier: workspace:* + version: link:../../packages/registry + '@tanstack/react-router': + specifier: ^1.170.5 + version: 1.170.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-start': + specifier: ^1.168.7 + version: 1.168.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)) + react: + specifier: ^19.2.6 + version: 19.2.6 + react-dom: + specifier: ^19.2.6 + version: 19.2.6(react@19.2.6) + devDependencies: + '@types/react': + specifier: ^19.2.15 + version: 19.2.15 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.15) + '@vitejs/plugin-react': + specifier: ^6.0.2 + version: 6.0.2(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)) + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite: + specifier: ^8.0.13 + version: 8.0.13(@types/node@25.9.1)(jiti@2.7.0) + + packages/codemods: + dependencies: + '@stanza/registry': + specifier: workspace:* + version: link:../registry + ts-morph: + specifier: ^28.0.0 + version: 28.0.0 + devDependencies: + '@types/node': + specifier: ^25.9.1 + version: 25.9.1 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.7 + version: 4.1.7(@types/node@25.9.1)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)) + + packages/create-stanza: + dependencies: + '@stanza/cli': + specifier: workspace:* + version: link:../../apps/cli + mri: + specifier: ^1.2.0 + version: 1.2.0 + devDependencies: + '@types/node': + specifier: ^25.9.1 + version: 25.9.1 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + + packages/registry: + dependencies: + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.7 + version: 4.1.7(@types/node@25.9.1)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)) + + packages/registry-build: + dependencies: + '@stanza/registry': + specifier: workspace:* + version: link:../registry + devDependencies: + '@types/bun': + specifier: ^1.3.14 + version: 1.3.14 + '@types/node': + specifier: ^25.9.1 + version: 25.9.1 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + + registry/modules/auth-better-auth: + dependencies: + '@stanza/codemods': + specifier: workspace:* + version: link:../../../packages/codemods + '@stanza/registry': + specifier: workspace:* + version: link:../../../packages/registry + + registry/modules/auth-clerk: + dependencies: + '@stanza/codemods': + specifier: workspace:* + version: link:../../../packages/codemods + '@stanza/registry': + specifier: workspace:* + version: link:../../../packages/registry + + registry/modules/db-postgres: + dependencies: + '@stanza/codemods': + specifier: workspace:* + version: link:../../../packages/codemods + '@stanza/registry': + specifier: workspace:* + version: link:../../../packages/registry + + registry/modules/db-sqlite: + dependencies: + '@stanza/codemods': + specifier: workspace:* + version: link:../../../packages/codemods + '@stanza/registry': + specifier: workspace:* + version: link:../../../packages/registry + + registry/modules/framework-next: + dependencies: + '@stanza/codemods': + specifier: workspace:* + version: link:../../../packages/codemods + '@stanza/registry': + specifier: workspace:* + version: link:../../../packages/registry + + registry/modules/framework-tanstack-start: + dependencies: + '@stanza/codemods': + specifier: workspace:* + version: link:../../../packages/codemods + '@stanza/registry': + specifier: workspace:* + version: link:../../../packages/registry + + registry/modules/orm-drizzle: + dependencies: + '@stanza/codemods': + specifier: workspace:* + version: link:../../../packages/codemods + '@stanza/registry': + specifier: workspace:* + version: link:../../../packages/registry + + registry/modules/orm-prisma: + dependencies: + '@stanza/codemods': + specifier: workspace:* + version: link:../../../packages/codemods + '@stanza/registry': + specifier: workspace:* + version: link:../../../packages/registry + + registry/modules/styling-tailwind: + dependencies: + '@stanza/codemods': + specifier: workspace:* + version: link:../../../packages/codemods + '@stanza/registry': + specifier: workspace:* + version: link:../../../packages/registry + +packages: + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@clack/core@1.3.1': + resolution: {integrity: sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.4.0': + resolution: {integrity: sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==} + engines: {node: '>= 20.12.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oozcitak/dom@2.0.2': + resolution: {integrity: sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==} + engines: {node: '>=20.0'} + + '@oozcitak/infra@2.0.2': + resolution: {integrity: sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==} + engines: {node: '>=20.0'} + + '@oozcitak/url@3.0.0': + resolution: {integrity: sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==} + engines: {node: '>=20.0'} + + '@oozcitak/util@10.0.0': + resolution: {integrity: sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==} + engines: {node: '>=20.0'} + + '@oxc-project/types@0.130.0': + resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==} + + '@oxfmt/binding-android-arm-eabi@0.51.0': + resolution: {integrity: sha512-Ni0sCqg5CIHaLIYFGj+ncbcumylvNC6FE4rfD0KfdmnWHbPJ+zev0qZCXKxy2hFVa0fYRK0yPzf5nzPbkZou7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.51.0': + resolution: {integrity: sha512-eu5lAZjuo0KAkp+M24EhDqfOwA8owQ8d7wyBlOUUGRbDLHpU3IRlDHp8Dif+YqGlxs6jra7yS6WQu/NkPhAxeg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.51.0': + resolution: {integrity: sha512-6LsUNIdURhhcIfIn8+xsOb61mSTa9msAHTeSGx9Jf4rsP/gN8PGCF+SKWPAQZbND2w/WBkqQ6303jqEEIXzMdQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.51.0': + resolution: {integrity: sha512-9aUMGmVxdHjYMsEAW1tNRoieTJXlVNDFkRvIR1J7LttJXWjVYCu2ekclLij2KJtxBxSQOYSHd12ME/adVGVbZg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.51.0': + resolution: {integrity: sha512-mkY1nhZTqYb+NHaAWxOCKISN6FwdrwMNsu17vTUA3wzUV2VJ+Paq15ZokRcsMU/2PUdHO73prxyeJpjXQ3MPpQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.51.0': + resolution: {integrity: sha512-wtFwNwE4+YCNuPaWoGDZeGsKvD6D1YSUNBJNn/rJBh7CrDBThFE+TBI5kY7vRW9rIOQRsbW2IpyyL3Du4Zqwiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.51.0': + resolution: {integrity: sha512-rnOaNx86G7iRKM6lsCIQMux0SMGNC/TEbFR+r7lpruJ12bnrIWgxd5w1PLqOvgR9r8ZJbpK/zfRKctJnh8/Jfg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.51.0': + resolution: {integrity: sha512-jOgDzSqWcICGRjsp4mc08FxKMN8vzP2Kgs4E0d2HUP99F+nJDQKklRV4Zuj+0gcBgjrzx2CbpqaIdUVPepCojA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxfmt/binding-linux-arm64-musl@0.51.0': + resolution: {integrity: sha512-KBUCdrH5bwVrAvI9gU/1S55oH6fzXjr++J/oVocdu7bYTks1l7DNNT+rLd/1TDdAEjObGwmfWamn7LC1m8A0DQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxfmt/binding-linux-ppc64-gnu@0.51.0': + resolution: {integrity: sha512-NapfjYsABFqTJ1Dn9Efq6sN5esaHconVKwVLbDGNQLrwpOx/g17mkwErHzU72PutL67nf3wNAkbq122H+zLxag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxfmt/binding-linux-riscv64-gnu@0.51.0': + resolution: {integrity: sha512-5dlDt1dUZCVi6elIhiK1PWg9wpTzTcIuj0IZnSurvIoMrhOWqqTcc1dSTxcSkNaBZhfsNqRZdINI1zAgbKkJNQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxfmt/binding-linux-riscv64-musl@0.51.0': + resolution: {integrity: sha512-pgdWUJn0S5nulyiVdlFV8DzCUnGXkU99W5PSkkmbaZW+LrZBPxpezun4G0DDHbQaVYuJeCuKsXsGKGo77CkUTQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxfmt/binding-linux-s390x-gnu@0.51.0': + resolution: {integrity: sha512-2XTFUe97CbDGAI8vjwDfZ1HdakO0XIADyJ24idEg64SC4/K4in/OisXVnrW4NMK7I6TgC7EqRhC0Ln/nKhAemA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxfmt/binding-linux-x64-gnu@0.51.0': + resolution: {integrity: sha512-kQ1OuCqqt/yyf0ZN9VFxW1/JnlgJgii3Dr7pWf9vNBvrX1hv6g39/+mc5oGRHRGJFZtl3zsGDWR9c5N2B/gwBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxfmt/binding-linux-x64-musl@0.51.0': + resolution: {integrity: sha512-ARTYqxHF475o96Gbn41hvSWSSRygPlRDXZZgZ9I2scU1y0qiWpCQyZCoefaQa0mwv+wwtZ+luS4YOzsRzM/izg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxfmt/binding-openharmony-arm64@0.51.0': + resolution: {integrity: sha512-QiC1XrCl6a6BmqMzduO8hdIRMf1m44hCkt2Q68KWkTvUB/E7fd2iomyNh6KnnRca5w6eBrRAAtLFqTh+xjsjJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.51.0': + resolution: {integrity: sha512-NC/hJb9dtU23Zf8L7IVK95xnFjiQ7AfcLO2l5pb69TDEr958qxrtnB2CveeeNSCBFNIkgaTCfd/vHNSoG78l9g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.51.0': + resolution: {integrity: sha512-2C45za4Rj36n8YIbhRL1PQbxmXJYf81WEcAgvj5I4ptRROG+A+81hREEN5bmCHADE1UfYaN312U6tkILoZZy6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.51.0': + resolution: {integrity: sha512-73RqdAuVKQTkjZIDw08JaDHUM4lav5Qu+CaPwg4QbbA7k8o7LEW0p3UsfZ/F8dsO/pwVYh3RzFcanwLRTTahbQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.66.0': + resolution: {integrity: sha512-f7kq8N51T4phpzqfBpA2qaVTI/KrkCmNwaj3t/97I/WLTDI+UhlP5GL9eER+zVxBhtlx5rKXWByJU1/zDAvyaw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.66.0': + resolution: {integrity: sha512-xu6QO71tdDS9mjmLZ3AqhtaVHBvdmsOKkYnReNNDgh+XiwnsipeQOIxbiYOOO0iAXycJ+GK0wdMSZP/2j/AmSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.66.0': + resolution: {integrity: sha512-HZ24VimSOC7mxuEA99e0H2FS0C1yO3+iW13jPRAk+e2njsUs3QeAXsafCDyaIrV/MirdOVez+etQNQsJE43zNQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.66.0': + resolution: {integrity: sha512-awhj8ZvJrrRSnXj7V++rpZvTmnl99L6mi0B7gg7Cp7BN6cKpzuI481bHNLvXGA9GB1/oEgA3ponuyoAc6Md12A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.66.0': + resolution: {integrity: sha512-KQF0oVV21/FjIqkRuL8Q1vh8ECsE5+ocdH5tcqTQ4ZnYuDVoYibQUNfqBjQaUsP6UIIda5Y75Wpm5p4RgQWiWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.66.0': + resolution: {integrity: sha512-9u1rgwZSEXWb30vbFZzQ78HVXBo0WCKNwJ3a2InRUTNMRng+PUDIoSFmA+m4HdUfBaIqftShq8J8qHc+eE/Vig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.66.0': + resolution: {integrity: sha512-Ynot2HR1bHxUaNWoC280MVTDfZuaWuP3XfSMRDhyuZrVjhzoaBCVFlw8h8qeZjWKVUBhPWFIxB7AQTlK8Z2WWg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.66.0': + resolution: {integrity: sha512-xCbgzciGgo+A4aQZEknsNrNiIwY7sU5SfRuMmRjPIvZAgdF34cIHiKvwOsS5XRLjlTVSFwitmq6YclTtHTfU+g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxlint/binding-linux-arm64-musl@1.66.0': + resolution: {integrity: sha512-hmo+ZB/lHkR1HdDmnziNpzSLmulnUSu10VEqX2Yex7OwvoBAbjJQLvy4gIBRV3AAwWnCvAxKp5Nv1GE6LU1QMg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxlint/binding-linux-ppc64-gnu@1.66.0': + resolution: {integrity: sha512-2Invd4Uyy81mVooQC5FBtfxSNrvcX1OxbMlVQ6M2erRrNI2awFYF26YNW2yFxdVFZ4ffNOWKghtMjhnUPsXsVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxlint/binding-linux-riscv64-gnu@1.66.0': + resolution: {integrity: sha512-s0iXPDQVdgayE3RGa/N2DZF7tjgg0TwEtD1sGoDxqPDGrIXgo45H0yHknT0f9A0yteASsweYZtDyTuVlM4aSag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-riscv64-musl@1.66.0': + resolution: {integrity: sha512-OekL4XFiu7RPK0JIZi8VeHgtIXPREf42t8Cy/rKEsC+P3gcqDgNAAGiyuUOpdbG4wwbfue1q4CHcCO7spSve6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-s390x-gnu@1.66.0': + resolution: {integrity: sha512-Ga1D0kj1SFslm34ThA/BdkUlyAYEnTsXyRC4pF0C5agZSwtGdHYWMTQWemUfBGp4RCG4QWXgdO+HmmmKqOtlBg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxlint/binding-linux-x64-gnu@1.66.0': + resolution: {integrity: sha512-p5jfP1wUZe/IC3qpQO84n9DRnf9g3lKRtLBlQq23ykyrDglHcVx7sWmVTlPuU6SBw8mNnPzyOn022G3XZHnlww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxlint/binding-linux-x64-musl@1.66.0': + resolution: {integrity: sha512-vUB/sYlYZorDL1ZD+o9mRv7zbsykrrFRtmgS6R8musZqLtrPRQn1gc1eGpuX+sfdccz42STl/AqldY6XRb2upQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxlint/binding-openharmony-arm64@1.66.0': + resolution: {integrity: sha512-yde+6p/F59xRkGR9H1HfngWRif1QRJjynZK349l+UI0H6w9hL3G8/AVaTHFyTtLVQ56qtNbX2/5Dc77n1ovnOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.66.0': + resolution: {integrity: sha512-O9GLucgoTdmOrbBX+EjzNe7o/Ze5TFOvXcib6bzUOtBOmj6cV+zw18NgB+cGKAkDw1Pdqs8vGkfHbbsLuDtXWg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.66.0': + resolution: {integrity: sha512-m3Pjwc2MfTcom4E4gOv7DyuGyt7OfGNCbmqDHd+N7EzXmP+ppHuudm2NjcA3AjV5TSeGxaguVF4SbTKHe1USYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.66.0': + resolution: {integrity: sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@posthog/core@1.29.6': + resolution: {integrity: sha512-qLA/4xTzxG1FabliYjsOy5pTC9XZWA225MHnpCmqGBoDVGL4sKKgLixMK2dpsHZbSo6AHpBLUfqkvTsh2YxZcQ==} + + '@posthog/types@1.374.3': + resolution: {integrity: sha512-AewLXVP/JR0iUTcY3wkYeDomNDAEWagX6g+39U61HyYcnWOjxzEVPsMGV2VdVjaAP2lRtkIOc00EzoIc9fIZ+Q==} + + '@rolldown/binding-android-arm64@1.0.1': + resolution: {integrity: sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.1': + resolution: {integrity: sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.1': + resolution: {integrity: sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.1': + resolution: {integrity: sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.1': + resolution: {integrity: sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.1': + resolution: {integrity: sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.1': + resolution: {integrity: sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.1': + resolution: {integrity: sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.1': + resolution: {integrity: sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.1': + resolution: {integrity: sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.1': + resolution: {integrity: sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.1': + resolution: {integrity: sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.1': + resolution: {integrity: sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.1': + resolution: {integrity: sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.1': + resolution: {integrity: sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tanstack/history@1.162.0': + resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==} + engines: {node: '>=20.19'} + + '@tanstack/react-router@1.170.5': + resolution: {integrity: sha512-SML7KhtehzfuoflBWhlUf3SA30bdk82cyoMet2ZZDK3fW4x/dKx5LKNwMUp8TOqUOTW7FW+Rfle9Ae/FswwsZA==} + engines: {node: '>=20.19'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-start-client@1.168.0': + resolution: {integrity: sha512-U7OhNoGOqJO/IRYdn93hlz1qe5PU9rLIh1oO5Ag0Ll9CKdNrACCsO/SVkdimo88h/YJ8oRaJstd4sge4F8xk3w==} + engines: {node: '>=22.12.0'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-start-rsc@0.1.7': + resolution: {integrity: sha512-guFvVUNxsNNw3T0szam7kXyLgKgFLYLw1x5W4kLU084hSdSZCxbOE0XS6q7+fWCt+KfNTqoQ1cFq88QEMzLZBA==} + engines: {node: '>=22.12.0'} + peerDependencies: + '@rspack/core': '>=2.0.0-0' + '@vitejs/plugin-rsc': '>=0.5.20' + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + react-server-dom-rspack: '>=0.0.2' + peerDependenciesMeta: + '@rspack/core': + optional: true + '@vitejs/plugin-rsc': + optional: true + react-server-dom-rspack: + optional: true + + '@tanstack/react-start-server@1.167.5': + resolution: {integrity: sha512-RZEJVSa1+MER/L+8F5Z2a5YzLOLaoyMlTkqE168wzIzNERRUZW/NiHZRvxX76Idd1kFr5eIh7FZWONELK4mXBQ==} + engines: {node: '>=22.12.0'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-start@1.168.7': + resolution: {integrity: sha512-qecAnWFFldYtg27ldqa0bcW6Gk5snVjKHGT6PlUh/9RLJW9CIXWMiYmLl/EuM/t7Qzb7I9G5AbzRbxYjIjXrzg==} + engines: {node: '>=22.12.0'} + peerDependencies: + '@rsbuild/core': ^2.0.0 + '@vitejs/plugin-rsc': '*' + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + vite: '>=7.0.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@vitejs/plugin-rsc': + optional: true + vite: + optional: true + + '@tanstack/react-store@0.9.3': + resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/router-core@1.171.3': + resolution: {integrity: sha512-CbaA38aiV1SC8N0ZAMs8P2PZPoNATim7lq8zhKhno5i5+iriaH6PQMrixYLrq3yhjAOwWhN3bj/4DCjPyHqHnA==} + engines: {node: '>=20.19'} + + '@tanstack/router-generator@1.167.6': + resolution: {integrity: sha512-0G7ETmSo0AleOIM2u8J6Oxyf2y5R7FIn3kAr86ItS2nbNDhHyA0Sj+Ij3VH5YjJKT2uurqYg1Bx++dU20CFN7g==} + engines: {node: '>=20.19'} + + '@tanstack/router-plugin@1.168.7': + resolution: {integrity: sha512-KqxXSEKM3XlhUKZjia073fk8SrCCpiyDlVmOyey+PxwpHOQsoNYbaTjFkTakXPxW0knphVyyUoWKiy4HrvZoww==} + engines: {node: '>=20.19'} + peerDependencies: + '@rsbuild/core': '>=1.0.2 || ^2.0.0' + '@tanstack/react-router': ^1.170.5 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0' + vite-plugin-solid: ^2.11.10 || ^3.0.0-0 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + + '@tanstack/router-utils@1.162.1': + resolution: {integrity: sha512-62layyTGmclHDQS/eidwKRfN1hhCKwViG7iEBcVmL0MXgcAB3OOucWCEcDDGd9Cu11H6b4QQ5oOo47MWIqwz0A==} + engines: {node: '>=20.19'} + + '@tanstack/start-client-core@1.170.0': + resolution: {integrity: sha512-0tAyjFoSDm3EyIPOyz21uPBS82UWWwiZtBK+Tyc4kqNkzdvoCH21ywdWDYK9wCHdoSTm1m1KEWOLm31gtiVdoQ==} + engines: {node: '>=22.12.0'} + + '@tanstack/start-fn-stubs@1.162.0': + resolution: {integrity: sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ==} + engines: {node: '>=22.12.0'} + + '@tanstack/start-plugin-core@1.171.0': + resolution: {integrity: sha512-Zwuy+AwXO5SyOhJmIOn7rfFPLCADMNiPnEO+hfJA4RAdZjB7z3IBRvGPSGviohaAM4HUs8tZyEYQPS0l2TLXzg==} + engines: {node: '>=22.12.0'} + peerDependencies: + '@rsbuild/core': ^2.0.0 + vite: '>=7.0.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + vite: + optional: true + + '@tanstack/start-server-core@1.169.0': + resolution: {integrity: sha512-wHZU9ph2r59Zwfnr5xigfIL7BzbQ8w8f4eesY/OdnxoWEVTvpWPjbDLJNH9L+c4U1F1GFPeg5b3r12MhGGejDg==} + engines: {node: '>=22.12.0'} + + '@tanstack/start-storage-context@1.167.5': + resolution: {integrity: sha512-CkxYbrn7Sm8nOBmNNdYbEtXKBhxy3Em6KKSClKs78Lfsn1W2YY+l4KZ1doLzVkAUOWKsUYULWMS/5U9m8FQwQA==} + engines: {node: '>=22.12.0'} + + '@tanstack/store@0.9.3': + resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} + + '@tanstack/virtual-file-routes@1.162.0': + resolution: {integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==} + engines: {node: '>=20.19'} + + '@ts-morph/common@0.29.0': + resolution: {integrity: sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==} + + '@turbo/darwin-64@2.9.14': + resolution: {integrity: sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.9.14': + resolution: {integrity: sha512-d23147mC9BsCPA9mJ0h/ubcpbRgcJBXbcG3+Vq7YLhjz3IXuvQsJ1UXH8f4MD76ZjJ4m/E4aRdJV+MW88CDfbw==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.9.14': + resolution: {integrity: sha512-P3ZKB5tuUDdDQWuAsACGUR1qv9W7BNWxdxqVJ0kZNuNNPRaVYTPPikLcp79+GiEcW3npsR+KyP38lnQiBc5aSA==} + cpu: [x64] + os: [linux] + + '@turbo/linux-arm64@2.9.14': + resolution: {integrity: sha512-ZRTlzcUMrrPv9ZuDzRF9n60Ym13bKeG9jDB8WjxyLhWNzV+AJQN+zdpIk3NJYf2zQsGUm1mNar2P0elRzLw25g==} + cpu: [arm64] + os: [linux] + + '@turbo/windows-64@2.9.14': + resolution: {integrity: sha512-exanwN6sIduZwykYeiTQj8kCmOhazP5WOz3bvXMcYtjhL6Z3iRWLewKrXCBq0bqwSP3iBMb/AerRCnHI4lx46A==} + cpu: [x64] + os: [win32] + + '@turbo/windows-arm64@2.9.14': + resolution: {integrity: sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g==} + cpu: [arm64] + os: [win32] + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/bun@1.3.14': + resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.15': + resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} + + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + + '@vitejs/plugin-react@6.0.2': + resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + '@vitest/expect@4.1.7': + resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} + + '@vitest/mocker@4.1.7': + resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.7': + resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + + '@vitest/runner@4.1.7': + resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} + + '@vitest/snapshot@4.1.7': + resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} + + '@vitest/spy@4.1.7': + resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} + + '@vitest/utils@4.1.7': + resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + + ansis@4.3.0: + resolution: {integrity: sha512-44mvgtPvohuU/70DdY5Oz2AIrLJ9k6/5x4KmoSvPwO+5Moijo0+N9D0fKbbYZQWP1hNm5CpOf+E01jhxG/r8xg==} + engines: {node: '>=14'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + babel-dead-code-elimination@1.0.12: + resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.31: + resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bun-types@1.3.14: + resolution: {integrity: sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==} + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.2.0: + resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} + engines: {node: '>=20.18.1'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + electron-to-chromium@1.5.360: + resolution: {integrity: sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==} + + encoding-sniffer@0.2.1: + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetchdts@0.1.7: + resolution: {integrity: sha512-YoZjBdafyLIop9lSxXVI33oLD5kN31q4Td+CasofLLYeLXRFeOsuOw0Uo+XNRi9PZlbfdlN2GmRtm4tCEQ9/KA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + h3@2.0.1-rc.20: + resolution: {integrity: sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==} + engines: {node: '>=20.11.1'} + hasBin: true + peerDependencies: + crossws: ^0.4.1 + peerDependenciesMeta: + crossws: + optional: true + + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + isbot@5.1.40: + resolution: {integrity: sha512-yNeeynhhtIVRBk12tBV4eHNxwB42HzR4Q3Ea7vCOiJhImGaAIdIMrbJtacQlBizGLjUPw+akkFI5Dn9T70XoVQ==} + engines: {node: '>=18'} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.44: + resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + oxfmt@0.51.0: + resolution: {integrity: sha512-l/AoAnaEOV7Q5/Z9kHOMDehVJnCgYN7wRoooWCTUMBMi16BJhLZqd9cmCnwcVFfVlzkt53zK2KLPFNp8vSsoDg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + oxlint@1.66.0: + resolution: {integrity: sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.22.1' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + posthog-node@5.34.7: + resolution: {integrity: sha512-OwZ7k6KnJqvhda+CeZnj0fStHmSiKSpHlbFYnC8thn+fwkMgDRi6L1FVfscbrC+c226I78u91LJq6MTAt0qrCw==} + engines: {node: ^20.20.0 || >=22.22.0} + peerDependencies: + rxjs: ^7.0.0 + peerDependenciesMeta: + rxjs: + optional: true + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + rolldown@1.0.1: + resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rou3@0.8.1: + resolution: {integrity: sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + + seroval-plugins@1.5.4: + resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.4: + resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==} + engines: {node: '>=10'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + srvx@0.11.15: + resolution: {integrity: sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg==} + engines: {node: '>=20.16.0'} + hasBin: true + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + ts-morph@28.0.0: + resolution: {integrity: sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + turbo@2.9.14: + resolution: {integrity: sha512-BQqXRr4UoWI3UPFrtznCLykYHxwxWh53iCB57x092jPMjIlW1wnm3N895g5irpiXmnxUhREBB0n6+y8BHhs4nw==} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vite@8.0.13: + resolution: {integrity: sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@4.1.7: + resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.7 + '@vitest/browser-preview': 4.1.7 + '@vitest/browser-webdriverio': 4.1.7 + '@vitest/coverage-istanbul': 4.1.7 + '@vitest/coverage-v8': 4.1.7 + '@vitest/ui': 4.1.7 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + xmlbuilder2@4.0.3: + resolution: {integrity: sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==} + engines: {node: '>=20.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@clack/core@1.3.1': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@clack/prompts@1.4.0': + dependencies: + '@clack/core': 1.3.1 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@oozcitak/dom@2.0.2': + dependencies: + '@oozcitak/infra': 2.0.2 + '@oozcitak/url': 3.0.0 + '@oozcitak/util': 10.0.0 + + '@oozcitak/infra@2.0.2': + dependencies: + '@oozcitak/util': 10.0.0 + + '@oozcitak/url@3.0.0': + dependencies: + '@oozcitak/infra': 2.0.2 + '@oozcitak/util': 10.0.0 + + '@oozcitak/util@10.0.0': {} + + '@oxc-project/types@0.130.0': {} + + '@oxfmt/binding-android-arm-eabi@0.51.0': + optional: true + + '@oxfmt/binding-android-arm64@0.51.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.51.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.51.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.51.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.51.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.51.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.51.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.51.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.51.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.51.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.51.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.51.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.51.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.51.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.51.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.51.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.51.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.51.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.66.0': + optional: true + + '@oxlint/binding-android-arm64@1.66.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.66.0': + optional: true + + '@oxlint/binding-darwin-x64@1.66.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.66.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.66.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.66.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.66.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.66.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.66.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.66.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.66.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.66.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.66.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.66.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.66.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.66.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.66.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.66.0': + optional: true + + '@posthog/core@1.29.6': + dependencies: + '@posthog/types': 1.374.3 + + '@posthog/types@1.374.3': {} + + '@rolldown/binding-android-arm64@1.0.1': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.1': + optional: true + + '@rolldown/binding-darwin-x64@1.0.1': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.1': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.1': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.1': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.1': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.1': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.1': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.1': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.1': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.1': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.1': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.1': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.1': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@tanstack/history@1.162.0': {} + + '@tanstack/react-router@1.170.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@tanstack/history': 1.162.0 + '@tanstack/react-store': 0.9.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/router-core': 1.171.3 + isbot: 5.1.40 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@tanstack/react-start-client@1.168.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@tanstack/react-router': 1.170.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/router-core': 1.171.3 + '@tanstack/start-client-core': 1.170.0 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + '@tanstack/react-start-rsc@0.1.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0))': + dependencies: + '@tanstack/react-router': 1.170.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-start-server': 1.167.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/router-core': 1.171.3 + '@tanstack/router-utils': 1.162.1 + '@tanstack/start-client-core': 1.170.0 + '@tanstack/start-fn-stubs': 1.162.0 + '@tanstack/start-plugin-core': 1.171.0(@tanstack/react-router@1.170.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)) + '@tanstack/start-server-core': 1.169.0 + '@tanstack/start-storage-context': 1.167.5 + pathe: 2.0.3 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + transitivePeerDependencies: + - '@rsbuild/core' + - crossws + - supports-color + - vite + - vite-plugin-solid + - webpack + + '@tanstack/react-start-server@1.167.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@tanstack/history': 1.162.0 + '@tanstack/react-router': 1.170.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/router-core': 1.171.3 + '@tanstack/start-client-core': 1.170.0 + '@tanstack/start-server-core': 1.169.0 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + transitivePeerDependencies: + - crossws + + '@tanstack/react-start@1.168.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0))': + dependencies: + '@tanstack/react-router': 1.170.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-start-client': 1.168.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/react-start-rsc': 0.1.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)) + '@tanstack/react-start-server': 1.167.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/router-utils': 1.162.1 + '@tanstack/start-client-core': 1.170.0 + '@tanstack/start-plugin-core': 1.171.0(@tanstack/react-router@1.170.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)) + '@tanstack/start-server-core': 1.169.0 + pathe: 2.0.3 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0) + transitivePeerDependencies: + - '@rspack/core' + - crossws + - react-server-dom-rspack + - supports-color + - vite-plugin-solid + - webpack + + '@tanstack/react-store@0.9.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@tanstack/store': 0.9.3 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + use-sync-external-store: 1.6.0(react@19.2.6) + + '@tanstack/router-core@1.171.3': + dependencies: + '@tanstack/history': 1.162.0 + cookie-es: 3.1.1 + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) + + '@tanstack/router-generator@1.167.6': + dependencies: + '@babel/types': 7.29.0 + '@tanstack/router-core': 1.171.3 + '@tanstack/router-utils': 1.162.1 + '@tanstack/virtual-file-routes': 1.162.0 + jiti: 2.7.0 + magic-string: 0.30.21 + prettier: 3.8.3 + zod: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.168.7(@tanstack/react-router@1.170.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@tanstack/router-core': 1.171.3 + '@tanstack/router-generator': 1.167.6 + '@tanstack/router-utils': 1.162.1 + '@tanstack/virtual-file-routes': 1.162.0 + chokidar: 5.0.0 + unplugin: 3.0.0 + zod: 4.4.3 + optionalDependencies: + '@tanstack/react-router': 1.170.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0) + transitivePeerDependencies: + - supports-color + + '@tanstack/router-utils@1.162.1': + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + ansis: 4.3.0 + babel-dead-code-elimination: 1.0.12 + diff: 8.0.4 + pathe: 2.0.3 + tinyglobby: 0.2.16 + transitivePeerDependencies: + - supports-color + + '@tanstack/start-client-core@1.170.0': + dependencies: + '@tanstack/router-core': 1.171.3 + '@tanstack/start-fn-stubs': 1.162.0 + '@tanstack/start-storage-context': 1.167.5 + seroval: 1.5.4 + + '@tanstack/start-fn-stubs@1.162.0': {} + + '@tanstack/start-plugin-core@1.171.0(@tanstack/react-router@1.170.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0))': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/core': 7.29.0 + '@babel/types': 7.29.0 + '@rolldown/pluginutils': 1.0.1 + '@tanstack/router-core': 1.171.3 + '@tanstack/router-generator': 1.167.6 + '@tanstack/router-plugin': 1.168.7(@tanstack/react-router@1.170.5(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)) + '@tanstack/router-utils': 1.162.1 + '@tanstack/start-client-core': 1.170.0 + '@tanstack/start-server-core': 1.169.0 + cheerio: 1.2.0 + exsolve: 1.0.8 + lightningcss: 1.32.0 + pathe: 2.0.3 + picomatch: 4.0.4 + seroval: 1.5.4 + source-map: 0.7.6 + srvx: 0.11.15 + tinyglobby: 0.2.16 + ufo: 1.6.4 + vitefu: 1.1.3(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)) + xmlbuilder2: 4.0.3 + zod: 4.4.3 + optionalDependencies: + vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0) + transitivePeerDependencies: + - '@tanstack/react-router' + - crossws + - supports-color + - vite-plugin-solid + - webpack + + '@tanstack/start-server-core@1.169.0': + dependencies: + '@tanstack/history': 1.162.0 + '@tanstack/router-core': 1.171.3 + '@tanstack/start-client-core': 1.170.0 + '@tanstack/start-storage-context': 1.167.5 + fetchdts: 0.1.7 + h3-v2: h3@2.0.1-rc.20 + seroval: 1.5.4 + transitivePeerDependencies: + - crossws + + '@tanstack/start-storage-context@1.167.5': + dependencies: + '@tanstack/router-core': 1.171.3 + + '@tanstack/store@0.9.3': {} + + '@tanstack/virtual-file-routes@1.162.0': {} + + '@ts-morph/common@0.29.0': + dependencies: + minimatch: 10.2.5 + path-browserify: 1.0.1 + tinyglobby: 0.2.16 + + '@turbo/darwin-64@2.9.14': + optional: true + + '@turbo/darwin-arm64@2.9.14': + optional: true + + '@turbo/linux-64@2.9.14': + optional: true + + '@turbo/linux-arm64@2.9.14': + optional: true + + '@turbo/windows-64@2.9.14': + optional: true + + '@turbo/windows-arm64@2.9.14': + optional: true + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/bun@1.3.14': + dependencies: + bun-types: 1.3.14 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@25.9.1': + dependencies: + undici-types: 7.24.6 + + '@types/react-dom@19.2.3(@types/react@19.2.15)': + dependencies: + '@types/react': 19.2.15 + + '@types/react@19.2.15': + dependencies: + csstype: 3.2.3 + + '@types/semver@7.7.1': {} + + '@vitejs/plugin-react@6.0.2(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0) + + '@vitest/expect@4.1.7': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.7(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0))': + dependencies: + '@vitest/spy': 4.1.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0) + + '@vitest/pretty-format@4.1.7': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.7': + dependencies: + '@vitest/utils': 4.1.7 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.7': + dependencies: + '@vitest/pretty-format': 4.1.7 + '@vitest/utils': 4.1.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.7': {} + + '@vitest/utils@4.1.7': + dependencies: + '@vitest/pretty-format': 4.1.7 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + ansis@4.3.0: {} + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + babel-dead-code-elimination@1.0.12: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.31: {} + + boolbase@1.0.0: {} + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.31 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.360 + node-releases: 2.0.44 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + bun-types@1.3.14: + dependencies: + '@types/node': 25.9.1 + + caniuse-lite@1.0.30001793: {} + + chai@6.2.2: {} + + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.2.2 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@1.2.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + encoding-sniffer: 0.2.1 + htmlparser2: 10.1.0 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 7.25.0 + whatwg-mimetype: 4.0.0 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + code-block-writer@13.0.3: {} + + convert-source-map@2.0.0: {} + + cookie-es@3.1.1: {} + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.2.2: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-libc@2.1.2: {} + + diff@8.0.4: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + electron-to-chromium@1.5.360: {} + + encoding-sniffer@0.2.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + + entities@4.5.0: {} + + entities@6.0.1: {} + + entities@7.0.1: {} + + es-module-lexer@2.1.0: {} + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.3.0: {} + + exsolve@1.0.8: {} + + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fetchdts@0.1.7: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + h3@2.0.1-rc.20: + dependencies: + rou3: 0.8.1 + srvx: 0.11.15 + + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + isbot@5.1.40: {} + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + kleur@4.1.5: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + mri@1.2.0: {} + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + node-releases@2.0.44: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + obug@2.1.1: {} + + oxfmt@0.51.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.51.0 + '@oxfmt/binding-android-arm64': 0.51.0 + '@oxfmt/binding-darwin-arm64': 0.51.0 + '@oxfmt/binding-darwin-x64': 0.51.0 + '@oxfmt/binding-freebsd-x64': 0.51.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.51.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.51.0 + '@oxfmt/binding-linux-arm64-gnu': 0.51.0 + '@oxfmt/binding-linux-arm64-musl': 0.51.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.51.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.51.0 + '@oxfmt/binding-linux-riscv64-musl': 0.51.0 + '@oxfmt/binding-linux-s390x-gnu': 0.51.0 + '@oxfmt/binding-linux-x64-gnu': 0.51.0 + '@oxfmt/binding-linux-x64-musl': 0.51.0 + '@oxfmt/binding-openharmony-arm64': 0.51.0 + '@oxfmt/binding-win32-arm64-msvc': 0.51.0 + '@oxfmt/binding-win32-ia32-msvc': 0.51.0 + '@oxfmt/binding-win32-x64-msvc': 0.51.0 + + oxlint@1.66.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.66.0 + '@oxlint/binding-android-arm64': 1.66.0 + '@oxlint/binding-darwin-arm64': 1.66.0 + '@oxlint/binding-darwin-x64': 1.66.0 + '@oxlint/binding-freebsd-x64': 1.66.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.66.0 + '@oxlint/binding-linux-arm-musleabihf': 1.66.0 + '@oxlint/binding-linux-arm64-gnu': 1.66.0 + '@oxlint/binding-linux-arm64-musl': 1.66.0 + '@oxlint/binding-linux-ppc64-gnu': 1.66.0 + '@oxlint/binding-linux-riscv64-gnu': 1.66.0 + '@oxlint/binding-linux-riscv64-musl': 1.66.0 + '@oxlint/binding-linux-s390x-gnu': 1.66.0 + '@oxlint/binding-linux-x64-gnu': 1.66.0 + '@oxlint/binding-linux-x64-musl': 1.66.0 + '@oxlint/binding-openharmony-arm64': 1.66.0 + '@oxlint/binding-win32-arm64-msvc': 1.66.0 + '@oxlint/binding-win32-ia32-msvc': 1.66.0 + '@oxlint/binding-win32-x64-msvc': 1.66.0 + + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.3.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-browserify@1.0.1: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + posthog-node@5.34.7: + dependencies: + '@posthog/core': 1.29.6 + + prettier@3.8.3: {} + + react-dom@19.2.6(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + + react@19.2.6: {} + + readdirp@5.0.0: {} + + rolldown@1.0.1: + dependencies: + '@oxc-project/types': 0.130.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.1 + '@rolldown/binding-darwin-arm64': 1.0.1 + '@rolldown/binding-darwin-x64': 1.0.1 + '@rolldown/binding-freebsd-x64': 1.0.1 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.1 + '@rolldown/binding-linux-arm64-gnu': 1.0.1 + '@rolldown/binding-linux-arm64-musl': 1.0.1 + '@rolldown/binding-linux-ppc64-gnu': 1.0.1 + '@rolldown/binding-linux-s390x-gnu': 1.0.1 + '@rolldown/binding-linux-x64-gnu': 1.0.1 + '@rolldown/binding-linux-x64-musl': 1.0.1 + '@rolldown/binding-openharmony-arm64': 1.0.1 + '@rolldown/binding-wasm32-wasi': 1.0.1 + '@rolldown/binding-win32-arm64-msvc': 1.0.1 + '@rolldown/binding-win32-x64-msvc': 1.0.1 + + rou3@0.8.1: {} + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.8.0: {} + + seroval-plugins@1.5.4(seroval@1.5.4): + dependencies: + seroval: 1.5.4 + + seroval@1.5.4: {} + + siginfo@2.0.0: {} + + sisteransi@1.0.5: {} + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + srvx@0.11.15: {} + + stackback@0.0.2: {} + + std-env@4.1.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.1.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@2.1.0: {} + + tinyrainbow@3.1.0: {} + + ts-morph@28.0.0: + dependencies: + '@ts-morph/common': 0.29.0 + code-block-writer: 13.0.3 + + tslib@2.8.1: + optional: true + + turbo@2.9.14: + optionalDependencies: + '@turbo/darwin-64': 2.9.14 + '@turbo/darwin-arm64': 2.9.14 + '@turbo/linux-64': 2.9.14 + '@turbo/linux-arm64': 2.9.14 + '@turbo/windows-64': 2.9.14 + '@turbo/windows-arm64': 2.9.14 + + typescript@6.0.3: {} + + ufo@1.6.4: {} + + undici-types@7.24.6: {} + + undici@7.25.0: {} + + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.6.0(react@19.2.6): + dependencies: + react: 19.2.6 + + vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.1 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.9.1 + fsevents: 2.3.3 + jiti: 2.7.0 + + vitefu@1.1.3(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)): + optionalDependencies: + vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0) + + vitest@4.1.7(@types/node@25.9.1)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)): + dependencies: + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.9.1 + transitivePeerDependencies: + - msw + + webpack-virtual-modules@0.6.2: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + xmlbuilder2@4.0.3: + dependencies: + '@oozcitak/dom': 2.0.2 + '@oozcitak/infra': 2.0.2 + '@oozcitak/util': 10.0.0 + js-yaml: 4.1.1 + + yallist@3.1.1: {} + + zod@4.4.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..6d9e406 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +packages: + - "apps/*" + - "packages/*" + - "registry/modules/*" + +onlyBuiltDependencies: + - "esbuild" + - "better-sqlite3" + +ignoredOptionalDependencies: [] diff --git a/registry/modules/auth-better-auth/module.ts b/registry/modules/auth-better-auth/module.ts new file mode 100644 index 0000000..39ca1e1 --- /dev/null +++ b/registry/modules/auth-better-auth/module.ts @@ -0,0 +1,111 @@ +import { defineModule } from "@stanza/registry"; + +export default defineModule({ + id: "better-auth", + slot: "auth", + label: "Better Auth", + description: "Framework-agnostic, headless TypeScript auth library.", + version: "0.1.0", + peers: { orm: ["drizzle", "prisma"], framework: ["next", "tanstack-start"] }, + homepage: "https://better-auth.com", + adapters: [ + { + key: "next+drizzle", + match: { framework: "next", orm: "drizzle" }, + dependencies: { "better-auth": "^1.6.11" }, + env: [ + { + name: "BETTER_AUTH_SECRET", + example: "change-me-in-prod", + required: true, + description: "Better Auth signing secret.", + }, + { + name: "BETTER_AUTH_URL", + example: "http://localhost:3000", + required: true, + description: "Public URL of the app.", + }, + ], + templates: [ + { src: "next/auth.drizzle.ts", dest: "src/lib/auth.ts", scope: "app" }, + { src: "next/auth-client.ts", dest: "src/lib/auth-client.ts", scope: "app" }, + { src: "next/route.ts", dest: "app/api/auth/[...all]/route.ts", scope: "app" }, + { src: "shared/auth-schema.drizzle.ts", dest: "src/db/auth-schema.ts", scope: "app" }, + ], + }, + { + key: "next+prisma", + match: { framework: "next", orm: "prisma" }, + dependencies: { "better-auth": "^1.6.11" }, + env: [ + { + name: "BETTER_AUTH_SECRET", + example: "change-me-in-prod", + required: true, + description: "Better Auth signing secret.", + }, + { + name: "BETTER_AUTH_URL", + example: "http://localhost:3000", + required: true, + description: "Public URL of the app.", + }, + ], + templates: [ + { src: "next/auth.prisma.ts", dest: "src/lib/auth.ts", scope: "app" }, + { src: "next/auth-client.ts", dest: "src/lib/auth-client.ts", scope: "app" }, + { src: "next/route.ts", dest: "app/api/auth/[...all]/route.ts", scope: "app" }, + ], + }, + { + key: "tanstack-start+drizzle", + match: { framework: "tanstack-start", orm: "drizzle" }, + dependencies: { "better-auth": "^1.6.11" }, + env: [ + { + name: "BETTER_AUTH_SECRET", + example: "change-me-in-prod", + required: true, + description: "Better Auth signing secret.", + }, + { + name: "BETTER_AUTH_URL", + example: "http://localhost:3000", + required: true, + description: "Public URL of the app.", + }, + ], + templates: [ + { src: "tanstack/auth.drizzle.ts", dest: "src/lib/auth.ts", scope: "app" }, + { src: "tanstack/auth-client.ts", dest: "src/lib/auth-client.ts", scope: "app" }, + { src: "tanstack/api.ts", dest: "src/routes/api/auth/$.ts", scope: "app" }, + { src: "shared/auth-schema.drizzle.ts", dest: "src/db/auth-schema.ts", scope: "app" }, + ], + }, + { + key: "tanstack-start+prisma", + match: { framework: "tanstack-start", orm: "prisma" }, + dependencies: { "better-auth": "^1.6.11" }, + env: [ + { + name: "BETTER_AUTH_SECRET", + example: "change-me-in-prod", + required: true, + description: "Better Auth signing secret.", + }, + { + name: "BETTER_AUTH_URL", + example: "http://localhost:3000", + required: true, + description: "Public URL of the app.", + }, + ], + templates: [ + { src: "tanstack/auth.prisma.ts", dest: "src/lib/auth.ts", scope: "app" }, + { src: "tanstack/auth-client.ts", dest: "src/lib/auth-client.ts", scope: "app" }, + { src: "tanstack/api.ts", dest: "src/routes/api/auth/$.ts", scope: "app" }, + ], + }, + ], +}); diff --git a/registry/modules/auth-better-auth/package.json b/registry/modules/auth-better-auth/package.json new file mode 100644 index 0000000..f518aa4 --- /dev/null +++ b/registry/modules/auth-better-auth/package.json @@ -0,0 +1,12 @@ +{ + "name": "@stanza-modules/auth-better-auth", + "version": "0.1.0", + "private": true, + "license": "MIT", + "type": "module", + "main": "./module.ts", + "dependencies": { + "@stanza/codemods": "workspace:*", + "@stanza/registry": "workspace:*" + } +} diff --git a/registry/modules/auth-better-auth/templates/next/auth-client.ts b/registry/modules/auth-better-auth/templates/next/auth-client.ts new file mode 100644 index 0000000..134f063 --- /dev/null +++ b/registry/modules/auth-better-auth/templates/next/auth-client.ts @@ -0,0 +1,5 @@ +import { createAuthClient } from "better-auth/react"; + +export const authClient = createAuthClient({ + baseURL: process.env.BETTER_AUTH_URL, +}); diff --git a/registry/modules/auth-better-auth/templates/next/auth.drizzle.ts b/registry/modules/auth-better-auth/templates/next/auth.drizzle.ts new file mode 100644 index 0000000..47288e2 --- /dev/null +++ b/registry/modules/auth-better-auth/templates/next/auth.drizzle.ts @@ -0,0 +1,8 @@ +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { db } from "@/db"; + +export const auth = betterAuth({ + database: drizzleAdapter(db, { provider: "pg" }), + emailAndPassword: { enabled: true }, +}); diff --git a/registry/modules/auth-better-auth/templates/next/auth.prisma.ts b/registry/modules/auth-better-auth/templates/next/auth.prisma.ts new file mode 100644 index 0000000..3251912 --- /dev/null +++ b/registry/modules/auth-better-auth/templates/next/auth.prisma.ts @@ -0,0 +1,8 @@ +import { betterAuth } from "better-auth"; +import { prismaAdapter } from "better-auth/adapters/prisma"; +import { db } from "@/db"; + +export const auth = betterAuth({ + database: prismaAdapter(db, { provider: "postgresql" }), + emailAndPassword: { enabled: true }, +}); diff --git a/registry/modules/auth-better-auth/templates/next/route.ts b/registry/modules/auth-better-auth/templates/next/route.ts new file mode 100644 index 0000000..5b67b06 --- /dev/null +++ b/registry/modules/auth-better-auth/templates/next/route.ts @@ -0,0 +1,4 @@ +import { auth } from "@/lib/auth"; +import { toNextJsHandler } from "better-auth/next-js"; + +export const { GET, POST } = toNextJsHandler(auth); diff --git a/registry/modules/auth-better-auth/templates/shared/auth-schema.drizzle.ts b/registry/modules/auth-better-auth/templates/shared/auth-schema.drizzle.ts new file mode 100644 index 0000000..d0abcad --- /dev/null +++ b/registry/modules/auth-better-auth/templates/shared/auth-schema.drizzle.ts @@ -0,0 +1,51 @@ +import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core"; + +export const user = pgTable("user", { + id: text("id").primaryKey(), + name: text("name").notNull(), + email: text("email").notNull().unique(), + emailVerified: boolean("email_verified").notNull().default(false), + image: text("image"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); + +export const session = pgTable("session", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + expiresAt: timestamp("expires_at").notNull(), + token: text("token").notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); + +export const account = pgTable("account", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + accountId: text("account_id").notNull(), + providerId: text("provider_id").notNull(), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + idToken: text("id_token"), + accessTokenExpiresAt: timestamp("access_token_expires_at"), + refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), + scope: text("scope"), + password: text("password"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); + +export const verification = pgTable("verification", { + id: text("id").primaryKey(), + identifier: text("identifier").notNull(), + value: text("value").notNull(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); diff --git a/registry/modules/auth-better-auth/templates/tanstack/api.ts b/registry/modules/auth-better-auth/templates/tanstack/api.ts new file mode 100644 index 0000000..7259886 --- /dev/null +++ b/registry/modules/auth-better-auth/templates/tanstack/api.ts @@ -0,0 +1,7 @@ +import { createAPIFileRoute } from "@tanstack/react-start/api"; +import { auth } from "~/lib/auth"; + +export const APIRoute = createAPIFileRoute("/api/auth/$")({ + GET: ({ request }) => auth.handler(request), + POST: ({ request }) => auth.handler(request), +}); diff --git a/registry/modules/auth-better-auth/templates/tanstack/auth-client.ts b/registry/modules/auth-better-auth/templates/tanstack/auth-client.ts new file mode 100644 index 0000000..126ccba --- /dev/null +++ b/registry/modules/auth-better-auth/templates/tanstack/auth-client.ts @@ -0,0 +1,5 @@ +import { createAuthClient } from "better-auth/react"; + +export const authClient = createAuthClient({ + baseURL: import.meta.env.VITE_BETTER_AUTH_URL, +}); diff --git a/registry/modules/auth-better-auth/templates/tanstack/auth.drizzle.ts b/registry/modules/auth-better-auth/templates/tanstack/auth.drizzle.ts new file mode 100644 index 0000000..00fdcc6 --- /dev/null +++ b/registry/modules/auth-better-auth/templates/tanstack/auth.drizzle.ts @@ -0,0 +1,8 @@ +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { db } from "~/db"; + +export const auth = betterAuth({ + database: drizzleAdapter(db, { provider: "pg" }), + emailAndPassword: { enabled: true }, +}); diff --git a/registry/modules/auth-better-auth/templates/tanstack/auth.prisma.ts b/registry/modules/auth-better-auth/templates/tanstack/auth.prisma.ts new file mode 100644 index 0000000..8fb4d4e --- /dev/null +++ b/registry/modules/auth-better-auth/templates/tanstack/auth.prisma.ts @@ -0,0 +1,8 @@ +import { betterAuth } from "better-auth"; +import { prismaAdapter } from "better-auth/adapters/prisma"; +import { db } from "~/db"; + +export const auth = betterAuth({ + database: prismaAdapter(db, { provider: "postgresql" }), + emailAndPassword: { enabled: true }, +}); diff --git a/registry/modules/auth-better-auth/tsconfig.json b/registry/modules/auth-better-auth/tsconfig.json new file mode 100644 index 0000000..8988b35 --- /dev/null +++ b/registry/modules/auth-better-auth/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["module.ts"], + "exclude": ["templates"] +} diff --git a/registry/modules/auth-clerk/module.ts b/registry/modules/auth-clerk/module.ts new file mode 100644 index 0000000..c972c85 --- /dev/null +++ b/registry/modules/auth-clerk/module.ts @@ -0,0 +1,42 @@ +import { defineModule } from "@stanza/registry"; + +/** + * Clerk ships its own hosted UI and session store; it bypasses the user-owned + * DB. That means it has no peer requirement on `orm`/`db` — those slots can be + * unfilled and Clerk still works. Only the Next.js adapter is wired up so + * far; TanStack Start integration is planned. + */ +export default defineModule({ + id: "clerk", + slot: "auth", + label: "Clerk", + description: "Hosted user management with pre-built UI components.", + version: "0.1.0", + peers: { framework: ["next"] }, + homepage: "https://clerk.com", + adapters: [ + { + key: "next", + match: { framework: "next" }, + dependencies: { "@clerk/nextjs": "^7.3.7" }, + env: [ + { + name: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", + example: "pk_test_...", + required: true, + description: "Clerk publishable key.", + }, + { + name: "CLERK_SECRET_KEY", + example: "sk_test_...", + required: true, + description: "Clerk secret key.", + }, + ], + templates: [ + { src: "middleware.ts", dest: "middleware.ts", scope: "app" }, + { src: "layout-wrapper.tsx", dest: "app/_clerk-provider.tsx", scope: "app" }, + ], + }, + ], +}); diff --git a/registry/modules/auth-clerk/package.json b/registry/modules/auth-clerk/package.json new file mode 100644 index 0000000..e3a4533 --- /dev/null +++ b/registry/modules/auth-clerk/package.json @@ -0,0 +1,12 @@ +{ + "name": "@stanza-modules/auth-clerk", + "version": "0.1.0", + "private": true, + "license": "MIT", + "type": "module", + "main": "./module.ts", + "dependencies": { + "@stanza/codemods": "workspace:*", + "@stanza/registry": "workspace:*" + } +} diff --git a/registry/modules/auth-clerk/templates/layout-wrapper.tsx b/registry/modules/auth-clerk/templates/layout-wrapper.tsx new file mode 100644 index 0000000..2d10164 --- /dev/null +++ b/registry/modules/auth-clerk/templates/layout-wrapper.tsx @@ -0,0 +1,6 @@ +import type { ReactNode } from "react"; +import { ClerkProvider } from "@clerk/nextjs"; + +export function ClerkRootProvider({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/registry/modules/auth-clerk/templates/middleware.ts b/registry/modules/auth-clerk/templates/middleware.ts new file mode 100644 index 0000000..7543980 --- /dev/null +++ b/registry/modules/auth-clerk/templates/middleware.ts @@ -0,0 +1,10 @@ +import { clerkMiddleware } from "@clerk/nextjs/server"; + +export default clerkMiddleware(); + +export const config = { + matcher: [ + "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", + "/(api|trpc)(.*)", + ], +}; diff --git a/registry/modules/auth-clerk/tsconfig.json b/registry/modules/auth-clerk/tsconfig.json new file mode 100644 index 0000000..8988b35 --- /dev/null +++ b/registry/modules/auth-clerk/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["module.ts"], + "exclude": ["templates"] +} diff --git a/registry/modules/db-postgres/module.ts b/registry/modules/db-postgres/module.ts new file mode 100644 index 0000000..0416342 --- /dev/null +++ b/registry/modules/db-postgres/module.ts @@ -0,0 +1,25 @@ +import { defineModule } from "@stanza/registry"; + +export default defineModule({ + id: "postgres", + slot: "db", + label: "PostgreSQL", + description: "Postgres via the `postgres` driver. Works locally or via Neon/Supabase.", + version: "0.1.0", + homepage: "https://www.postgresql.org", + adapters: [ + { + key: "default", + match: {}, + dependencies: { postgres: "^3.4.9" }, + env: [ + { + name: "DATABASE_URL", + example: "postgres://postgres:postgres@localhost:5432/stanza", + required: true, + description: "Postgres connection string.", + }, + ], + }, + ], +}); diff --git a/registry/modules/db-postgres/package.json b/registry/modules/db-postgres/package.json new file mode 100644 index 0000000..39b10bf --- /dev/null +++ b/registry/modules/db-postgres/package.json @@ -0,0 +1,12 @@ +{ + "name": "@stanza-modules/db-postgres", + "version": "0.1.0", + "private": true, + "license": "MIT", + "type": "module", + "main": "./module.ts", + "dependencies": { + "@stanza/codemods": "workspace:*", + "@stanza/registry": "workspace:*" + } +} diff --git a/registry/modules/db-postgres/tsconfig.json b/registry/modules/db-postgres/tsconfig.json new file mode 100644 index 0000000..8988b35 --- /dev/null +++ b/registry/modules/db-postgres/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["module.ts"], + "exclude": ["templates"] +} diff --git a/registry/modules/db-sqlite/module.ts b/registry/modules/db-sqlite/module.ts new file mode 100644 index 0000000..50be914 --- /dev/null +++ b/registry/modules/db-sqlite/module.ts @@ -0,0 +1,26 @@ +import { defineModule } from "@stanza/registry"; + +export default defineModule({ + id: "sqlite", + slot: "db", + label: "SQLite", + description: "Local SQLite via better-sqlite3. Zero-config for development.", + version: "0.1.0", + homepage: "https://www.sqlite.org", + adapters: [ + { + key: "default", + match: {}, + dependencies: { "better-sqlite3": "^12.10.0" }, + devDependencies: { "@types/better-sqlite3": "^7.6.13" }, + env: [ + { + name: "DATABASE_URL", + example: "file:./data/dev.db", + required: true, + description: "SQLite database file path.", + }, + ], + }, + ], +}); diff --git a/registry/modules/db-sqlite/package.json b/registry/modules/db-sqlite/package.json new file mode 100644 index 0000000..cf59544 --- /dev/null +++ b/registry/modules/db-sqlite/package.json @@ -0,0 +1,12 @@ +{ + "name": "@stanza-modules/db-sqlite", + "version": "0.1.0", + "private": true, + "license": "MIT", + "type": "module", + "main": "./module.ts", + "dependencies": { + "@stanza/codemods": "workspace:*", + "@stanza/registry": "workspace:*" + } +} diff --git a/registry/modules/db-sqlite/tsconfig.json b/registry/modules/db-sqlite/tsconfig.json new file mode 100644 index 0000000..8988b35 --- /dev/null +++ b/registry/modules/db-sqlite/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["module.ts"], + "exclude": ["templates"] +} diff --git a/registry/modules/framework-next/module.ts b/registry/modules/framework-next/module.ts new file mode 100644 index 0000000..3eaa50e --- /dev/null +++ b/registry/modules/framework-next/module.ts @@ -0,0 +1,41 @@ +import { defineModule } from "@stanza/registry"; + +export default defineModule({ + id: "next", + slot: "framework", + label: "Next.js", + description: "React framework with App Router, RSC, and edge runtime.", + version: "0.1.0", + provides: ["web", "react", "ssr", "rsc", "node", "edge"], + homepage: "https://nextjs.org", + adapters: [ + { + key: "default", + match: {}, + dependencies: { + next: "^16.2.6", + react: "^19.2.6", + "react-dom": "^19.2.6", + }, + devDependencies: { + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + typescript: "^6.0.3", + }, + scripts: { + dev: "next dev", + build: "next build", + start: "next start", + lint: "next lint", + }, + templates: [ + { src: "tsconfig.json", dest: "tsconfig.json", scope: "app" }, + { src: "next.config.ts", dest: "next.config.ts", scope: "app" }, + { src: "next-env.d.ts", dest: "next-env.d.ts", scope: "app" }, + { src: "app/layout.tsx", dest: "app/layout.tsx", scope: "app" }, + { src: "app/page.tsx", dest: "app/page.tsx", scope: "app" }, + { src: "app/globals.css", dest: "app/globals.css", scope: "app" }, + ], + }, + ], +}); diff --git a/registry/modules/framework-next/package.json b/registry/modules/framework-next/package.json new file mode 100644 index 0000000..8b32388 --- /dev/null +++ b/registry/modules/framework-next/package.json @@ -0,0 +1,12 @@ +{ + "name": "@stanza-modules/framework-next", + "version": "0.1.0", + "private": true, + "license": "MIT", + "type": "module", + "main": "./module.ts", + "dependencies": { + "@stanza/codemods": "workspace:*", + "@stanza/registry": "workspace:*" + } +} diff --git a/registry/modules/framework-next/templates/app/globals.css b/registry/modules/framework-next/templates/app/globals.css new file mode 100644 index 0000000..5df1bc8 --- /dev/null +++ b/registry/modules/framework-next/templates/app/globals.css @@ -0,0 +1,15 @@ +html, +body { + margin: 0; + padding: 0; + font-family: + system-ui, + -apple-system, + sans-serif; +} + +main { + max-width: 48rem; + margin: 4rem auto; + padding: 0 1rem; +} diff --git a/registry/modules/framework-next/templates/app/layout.tsx b/registry/modules/framework-next/templates/app/layout.tsx new file mode 100644 index 0000000..d15baac --- /dev/null +++ b/registry/modules/framework-next/templates/app/layout.tsx @@ -0,0 +1,15 @@ +import type { ReactNode } from "react"; +import "./globals.css"; + +export const metadata = { + title: "stanza app", + description: "Generated by stanza", +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/registry/modules/framework-next/templates/app/page.tsx b/registry/modules/framework-next/templates/app/page.tsx new file mode 100644 index 0000000..d2f6727 --- /dev/null +++ b/registry/modules/framework-next/templates/app/page.tsx @@ -0,0 +1,10 @@ +export default function Page() { + return ( +
+

Welcome to stanza

+

+ Edit app/page.tsx to get started. +

+
+ ); +} diff --git a/registry/modules/framework-next/templates/next-env.d.ts b/registry/modules/framework-next/templates/next-env.d.ts new file mode 100644 index 0000000..6080add --- /dev/null +++ b/registry/modules/framework-next/templates/next-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/registry/modules/framework-next/templates/next.config.ts b/registry/modules/framework-next/templates/next.config.ts new file mode 100644 index 0000000..3cefa3f --- /dev/null +++ b/registry/modules/framework-next/templates/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const config: NextConfig = { + reactStrictMode: true, +}; + +export default config; diff --git a/registry/modules/framework-next/templates/tsconfig.json b/registry/modules/framework-next/templates/tsconfig.json new file mode 100644 index 0000000..8fd8ca4 --- /dev/null +++ b/registry/modules/framework-next/templates/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "module": "esnext", + "moduleResolution": "bundler", + "jsx": "preserve", + "strict": true, + "noUncheckedIndexedAccess": true, + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "incremental": true, + "noEmit": true, + "plugins": [{ "name": "next" }], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/registry/modules/framework-next/tsconfig.json b/registry/modules/framework-next/tsconfig.json new file mode 100644 index 0000000..8988b35 --- /dev/null +++ b/registry/modules/framework-next/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["module.ts"], + "exclude": ["templates"] +} diff --git a/registry/modules/framework-tanstack-start/module.ts b/registry/modules/framework-tanstack-start/module.ts new file mode 100644 index 0000000..6589890 --- /dev/null +++ b/registry/modules/framework-tanstack-start/module.ts @@ -0,0 +1,42 @@ +import { defineModule } from "@stanza/registry"; + +export default defineModule({ + id: "tanstack-start", + slot: "framework", + label: "TanStack Start", + description: "Full-stack React framework on Vite + TanStack Router.", + version: "0.1.0", + provides: ["web", "react", "ssr", "node"], + homepage: "https://tanstack.com/start", + adapters: [ + { + key: "default", + match: {}, + dependencies: { + "@tanstack/react-router": "^1.170.5", + "@tanstack/react-start": "^1.168.7", + react: "^19.2.6", + "react-dom": "^19.2.6", + }, + devDependencies: { + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.2", + typescript: "^6.0.3", + vite: "^8.0.13", + }, + scripts: { + dev: "vite dev", + build: "vite build", + start: "node .output/server/index.mjs", + }, + templates: [ + { src: "vite.config.ts", dest: "vite.config.ts", scope: "app" }, + { src: "tsconfig.json", dest: "tsconfig.json", scope: "app" }, + { src: "src/router.tsx", dest: "src/router.tsx", scope: "app" }, + { src: "src/routes/__root.tsx", dest: "src/routes/__root.tsx", scope: "app" }, + { src: "src/routes/index.tsx", dest: "src/routes/index.tsx", scope: "app" }, + ], + }, + ], +}); diff --git a/registry/modules/framework-tanstack-start/package.json b/registry/modules/framework-tanstack-start/package.json new file mode 100644 index 0000000..649c7e1 --- /dev/null +++ b/registry/modules/framework-tanstack-start/package.json @@ -0,0 +1,12 @@ +{ + "name": "@stanza-modules/framework-tanstack-start", + "version": "0.1.0", + "private": true, + "license": "MIT", + "type": "module", + "main": "./module.ts", + "dependencies": { + "@stanza/codemods": "workspace:*", + "@stanza/registry": "workspace:*" + } +} diff --git a/registry/modules/framework-tanstack-start/templates/src/router.tsx b/registry/modules/framework-tanstack-start/templates/src/router.tsx new file mode 100644 index 0000000..1439068 --- /dev/null +++ b/registry/modules/framework-tanstack-start/templates/src/router.tsx @@ -0,0 +1,12 @@ +import { createRouter as createTanStackRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen"; + +export function createRouter() { + return createTanStackRouter({ routeTree }); +} + +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} diff --git a/registry/modules/framework-tanstack-start/templates/src/routes/__root.tsx b/registry/modules/framework-tanstack-start/templates/src/routes/__root.tsx new file mode 100644 index 0000000..c12629e --- /dev/null +++ b/registry/modules/framework-tanstack-start/templates/src/routes/__root.tsx @@ -0,0 +1,11 @@ +import { Outlet, createRootRoute } from "@tanstack/react-router"; + +export const Route = createRootRoute({ + component: () => ( + + + + + + ), +}); diff --git a/registry/modules/framework-tanstack-start/templates/src/routes/index.tsx b/registry/modules/framework-tanstack-start/templates/src/routes/index.tsx new file mode 100644 index 0000000..ade7a61 --- /dev/null +++ b/registry/modules/framework-tanstack-start/templates/src/routes/index.tsx @@ -0,0 +1,12 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/")({ + component: () => ( +
+

Welcome to stanza

+

+ Edit src/routes/index.tsx to get started. +

+
+ ), +}); diff --git a/registry/modules/framework-tanstack-start/templates/tsconfig.json b/registry/modules/framework-tanstack-start/templates/tsconfig.json new file mode 100644 index 0000000..35d166b --- /dev/null +++ b/registry/modules/framework-tanstack-start/templates/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "module": "esnext", + "moduleResolution": "bundler", + "jsx": "preserve", + "strict": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "allowJs": true, + "paths": { + "~/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "app.config.ts"], + "exclude": ["node_modules"] +} diff --git a/registry/modules/framework-tanstack-start/templates/vite.config.ts b/registry/modules/framework-tanstack-start/templates/vite.config.ts new file mode 100644 index 0000000..7649051 --- /dev/null +++ b/registry/modules/framework-tanstack-start/templates/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vite"; +import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [ + // tanstackStart() must precede react() — Start transforms server functions + // and generates the route tree first. + tanstackStart(), + react(), + ], +}); diff --git a/registry/modules/framework-tanstack-start/tsconfig.json b/registry/modules/framework-tanstack-start/tsconfig.json new file mode 100644 index 0000000..8988b35 --- /dev/null +++ b/registry/modules/framework-tanstack-start/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["module.ts"], + "exclude": ["templates"] +} diff --git a/registry/modules/orm-drizzle/module.ts b/registry/modules/orm-drizzle/module.ts new file mode 100644 index 0000000..88fab80 --- /dev/null +++ b/registry/modules/orm-drizzle/module.ts @@ -0,0 +1,45 @@ +import { defineModule } from "@stanza/registry"; + +export default defineModule({ + id: "drizzle", + slot: "orm", + label: "Drizzle", + description: "TypeScript-first ORM with schema-as-code and zero runtime overhead.", + version: "0.1.0", + peers: { db: ["postgres", "sqlite"] }, + homepage: "https://orm.drizzle.team", + adapters: [ + { + key: "postgres", + match: { db: "postgres" }, + dependencies: { "drizzle-orm": "^0.45.2" }, + devDependencies: { "drizzle-kit": "^0.31.10" }, + scripts: { + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio", + }, + templates: [ + { src: "drizzle.config.postgres.ts", dest: "drizzle.config.ts", scope: "app" }, + { src: "src/db/index.postgres.ts", dest: "src/db/index.ts", scope: "app" }, + { src: "src/db/schema.postgres.ts", dest: "src/db/schema.ts", scope: "app" }, + ], + }, + { + key: "sqlite", + match: { db: "sqlite" }, + dependencies: { "drizzle-orm": "^0.45.2" }, + devDependencies: { "drizzle-kit": "^0.31.10" }, + scripts: { + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio", + }, + templates: [ + { src: "drizzle.config.sqlite.ts", dest: "drizzle.config.ts", scope: "app" }, + { src: "src/db/index.sqlite.ts", dest: "src/db/index.ts", scope: "app" }, + { src: "src/db/schema.sqlite.ts", dest: "src/db/schema.ts", scope: "app" }, + ], + }, + ], +}); diff --git a/registry/modules/orm-drizzle/package.json b/registry/modules/orm-drizzle/package.json new file mode 100644 index 0000000..82afd53 --- /dev/null +++ b/registry/modules/orm-drizzle/package.json @@ -0,0 +1,12 @@ +{ + "name": "@stanza-modules/orm-drizzle", + "version": "0.1.0", + "private": true, + "license": "MIT", + "type": "module", + "main": "./module.ts", + "dependencies": { + "@stanza/codemods": "workspace:*", + "@stanza/registry": "workspace:*" + } +} diff --git a/registry/modules/orm-drizzle/templates/drizzle.config.postgres.ts b/registry/modules/orm-drizzle/templates/drizzle.config.postgres.ts new file mode 100644 index 0000000..df6df93 --- /dev/null +++ b/registry/modules/orm-drizzle/templates/drizzle.config.postgres.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/db/schema.ts", + out: "./drizzle", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/registry/modules/orm-drizzle/templates/drizzle.config.sqlite.ts b/registry/modules/orm-drizzle/templates/drizzle.config.sqlite.ts new file mode 100644 index 0000000..4e65401 --- /dev/null +++ b/registry/modules/orm-drizzle/templates/drizzle.config.sqlite.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/db/schema.ts", + out: "./drizzle", + dialect: "sqlite", + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/registry/modules/orm-drizzle/templates/src/db/index.postgres.ts b/registry/modules/orm-drizzle/templates/src/db/index.postgres.ts new file mode 100644 index 0000000..3a9ea19 --- /dev/null +++ b/registry/modules/orm-drizzle/templates/src/db/index.postgres.ts @@ -0,0 +1,7 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import * as schema from "./schema"; + +const client = postgres(process.env.DATABASE_URL!); +export const db = drizzle(client, { schema }); +export type Database = typeof db; diff --git a/registry/modules/orm-drizzle/templates/src/db/index.sqlite.ts b/registry/modules/orm-drizzle/templates/src/db/index.sqlite.ts new file mode 100644 index 0000000..c3fcc55 --- /dev/null +++ b/registry/modules/orm-drizzle/templates/src/db/index.sqlite.ts @@ -0,0 +1,8 @@ +import { drizzle } from "drizzle-orm/better-sqlite3"; +import Database from "better-sqlite3"; +import * as schema from "./schema"; + +const url = process.env.DATABASE_URL!.replace(/^file:/, ""); +const client = new Database(url); +export const db = drizzle(client, { schema }); +export type Database = typeof db; diff --git a/registry/modules/orm-drizzle/templates/src/db/schema.postgres.ts b/registry/modules/orm-drizzle/templates/src/db/schema.postgres.ts new file mode 100644 index 0000000..1b83629 --- /dev/null +++ b/registry/modules/orm-drizzle/templates/src/db/schema.postgres.ts @@ -0,0 +1,10 @@ +import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; + +// Stanza-managed: this file is the canonical schema. Auth and other modules +// extend it via additional schema files in src/db/*.ts — drizzle picks them +// up automatically as long as they're re-exported from this file. + +export const _stanza = pgTable("_stanza", { + id: text("id").primaryKey(), + createdAt: timestamp("created_at").notNull().defaultNow(), +}); diff --git a/registry/modules/orm-drizzle/templates/src/db/schema.sqlite.ts b/registry/modules/orm-drizzle/templates/src/db/schema.sqlite.ts new file mode 100644 index 0000000..f875359 --- /dev/null +++ b/registry/modules/orm-drizzle/templates/src/db/schema.sqlite.ts @@ -0,0 +1,6 @@ +import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; + +export const _stanza = sqliteTable("_stanza", { + id: text("id").primaryKey(), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), +}); diff --git a/registry/modules/orm-drizzle/tsconfig.json b/registry/modules/orm-drizzle/tsconfig.json new file mode 100644 index 0000000..8988b35 --- /dev/null +++ b/registry/modules/orm-drizzle/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["module.ts"], + "exclude": ["templates"] +} diff --git a/registry/modules/orm-prisma/module.ts b/registry/modules/orm-prisma/module.ts new file mode 100644 index 0000000..bfcf638 --- /dev/null +++ b/registry/modules/orm-prisma/module.ts @@ -0,0 +1,43 @@ +import { defineModule } from "@stanza/registry"; + +export default defineModule({ + id: "prisma", + slot: "orm", + label: "Prisma", + description: "Type-safe ORM with migrations, studio, and broad DB support.", + version: "0.1.0", + peers: { db: ["postgres", "sqlite"] }, + homepage: "https://www.prisma.io", + adapters: [ + { + key: "postgres", + match: { db: "postgres" }, + dependencies: { "@prisma/client": "^7.8.0" }, + devDependencies: { prisma: "^7.8.0" }, + scripts: { + "db:generate": "prisma generate", + "db:migrate": "prisma migrate dev", + "db:studio": "prisma studio", + }, + templates: [ + { src: "prisma/schema.postgres.prisma", dest: "prisma/schema.prisma", scope: "app" }, + { src: "src/db.ts", dest: "src/db.ts", scope: "app" }, + ], + }, + { + key: "sqlite", + match: { db: "sqlite" }, + dependencies: { "@prisma/client": "^7.8.0" }, + devDependencies: { prisma: "^7.8.0" }, + scripts: { + "db:generate": "prisma generate", + "db:migrate": "prisma migrate dev", + "db:studio": "prisma studio", + }, + templates: [ + { src: "prisma/schema.sqlite.prisma", dest: "prisma/schema.prisma", scope: "app" }, + { src: "src/db.ts", dest: "src/db.ts", scope: "app" }, + ], + }, + ], +}); diff --git a/registry/modules/orm-prisma/package.json b/registry/modules/orm-prisma/package.json new file mode 100644 index 0000000..146b1b6 --- /dev/null +++ b/registry/modules/orm-prisma/package.json @@ -0,0 +1,12 @@ +{ + "name": "@stanza-modules/orm-prisma", + "version": "0.1.0", + "private": true, + "license": "MIT", + "type": "module", + "main": "./module.ts", + "dependencies": { + "@stanza/codemods": "workspace:*", + "@stanza/registry": "workspace:*" + } +} diff --git a/registry/modules/orm-prisma/templates/prisma/schema.postgres.prisma b/registry/modules/orm-prisma/templates/prisma/schema.postgres.prisma new file mode 100644 index 0000000..cc10721 --- /dev/null +++ b/registry/modules/orm-prisma/templates/prisma/schema.postgres.prisma @@ -0,0 +1,8 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} diff --git a/registry/modules/orm-prisma/templates/prisma/schema.sqlite.prisma b/registry/modules/orm-prisma/templates/prisma/schema.sqlite.prisma new file mode 100644 index 0000000..c2c01bb --- /dev/null +++ b/registry/modules/orm-prisma/templates/prisma/schema.sqlite.prisma @@ -0,0 +1,8 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} diff --git a/registry/modules/orm-prisma/templates/src/db.ts b/registry/modules/orm-prisma/templates/src/db.ts new file mode 100644 index 0000000..7f696f6 --- /dev/null +++ b/registry/modules/orm-prisma/templates/src/db.ts @@ -0,0 +1,7 @@ +import { PrismaClient } from "@prisma/client"; + +const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }; + +export const db = globalForPrisma.prisma ?? new PrismaClient(); + +if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db; diff --git a/registry/modules/orm-prisma/tsconfig.json b/registry/modules/orm-prisma/tsconfig.json new file mode 100644 index 0000000..8988b35 --- /dev/null +++ b/registry/modules/orm-prisma/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["module.ts"], + "exclude": ["templates"] +} diff --git a/registry/modules/styling-tailwind/module.ts b/registry/modules/styling-tailwind/module.ts new file mode 100644 index 0000000..3f403e7 --- /dev/null +++ b/registry/modules/styling-tailwind/module.ts @@ -0,0 +1,39 @@ +import { defineModule } from "@stanza/registry"; + +export default defineModule({ + id: "tailwind", + slot: "styling", + label: "Tailwind CSS", + description: "Utility-first CSS framework. Pairs with any web framework.", + version: "0.1.0", + requires: ["web"], + peers: { framework: ["next", "tanstack-start"] }, + homepage: "https://tailwindcss.com", + adapters: [ + { + key: "next", + match: { framework: "next" }, + devDependencies: { + "@tailwindcss/postcss": "^4.3.0", + postcss: "^8.5.15", + tailwindcss: "^4.3.0", + }, + templates: [ + { src: "next/postcss.config.mjs", dest: "postcss.config.mjs", scope: "app" }, + { src: "globals.css", dest: "app/globals.css", scope: "app" }, + ], + }, + { + key: "tanstack-start", + match: { framework: "tanstack-start" }, + devDependencies: { + "@tailwindcss/vite": "^4.3.0", + tailwindcss: "^4.3.0", + }, + templates: [ + { src: "tanstack/vite.config.ts", dest: "vite.config.ts", scope: "app" }, + { src: "globals.css", dest: "src/globals.css", scope: "app" }, + ], + }, + ], +}); diff --git a/registry/modules/styling-tailwind/package.json b/registry/modules/styling-tailwind/package.json new file mode 100644 index 0000000..5940694 --- /dev/null +++ b/registry/modules/styling-tailwind/package.json @@ -0,0 +1,12 @@ +{ + "name": "@stanza-modules/styling-tailwind", + "version": "0.1.0", + "private": true, + "license": "MIT", + "type": "module", + "main": "./module.ts", + "dependencies": { + "@stanza/codemods": "workspace:*", + "@stanza/registry": "workspace:*" + } +} diff --git a/registry/modules/styling-tailwind/templates/globals.css b/registry/modules/styling-tailwind/templates/globals.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/registry/modules/styling-tailwind/templates/globals.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/registry/modules/styling-tailwind/templates/next/postcss.config.mjs b/registry/modules/styling-tailwind/templates/next/postcss.config.mjs new file mode 100644 index 0000000..c2ddf74 --- /dev/null +++ b/registry/modules/styling-tailwind/templates/next/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; diff --git a/registry/modules/styling-tailwind/templates/tanstack/vite.config.ts b/registry/modules/styling-tailwind/templates/tanstack/vite.config.ts new file mode 100644 index 0000000..0075ab7 --- /dev/null +++ b/registry/modules/styling-tailwind/templates/tanstack/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; +import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [ + // tanstackStart() must precede react() — Start transforms server functions + // and generates the route tree first. + tanstackStart(), + tailwindcss(), + react(), + ], +}); diff --git a/registry/modules/styling-tailwind/tsconfig.json b/registry/modules/styling-tailwind/tsconfig.json new file mode 100644 index 0000000..8988b35 --- /dev/null +++ b/registry/modules/styling-tailwind/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["module.ts"], + "exclude": ["templates"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..a5ccd7e --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "moduleResolution": "Bundler", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "noEmit": true, + "verbatimModuleSyntax": true, + "esModuleInterop": true, + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + "useDefineForClassFields": true, + "sourceMap": true + }, + "exclude": [ + "node_modules", + "dist", + ".output", + ".vercel", + "registry/modules/*/templates", + "**/templates/**" + ] +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..413f6c6 --- /dev/null +++ b/turbo.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://turborepo.com/schema.json", + "ui": "stream", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", ".output/**", ".vercel/output/**"] + }, + "dev": { + "cache": false, + "persistent": true + }, + "test": { + "dependsOn": ["^build"], + "outputs": [] + }, + "lint": { + "outputs": [] + }, + "typecheck": { + "dependsOn": ["^build"], + "outputs": [] + }, + "registry:build": { + "dependsOn": ["^build"], + "outputs": ["dist/registry/**"] + } + } +}