commit b74ffb9d1af04615486d552a02494c1d0d96ad29 Author: Jake Jarvis Date: Wed May 20 11:52:53 2026 -0400 feat: initial commit 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/**"] + } + } +}