18 KiB
TODO
State at end of last session: mobile/responsive pass on the web builder landed.
Command bar now surfaces above the slot cards below lg; file-preview tree is a
fixed height on mobile (it was collapsing to 0 since @pierre/trees needs a
definite height); command <pre>s wrap; header search collapses to an icon
below sm; disabled slot cards show a specific inline why-disabled reason. Also
fixed a theme bug: the file tree + Shiki previews rendered github-light on a
dark page for system/dark users — ThemeProvider now exposes a reactive
resolvedTheme and FileTree re-keys on it. Confirmed the TanStack devtools
button is stripped from the production build (no source gating needed). Details
under "UI polish / responsiveness" below.
Prior session: web-app QA polish + server-route refactor landed.
Fixed: CMD+K palette crash (missing <Command> wrapper), file-tree going stale
on selection change, file-tree ignoring light theme, OG cards inverting brand
logos, unstyled 404, and a Base UI native-<button> warning. Converted the
/og, /og/$slot/$id, and /sitemap.xml endpoints from a standalone Nitro
server/ dir to TanStack Start server routes in src/routes/, and adopted the
framework's server-fn file-organization convention under apps/web/src/server/:
*.functions.ts for createServerFn wrappers (safe to import anywhere),
*.server.ts(x) for server-only code (Shiki, @vercel/og cards, fs registry
reads), and plain *.ts for client-safe types (the Preview type).
Session before that: pre-1.0 architectural cleanup landed.
- B1–B6 (mechanical cleanup):
SLOTSarray is now the source of truth (was 5 hardcoded copies);pickRegistryRootdeduped; deadtelemetryIdremoved; codemod-not-found error now includes module/adapter context; orphan HTTP-bundle comment dropped;lazyProjectreturns a clean{ get, save }object. - A3: deleted
Module.provides/Module.requires/Capability— they were declared but never read by the resolver. Peer constraints already encode the same info more precisely. - A1: hoisted module-level install fields (
dependencies,devDependencies,env,scripts,consumesPackages). Adapters override per-key when needed. Better Auth's 6 adapters no longer duplicatedependencies,env, or workspace deps. - A4: web app is now the canonical registry host.
prebuildbuilds + copies the registry intoapps/web/public/registry/; deployed Vercel output ships it; the CLI's default URL points at the same path. - A2 deferred: the add-on schema (
manifest.addons[]+kinddiscriminator) is intentionally postponed until the first real add-on module forces concrete design decisions. Categorization documented in the module registry. - Cross-package wiring is now declared via module-level
consumesPackages(was per-adapterpeerPackages).stanza addcomposes modules across slots (verified:framework next→db postgres→orm drizzle→auth better-authproduces a working tree with the right adapter selection and the auth package depending on the db package). 49 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—@tailwindcss/vitev4 wired invite.config.ts, theme variables instyles.cssvia@theme(OKLCH), dark mode via.darkclass - Hand-roll a small component layer or pull in shadcn (web variant) — shadcn-style primitives at
apps/web/src/components/ui/(Card, Button, Input, Badge, Tooltip, Tabs, DropdownMenu, Separator, Sonner). All use base-ui under the hood - Replace the radio-list
Builderwith a card grid per slot —apps/web/src/components/builder/slot-cards.tsx - Show why a module is filtered out (e.g. "needs
framework: next, you picked tanstack-start") instead of just dropping it — disabled cards now render a specific inline reason from the resolver error (describeErrorinslot-cards.tsx):missing-peer→ "Pick a {Slot} module first.",incompatible-peer→ "Doesn't pair with {peer} (your {Slot} pick).",no-adapter→ generic. Inline (not just atitletooltip) so it works on touch - Deep linking: encode selections in URL search params (
?framework=next&orm=drizzle&...) — typedvalidateSearchonroutes/index.tsx, parser inlib/selection.ts - Copy-to-clipboard for the generated
pnpm create stanza ...command —command-bar.tsx - Preview pane: list of files stanza will write —
file-preview.tsxwith@pierre/trees+ Shiki-rendered server-side preview. The tree re-seeds viamodel.resetPaths()on selection change (theuseFileTreemodel is created once and isn't path-reactive on its own) and follows the app theme viathemeToTreeStyles(). Theme now comes from a reactiveresolvedThemeexposed byThemeProvider(computed post-mount) — the old per-componentuseResolvedThemereturned"light"during SSR and never re-rendered, so forsystem/dark users the tree + Shiki blocks renderedgithub-lighton a dark page. The shadow-DOM tree capturesstyleonly at mount, soFileTreeis also re-keyed onresolvedThemeto remount with the right palette (selection survives —modellives in the parent). Same fix applied to the detail page'stemplates-list.tsx - Show pinned npm versions per selected module —
apps/web/src/components/detail/deps-table.tsxon/m/$slot/$id(also covers devDeps + scripts + env). Builder cards stay clean per the design call; versions surface on the detail page - Module detail route at
/m/$slot/$id— full description, adapter switcher (peer chip rows), deps tables, env table, templates list with click-to-expand Shiki preview, "Try it" command.apps/web/src/routes/m.$slot.$id.tsx - Search route — implemented as a header-only popover (
apps/web/src/components/search/site-search.tsx) bound to ⌘K, mirrors the CLI's id/label/description/slot match (lib/module-search.ts) - Layout: header (logo, GitHub link, search), footer —
apps/web/src/components/header.tsx,footer.tsx - Dark mode — Tailwind 4
.darkvariant + ThemeProvider, system pref aware - SEO: meta tags via
head()on routes, OG image, sitemap —apps/web/src/lib/seo.tsbuildshead()output with title/og:*/twitter:*/canonical for every route. Dynamic OG via@vercel/ogat/og/$slot/$idand/og, andsitemap.xml, are TanStack Start server routes colocated inapps/web/src/routes/(og.index.ts,og.$slot.$id.ts,sitemap[.]xml.ts) viacreateFileRoute(...).server.handlers.GET— no separate Nitroserver/dir orserverDirconfig. OG image URLs are extensionless (/og/$slot/$id, not.png) so they aren't swallowed by Vite/Nitro static-asset handling; crawlers read theimage/pngcontent-type.public/robots.txtis static - Host the registry on the same domain —
prebuildscript copiesdist/registry/intoapps/web/public/registry/. CLI'sDEFAULT_REGISTRY_URLpoints at the same path.public/registry/is also registered as a NitroserverAssetsdir (vite.config.ts), so its JSON is embedded in the server bundle and SSR reads it viauseStorage("assets:registry")(apps/web/src/server/registry-base.server.ts) — works on serverless wherepublic/is CDN-only, and avoids the SSR loopback-fetch deadlock - Vercel deploy config — Vercel auto-detects TanStack Start's
.output/directory, novercel.jsonneeded. Env vars (STANZA_REGISTRY) optional override - Docs section (could be MDX routes): overview, authoring guide, registry spec
UI polish / responsiveness
Mobile pass landed and verified with agent-browser at 360/375/1440 in both
light and dark. The builder's two-column split (builder/index.tsx) still
only kicks in at lg, but the command bar now surfaces above the cards below
lg so output is reachable without scrolling past every slot.
- Builder layout on small/medium screens — the command bar is rendered
twice in
builder/index.tsx: alg:hiddeninstance above the slot cards (so phones see the copy-able command first) and the original inside thelg:stickyright column (hidden lg:block). It's stateless (props from the URL), so duplicating it is cheap; the heavyFilePreviewstays single. - File-preview height on mobile (
file-preview.tsx) — tree pane is a fixedh-[180px]belowsm(the virtualized@pierre/treestree needs a definite height —h-fullcollapsed to 0 once the mobilemin-hwas dropped) andsm:h-fullon desktop; code pane cappedmax-h-[360px]mobile /sm:max-h-[480px]. No more ~840px back-to-back blocks. - Command
<pre>overflow (command-bar.tsx+ detailtry-it.tsx) — both usewhitespace-pre-wrap break-words+text-[11px] sm:text-xsso the fullpnpm create stanza …command wraps at spaces and is visible at a glance. - Header density on phones (
site-search.tsx) — search trigger collapses to an icon-only button belowsm(label +⌘Khinthidden sm:inline,min-wand wider padding gated tosm:). - Slot-card touch targets / wrapping (
slot-cards.tsx) — verified clean at 360px: single-column full-width cards,p-4tap targets, footer (slot/id · vX) and label/check row don't wrap. - Confirm the TanStack devtools floating button is dev-only — the TanStack
Start plugin strips it from the production build automatically. Verified:
grep TanStackDevtools .output/public/assets/*.jsreturns nothing afterpnpm --filter @stanza/web build(so doesgrep shiki). No source gating needed. - Module detail page (
m.$slot.$id.tsx) mobile pass — adapter chip rows, deps/env tables, and the (now-wrapping) "Try it" command all fit at 375px.
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, prompt on first run, respect--no-telemetryandDO_NOT_TRACK=1 --yesflag for non-interactiveinit— takes picks from--framework / --styling / --db / --orm / --auth / --pmflags; missing slots are skipped (explicit is better than auto-default)- HTTP registry loader path is implemented but unverified — smoke test against the static JSON output
stanza init: today'sbootstrapShelldoesn't emitturbo.json. Decide whether stanza ships that or a tooling module does. (The rootpnpm-workspace.yamlis now correctly emitted withpackages/*, andapps/<dir>/package.jsonis bootstrapped so the runner's dep merges aren't silent no-ops)- Better error messages for
RegionConflictError(current message is technical; should suggeststanza remove <slot>or manual cleanup) - Build the CLI for publish —
pnpm --filter @stanza/cli buildruns tsdown, producingapps/cli/dist/bin.mjs(~64 KB unminified; ~18 KB gzipped). External npm deps (ts-morph,zod, etc.) stay external — users get them via the normal npm install chain. Workspace deps (@stanza/codemods,@stanza/registry) are inlined.create-stanzabuilds the same way (744 bytes). The published bin runs on plainnode; dev runs viatsx watch ./src/bin.ts. Bun is now gone from the dev workflow except as a maintainer-convenience shebang onscripts/*.ts - Tests for command handlers (
init,add,remove) — 10 tests inapps/cli/src/commands/commands.test.ts, exercise--yesinit, add to existing project, remove + slot-package sweep, cross-package dep cleanup
Modules
Functional but a few real issues to fix.
auth-better-authVITE_BETTER_AUTH_URL: stale — auth-client.ts defaults to current origin; server readsBETTER_AUTH_URLautomatically. No template references VITE_BETTER_AUTH_URL.auth-better-authdrizzleauth.tshardcodedprovider: "pg"— split into per-db templates (auth.drizzle.postgres.ts+auth.drizzle.sqlite.ts) so sqlite getsprovider: "sqlite"auth-better-authsqlite schema variant exists (shared/auth-schema.drizzle-sqlite.ts); confirm it matches what better-auth actually expects on SQLite end-to-endstyling-tailwind+framework-nextapp/globals.cssconflict — fixed by switching styling-tailwind's next adapter to prepend@import "tailwindcss";via theappend-to-filecodemod's newposition: "start"mode. Framework retains ownership of base styles; revert restores cleanly- tanstack-start globals.css orphan — fixed symmetrically to Next. framework-tanstack-start now ships
src/globals.cssand__root.tsximports it; styling-tailwind's tanstack adapter prepends@import "tailwindcss";viaappend-to-fileinstead of writing the file. No new codemod needed; pattern is now identical across both framework adapters. - Authoring guide: docs page covering
defineModule, slot/peer/capability semantics, template vs. codemod choice, region ownership, and thescope: "package"+consumesPackagesstory
Registry expansion
The full first-party module roadmap lives in the module registry. These are the schema/resolver changes needed before most of those modules can land.
- Unified
Categorytaxonomy — collapsed the slot/add-on split into oneCategoryconcept with two orthogonal, explicit axes:cardinality("one" | "many") andhome(app | repo | package, replacingpackageDir+repoScoped).Moduleis no longer a discriminated union — it carries a singlecategoryfield. The manifest unified to onemodules: Partial<Record<CategoryId, StanzaModuleRecord[]>>(arrays everywhere;"one"enforced ≤1 byadd/init), bumpingCURRENT_MANIFEST_VERSION0.1→0.2 (clean break, no migration). Constraint-bearing is now emergent — the resolver iteratesPEER_CATEGORIES(the"one"ids) only. DeletedisAddon/moduleGroup/SlotModule/AddonModule/slotLabel/addonLabel/groupLabel/SLOT_PACKAGE_DIR/SLOT_REPO_SCOPED/ADDON_PACKAGE_DIR/KNOWN_SLOTS/KNOWN_ADDONS; addedCATEGORIES/KNOWN_CATEGORIES/PEER_CATEGORIES/PACKAGE_DIRS/categoryHome/categoryCardinality/isMulti/categoryLabel/selectedOne/selectedAll. Routing decided once incategoryHome, shared by the CLI runner + websynthesizePackageJsons. Wired through resolver/runner/CLI (add/remove/init/list/wizard, unified--<category>flags) + web builder (cards render single- vs multi-select offisMulti). All 14 modules migratedslot→category - Add new categories to
CATEGORIES:api,ai,ui,payments(allcardinality: one). Decide topological order for the wizard prompts (api/ai after framework) - Capability tag expansion — current set is
web | native | react | node | edge | ssr | rsc. Adding Nuxt/Svelte/Solid needsvue,svelte,solidcapabilities; existing React-only modules must addrequires: ['react']so the resolver filters correctly monorepois hardcoded to Turborepo inbootstrapShell— promote to a real category when a second option landspackageManagerYarn 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 category count sane
Infrastructure
- GitHub Actions CI: lint, format check, registry build, web build (generates routeTree.gen.ts), typecheck (every workspace via turbo), tests, CLI bundle + smoke. Single workflow at
.github/workflows/ci.yml - Golden snapshot tests per module combination (per the plan's verification section) — for each valid
(framework, orm, db, auth, styling, pm)tuple, runstanza initheadless, 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 — Changesets configured (
.changeset/config.json, public access,@changesets/changelog-githubfor PR links). Release workflow runschangesets/action@v1on push to main: opens a "Version Packages" PR when changesets queue up; merging publishes viapnpm release(build CLI + create-stanza, thenchangeset publish). Provenance attestations enabled. Tarball dry-run verified:stanza-cliis 20 KB,create-stanzais 1.7 KB; bothnpm install-able from a clean tmpdir and--version/--helpwork. Repo needsNPM_TOKENsecret before the first publish .env.exampleat repo root listingSTANZA_REGISTRY, PostHog key, etc.
Open items from the plan
- Domain —
stanza.tools(registry served athttps://stanza.tools/registry, web builder athttps://stanza.tools) - npm name clearance — locked down unscoped
stanza-cliandcreate-stanzaon npm; the CLI publishes asstanza-cli(binary staysstanza) - 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 <slot> <to>— manifest already recordsversion+regionsto support it; slot-package extraction now means swap can replace the contents ofpackages/<dir>/without touching app importsstanza update— pinned-version 3-way merge- Third-party registry hosting — the spec exists implicitly; publish it formally later
- React Native / Expo modules — needs the
nativecapability + cross-platform framework slot - Additional first-party modules — full catalog tracked in the module registry; land the slot taxonomy changes above first