From 0cb24b149497842ee194e5360b1164c56768db15 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 2 May 2025 22:17:51 +0530 Subject: [PATCH] fix mongodb templates and add migrate and generate scripts --- .changeset/slow-melons-shine.md | 5 + apps/cli/src/helpers/project-config.ts | 48 +- apps/cli/src/helpers/template-manager.ts | 2 +- apps/cli/src/index.ts | 144 +-- .../templates/addons/turborepo/turbo.json.hbs | 8 + .../auth/server/base/src/lib/auth.ts.hbs | 5 +- .../db/mongoose/mongodb/src/db/index.ts | 11 +- .../prisma/base/src/routers/todo.ts.hbs | 16 + apps/web/package.json | 2 + .../app/(home)/_components/StackArchitech.tsx | 934 +++++++++++------- apps/web/src/app/layout.tsx | 2 + apps/web/src/components/ui/sonner.tsx | 25 + bun.lock | 6 +- 13 files changed, 765 insertions(+), 443 deletions(-) create mode 100644 .changeset/slow-melons-shine.md create mode 100644 apps/web/src/components/ui/sonner.tsx diff --git a/.changeset/slow-melons-shine.md b/.changeset/slow-melons-shine.md new file mode 100644 index 0000000..ebd7d95 --- /dev/null +++ b/.changeset/slow-melons-shine.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": patch +--- + +add migrate and generate scripts diff --git a/apps/cli/src/helpers/project-config.ts b/apps/cli/src/helpers/project-config.ts index 7129040..52c7a1d 100644 --- a/apps/cli/src/helpers/project-config.ts +++ b/apps/cli/src/helpers/project-config.ts @@ -58,8 +58,8 @@ async function updateRootPackageJson( const needsDbScripts = options.backend !== "convex" && options.database !== "none" && - options.orm !== "none"; - + options.orm !== "none" && + options.orm !== "mongoose"; if (options.addons.includes("turborepo")) { scripts.dev = "turbo dev"; scripts.build = "turbo build"; @@ -73,6 +73,13 @@ async function updateRootPackageJson( if (needsDbScripts) { scripts["db:push"] = `turbo -F ${backendPackageName} db:push`; scripts["db:studio"] = `turbo -F ${backendPackageName} db:studio`; + if (options.orm === "prisma") { + scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`; + scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`; + } else if (options.orm === "drizzle") { + scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`; + scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`; + } } } else if (options.packageManager === "pnpm") { scripts.dev = devScript; @@ -87,6 +94,17 @@ async function updateRootPackageJson( if (needsDbScripts) { scripts["db:push"] = `pnpm --filter ${backendPackageName} db:push`; scripts["db:studio"] = `pnpm --filter ${backendPackageName} db:studio`; + if (options.orm === "prisma") { + scripts["db:generate"] = + `pnpm --filter ${backendPackageName} db:generate`; + scripts["db:migrate"] = + `pnpm --filter ${backendPackageName} db:migrate`; + } else if (options.orm === "drizzle") { + scripts["db:generate"] = + `pnpm --filter ${backendPackageName} db:generate`; + scripts["db:migrate"] = + `pnpm --filter ${backendPackageName} db:migrate`; + } } } else if (options.packageManager === "npm") { scripts.dev = devScript; @@ -102,6 +120,17 @@ async function updateRootPackageJson( scripts["db:push"] = `npm run db:push --workspace ${backendPackageName}`; scripts["db:studio"] = `npm run db:studio --workspace ${backendPackageName}`; + if (options.orm === "prisma") { + scripts["db:generate"] = + `npm run db:generate --workspace ${backendPackageName}`; + scripts["db:migrate"] = + `npm run db:migrate --workspace ${backendPackageName}`; + } else if (options.orm === "drizzle") { + scripts["db:generate"] = + `npm run db:generate --workspace ${backendPackageName}`; + scripts["db:migrate"] = + `npm run db:migrate --workspace ${backendPackageName}`; + } } } else if (options.packageManager === "bun") { scripts.dev = devScript; @@ -116,6 +145,17 @@ async function updateRootPackageJson( if (needsDbScripts) { scripts["db:push"] = `bun run --filter ${backendPackageName} db:push`; scripts["db:studio"] = `bun run --filter ${backendPackageName} db:studio`; + if (options.orm === "prisma") { + scripts["db:generate"] = + `bun run --filter ${backendPackageName} db:generate`; + scripts["db:migrate"] = + `bun run --filter ${backendPackageName} db:migrate`; + } else if (options.orm === "drizzle") { + scripts["db:generate"] = + `bun run --filter ${backendPackageName} db:generate`; + scripts["db:migrate"] = + `bun run --filter ${backendPackageName} db:migrate`; + } } } @@ -192,9 +232,13 @@ async function updateServerPackageJson( if (options.orm === "prisma") { scripts["db:push"] = "prisma db push --schema ./prisma/schema"; scripts["db:studio"] = "prisma studio"; + scripts["db:generate"] = "prisma generate --schema ./prisma/schema"; + scripts["db:migrate"] = "prisma migrate dev"; } else if (options.orm === "drizzle") { scripts["db:push"] = "drizzle-kit push"; scripts["db:studio"] = "drizzle-kit studio"; + scripts["db:generate"] = "drizzle-kit generate"; + scripts["db:migrate"] = "drizzle-kit migrate"; } } diff --git a/apps/cli/src/helpers/template-manager.ts b/apps/cli/src/helpers/template-manager.ts index 79c42ad..81fbf5d 100644 --- a/apps/cli/src/helpers/template-manager.ts +++ b/apps/cli/src/helpers/template-manager.ts @@ -371,7 +371,7 @@ export async function setupAuthTemplate( authDbSrc = path.join( PKG_ROOT, `templates/auth/server/db/mongoose/${db}`, - ) + ); } if (authDbSrc && (await fs.pathExists(authDbSrc))) { await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context); diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index b32503a..913b52a 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -446,82 +446,86 @@ function processAndValidateFlags( config.dbSetup = "none"; } - if (effectiveDatabase === "mongodb" && effectiveOrm === "drizzle") { - consola.fatal( - "Drizzle ORM is not compatible with MongoDB. Please use --orm prisma or --orm mongoose.", - ); - process.exit(1); - } + if (config.orm === "mongoose" && !providedFlags.has("database")) { + config.database = "mongodb"; + } - if ( - effectiveOrm === "mongoose" && - effectiveDatabase && - effectiveDatabase !== "mongodb" - ) { - consola.fatal( - `Mongoose ORM requires MongoDB. Cannot use --orm mongoose with --database ${effectiveDatabase}.`, - ); - process.exit(1); - } - - if (config.dbSetup && config.dbSetup !== "none") { - const dbSetup = config.dbSetup; - - if (!effectiveDatabase || effectiveDatabase === "none") { + if (effectiveDatabase === "mongodb" && effectiveOrm === "drizzle") { consola.fatal( - `Database setup '--db-setup ${dbSetup}' requires a database. Cannot use when database is 'none'.`, + "Drizzle ORM is not compatible with MongoDB. Please use --orm prisma or --orm mongoose.", ); process.exit(1); } - if (dbSetup === "turso") { - if (effectiveDatabase && effectiveDatabase !== "sqlite") { + if ( + effectiveOrm === "mongoose" && + effectiveDatabase && + effectiveDatabase !== "mongodb" + ) { + consola.fatal( + `Mongoose ORM requires MongoDB. Cannot use --orm mongoose with --database ${effectiveDatabase}.`, + ); + process.exit(1); + } + + if (config.dbSetup && config.dbSetup !== "none") { + const dbSetup = config.dbSetup; + + if (!effectiveDatabase || effectiveDatabase === "none") { consola.fatal( - `Turso setup requires SQLite. Cannot use --db-setup turso with --database ${effectiveDatabase}`, + `Database setup '--db-setup ${dbSetup}' requires a database. Cannot use when database is 'none'.`, ); process.exit(1); } - if (effectiveOrm !== "drizzle") { - consola.fatal( - `Turso setup requires Drizzle ORM. Cannot use --db-setup turso with --orm ${effectiveOrm ?? "none"}.`, - ); - process.exit(1); - } - } else if (dbSetup === "prisma-postgres") { - if (effectiveDatabase !== "postgres") { - consola.fatal( - `Prisma PostgreSQL setup requires PostgreSQL. Cannot use --db-setup prisma-postgres with --database ${effectiveDatabase}.`, - ); - process.exit(1); - } - if (effectiveOrm !== "prisma") { - consola.fatal( - `Prisma PostgreSQL setup requires Prisma ORM. Cannot use --db-setup prisma-postgres with --orm ${effectiveOrm}.`, - ); - process.exit(1); - } - } else if (dbSetup === "mongodb-atlas") { - if (effectiveDatabase !== "mongodb") { - consola.fatal( - `MongoDB Atlas setup requires MongoDB. Cannot use --db-setup mongodb-atlas with --database ${effectiveDatabase}.`, - ); - process.exit(1); - } - if (effectiveOrm !== "prisma" && effectiveOrm !== "mongoose") { - consola.fatal( - `MongoDB Atlas setup requires Prisma or Mongoose ORM. Cannot use --db-setup mongodb-atlas with --orm ${effectiveOrm}.`, - ); - process.exit(1); - } - } else if (dbSetup === "neon") { - if (effectiveDatabase !== "postgres") { - consola.fatal( - `Neon PostgreSQL setup requires PostgreSQL. Cannot use --db-setup neon with --database ${effectiveDatabase}.`, - ); - process.exit(1); + + if (dbSetup === "turso") { + if (effectiveDatabase && effectiveDatabase !== "sqlite") { + consola.fatal( + `Turso setup requires SQLite. Cannot use --db-setup turso with --database ${effectiveDatabase}`, + ); + process.exit(1); + } + if (effectiveOrm !== "drizzle") { + consola.fatal( + `Turso setup requires Drizzle ORM. Cannot use --db-setup turso with --orm ${effectiveOrm ?? "none"}.`, + ); + process.exit(1); + } + } else if (dbSetup === "prisma-postgres") { + if (effectiveDatabase !== "postgres") { + consola.fatal( + `Prisma PostgreSQL setup requires PostgreSQL. Cannot use --db-setup prisma-postgres with --database ${effectiveDatabase}.`, + ); + process.exit(1); + } + if (effectiveOrm !== "prisma") { + consola.fatal( + `Prisma PostgreSQL setup requires Prisma ORM. Cannot use --db-setup prisma-postgres with --orm ${effectiveOrm}.`, + ); + process.exit(1); + } + } else if (dbSetup === "mongodb-atlas") { + if (effectiveDatabase !== "mongodb") { + consola.fatal( + `MongoDB Atlas setup requires MongoDB. Cannot use --db-setup mongodb-atlas with --database ${effectiveDatabase}.`, + ); + process.exit(1); + } + if (effectiveOrm !== "prisma" && effectiveOrm !== "mongoose") { + consola.fatal( + `MongoDB Atlas setup requires Prisma or Mongoose ORM. Cannot use --db-setup mongodb-atlas with --orm ${effectiveOrm}.`, + ); + process.exit(1); + } + } else if (dbSetup === "neon") { + if (effectiveDatabase !== "postgres") { + consola.fatal( + `Neon PostgreSQL setup requires PostgreSQL. Cannot use --db-setup neon with --database ${effectiveDatabase}.`, + ); + process.exit(1); + } } } - } const includesNuxt = effectiveFrontend?.includes("nuxt"); const includesSvelte = effectiveFrontend?.includes("svelte"); @@ -580,13 +584,13 @@ function processAndValidateFlags( process.exit(1); } - if (config.addons.includes("husky") && !config.addons.includes("biome")) { - consola.warn( - "Husky addon is recommended to be used with Biome for lint-staged configuration.", - ); + if (config.addons.includes("husky") && !config.addons.includes("biome")) { + consola.warn( + "Husky addon is recommended to be used with Biome for lint-staged configuration.", + ); + } + config.addons = [...new Set(config.addons)]; } - config.addons = [...new Set(config.addons)]; - } const onlyNativeFrontend = effectiveFrontend && diff --git a/apps/cli/templates/addons/turborepo/turbo.json.hbs b/apps/cli/templates/addons/turborepo/turbo.json.hbs index 5444744..32f9810 100644 --- a/apps/cli/templates/addons/turborepo/turbo.json.hbs +++ b/apps/cli/templates/addons/turborepo/turbo.json.hbs @@ -29,6 +29,14 @@ "db:studio": { "cache": false, "persistent": true + }, + "db:migrate": { + "cache": false, + "persistent": true + }, + "db:generate": { + "cache": false, + "persistent": true } {{/unless}}{{/if}} } diff --git a/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs b/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs index 6ea57c3..815f996 100644 --- a/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs +++ b/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs @@ -11,6 +11,7 @@ export const auth = betterAuth({ {{#if (eq database "postgres")}}provider: "postgresql"{{/if}} {{#if (eq database "sqlite")}}provider: "sqlite"{{/if}} {{#if (eq database "mysql")}}provider: "mysql"{{/if}} + {{#if (eq database "mongodb")}}provider: "mongodb"{{/if}} }), trustedOrigins: [ process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}} @@ -67,7 +68,7 @@ import { expo } from "@better-auth/expo"; import { client } from "../db"; export const auth = betterAuth({ - database: mongodbAdapter(client.db()), + database: mongodbAdapter(client), trustedOrigins: [ process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}} "my-better-t-app://",{{/if}} @@ -104,4 +105,4 @@ export const auth = betterAuth({ plugins: [expo()] {{/if~}} }); -{{/if}} \ No newline at end of file +{{/if}} diff --git a/apps/cli/templates/db/mongoose/mongodb/src/db/index.ts b/apps/cli/templates/db/mongoose/mongodb/src/db/index.ts index 82b3b6b..a4169e7 100644 --- a/apps/cli/templates/db/mongoose/mongodb/src/db/index.ts +++ b/apps/cli/templates/db/mongoose/mongodb/src/db/index.ts @@ -1,6 +1,9 @@ -import mongoose from 'mongoose'; +import mongoose from "mongoose"; -await mongoose.connect(process.env.DATABASE_URL || ""); -const client = mongoose.connection.getClient(); +await mongoose.connect(process.env.DATABASE_URL || "").catch((error) => { + console.log("Error connecting to database:", error); +}); -export { client }; \ No newline at end of file +const client = mongoose.connection.getClient().db("myDB"); + +export { client }; diff --git a/apps/cli/templates/examples/todo/server/prisma/base/src/routers/todo.ts.hbs b/apps/cli/templates/examples/todo/server/prisma/base/src/routers/todo.ts.hbs index 63e2eb5..dc1724b 100644 --- a/apps/cli/templates/examples/todo/server/prisma/base/src/routers/todo.ts.hbs +++ b/apps/cli/templates/examples/todo/server/prisma/base/src/routers/todo.ts.hbs @@ -23,7 +23,11 @@ export const todoRouter = { }), toggle: publicProcedure + {{#if (eq database "mongodb")}} + .input(z.object({ id: z.string(), completed: z.boolean() })) + {{else}} .input(z.object({ id: z.number(), completed: z.boolean() })) + {{/if}} .handler(async ({ input }) => { await prisma.todo.update({ where: { id: input.id }, @@ -33,7 +37,11 @@ export const todoRouter = { }), delete: publicProcedure + {{#if (eq database "mongodb")}} + .input(z.object({ id: z.string() })) + {{else}} .input(z.object({ id: z.number() })) + {{/if}} .handler(async ({ input }) => { await prisma.todo.delete({ where: { id: input.id }, @@ -69,7 +77,11 @@ export const todoRouter = router({ }), toggle: publicProcedure + {{#if (eq database "mongodb")}} + .input(z.object({ id: z.string(), completed: z.boolean() })) + {{else}} .input(z.object({ id: z.number(), completed: z.boolean() })) + {{/if}} .mutation(async ({ input }) => { try { return await prisma.todo.update({ @@ -85,7 +97,11 @@ export const todoRouter = router({ }), delete: publicProcedure + {{#if (eq database "mongodb")}} + .input(z.object({ id: z.string() })) + {{else}} .input(z.object({ id: z.number() })) + {{/if}} .mutation(async ({ input }) => { try { return await prisma.todo.delete({ diff --git a/apps/web/package.json b/apps/web/package.json index e0ae0a3..1835d08 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -22,10 +22,12 @@ "lucide-react": "^0.503.0", "motion": "^12.8.0", "next": "15.3.1", + "next-themes": "^0.4.6", "nuqs": "^2.4.3", "react": "^19.1.0", "react-dom": "^19.1.0", "react-tweet": "^3.2.2", + "sonner": "^2.0.3", "tailwind-merge": "^3.2.0" }, "devDependencies": { diff --git a/apps/web/src/app/(home)/_components/StackArchitech.tsx b/apps/web/src/app/(home)/_components/StackArchitech.tsx index b0445e0..250ecd8 100644 --- a/apps/web/src/app/(home)/_components/StackArchitech.tsx +++ b/apps/web/src/app/(home)/_components/StackArchitech.tsx @@ -34,7 +34,8 @@ import Image from "next/image"; import Link from "next/link"; import { useQueryStates } from "nuqs"; import type React from "react"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { toast } from "sonner"; const validateProjectName = (name: string): string | undefined => { const INVALID_CHARS = ["<", ">", ":", '"', "|", "?", "*"]; @@ -164,12 +165,15 @@ const getCategoryDisplayName = (categoryKey: string): string => { interface CompatibilityResult { adjustedStack: StackState | null; notes: Record; + changes: Array<{ category: string; message: string }>; } const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { const nextStack = { ...stack }; let changed = false; const notes: CompatibilityResult["notes"] = {}; + const changes: Array<{ category: string; message: string }> = []; + for (const cat of CATEGORY_ORDER) { notes[cat] = { notes: [], hasIssue: false }; } @@ -190,20 +194,25 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { for (const [key, value] of Object.entries(convexOverrides)) { const catKey = key as keyof StackState; if (JSON.stringify(nextStack[catKey]) !== JSON.stringify(value)) { + const displayName = getCategoryDisplayName(catKey); + const valueDisplay = Array.isArray(value) ? value.join(", ") : value; + const message = `${displayName} set to '${valueDisplay}'`; + notes[catKey].notes.push( - `Convex backend selected: ${getCategoryDisplayName( - catKey, - )} will be set to '${Array.isArray(value) ? value.join(", ") : value}'.`, + `Convex backend selected: ${displayName} will be set to '${valueDisplay}'.`, ); notes.backend.notes.push( - `Convex requires ${getCategoryDisplayName(catKey)} to be '${ - Array.isArray(value) ? value.join(", ") : value - }'.`, + `Convex requires ${displayName} to be '${valueDisplay}'.`, ); notes[catKey].hasIssue = true; notes.backend.hasIssue = true; (nextStack[catKey] as string | string[]) = value; changed = true; + + changes.push({ + category: "convex", + message, + }); } } } else { @@ -214,6 +223,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { notes.runtime.hasIssue = true; nextStack.runtime = DEFAULT_STACK.runtime; changed = true; + changes.push({ + category: "runtime", + message: "Runtime set to 'Bun' (None is only for Convex)", + }); } if (nextStack.api === "none") { notes.api.notes.push( @@ -222,160 +235,225 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { notes.api.hasIssue = true; nextStack.api = DEFAULT_STACK.api; changed = true; + changes.push({ + category: "api", + message: "API set to 'tRPC' (None is only for Convex)", + }); } - if (nextStack.database === "none") { - if (nextStack.orm !== "none") { - notes.database.notes.push( - "Database 'None' selected: ORM will be set to 'None'.", - ); - notes.orm.notes.push( - "ORM requires a database. It will be set to 'None'.", - ); - notes.database.hasIssue = true; - notes.orm.hasIssue = true; - nextStack.orm = "none"; - changed = true; + if (nextStack.database === "none") { + if (nextStack.orm !== "none") { + notes.database.notes.push( + "Database 'None' selected: ORM will be set to 'None'.", + ); + notes.orm.notes.push( + "ORM requires a database. It will be set to 'None'.", + ); + notes.database.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "none"; + changed = true; + changes.push({ + category: "database", + message: "ORM set to 'None' (requires a database)", + }); + } + if (nextStack.auth === "true") { + notes.database.notes.push( + "Database 'None' selected: Auth will be disabled.", + ); + notes.auth.notes.push( + "Authentication requires a database. It will be disabled.", + ); + notes.database.hasIssue = true; + notes.auth.hasIssue = true; + nextStack.auth = "false"; + changed = true; + changes.push({ + category: "database", + message: "Authentication disabled (requires a database)", + }); + } + if (nextStack.dbSetup !== "none") { + notes.database.notes.push( + "Database 'None' selected: DB Setup will be set to 'Basic'.", + ); + notes.dbSetup.notes.push( + "DB Setup requires a database. It will be set to 'Basic Setup'.", + ); + notes.database.hasIssue = true; + notes.dbSetup.hasIssue = true; + nextStack.dbSetup = "none"; + changed = true; + changes.push({ + category: "database", + message: "DB Setup set to 'None' (requires a database)", + }); + } + } else if (nextStack.database === "mongodb") { + if (nextStack.orm !== "prisma" && nextStack.orm !== "mongoose") { + notes.database.notes.push( + "MongoDB requires Prisma or Mongoose ORM. Prisma will be selected.", + ); + notes.orm.notes.push( + "MongoDB requires Prisma or Mongoose ORM. Prisma will be selected.", + ); + notes.database.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "prisma"; + changed = true; + changes.push({ + category: "database", + message: "ORM set to 'Prisma' (MongoDB requires Prisma or Mongoose)", + }); + } + } else { + if (nextStack.orm === "mongoose") { + notes.database.notes.push( + "Relational databases are not compatible with Mongoose ORM", + ); + notes.orm.notes.push( + "Relational databases are not compatible with Mongoose ORM", + ); + notes.database.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "prisma"; + changed = true; + changes.push({ + category: "database", + message: "ORM set to 'Prisma' (Mongoose only works with MongoDB)", + }); + } + if (nextStack.dbSetup === "mongodb-atlas") { + notes.database.notes.push( + "Relational databases are not compatible with MongoDB Atlas setup. DB Setup will be reset.", + ); + notes.dbSetup.notes.push( + "MongoDB Atlas setup requires MongoDB. It will be reset to 'Basic Setup'.", + ); + notes.database.hasIssue = true; + notes.dbSetup.hasIssue = true; + nextStack.dbSetup = "none"; + changed = true; + changes.push({ + category: "database", + message: "DB Setup reset to 'None' (MongoDB Atlas requires MongoDB)", + }); + } } - if (nextStack.auth === "true") { - notes.database.notes.push( - "Database 'None' selected: Auth will be disabled.", - ); - notes.auth.notes.push( - "Authentication requires a database. It will be disabled.", - ); - notes.database.hasIssue = true; - notes.auth.hasIssue = true; - nextStack.auth = "false"; - changed = true; - } - if (nextStack.dbSetup !== "none") { - notes.database.notes.push( - "Database 'None' selected: DB Setup will be set to 'Basic'.", - ); - notes.dbSetup.notes.push( - "DB Setup requires a database. It will be set to 'Basic Setup'.", - ); - notes.database.hasIssue = true; - notes.dbSetup.hasIssue = true; - nextStack.dbSetup = "none"; - changed = true; - } - } else if (nextStack.database === "mongodb") { - if (nextStack.orm !== "prisma" && nextStack.orm !== "mongoose") { - notes.database.notes.push( - "MongoDB requires Prisma or Mongoose ORM. Prisma will be selected.", - ); - notes.orm.notes.push("MongoDB requires Prisma or Mongoose ORM. Prisma will be selected."); - notes.database.hasIssue = true; - notes.orm.hasIssue = true; - nextStack.orm = "prisma"; - changed = true; - } - } else { - if (nextStack.orm === "mongoose") { - notes.database.notes.push( - "Relational databases are not compatible with Mongoose ORM", - ); - notes.orm.notes.push("Relational databases are not compatible with Mongoose ORM"); - notes.database.hasIssue = true; - notes.orm.hasIssue = true; - nextStack.orm = "prisma"; - changed = true; - } - if (nextStack.dbSetup === "mongodb-atlas") { - notes.database.notes.push( - "Relational databases are not compatible with MongoDB Atlas setup. DB Setup will be reset.", - ); - notes.dbSetup.notes.push( - "MongoDB Atlas setup requires MongoDB. It will be reset to 'Basic Setup'.", - ); - notes.database.hasIssue = true; - notes.dbSetup.hasIssue = true; - nextStack.dbSetup = "none"; - changed = true; - } - } - if (nextStack.dbSetup === "turso") { - if (nextStack.database !== "sqlite") { - notes.dbSetup.notes.push("Turso requires SQLite. It will be selected."); - notes.database.notes.push( - "Turso DB setup requires SQLite. It will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.database.hasIssue = true; - nextStack.database = "sqlite"; - changed = true; + if (nextStack.dbSetup === "turso") { + if (nextStack.database !== "sqlite") { + notes.dbSetup.notes.push("Turso requires SQLite. It will be selected."); + notes.database.notes.push( + "Turso DB setup requires SQLite. It will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.database.hasIssue = true; + nextStack.database = "sqlite"; + changed = true; + changes.push({ + category: "dbSetup", + message: "Database set to 'SQLite' (required by Turso)", + }); + } + if (nextStack.orm !== "drizzle") { + notes.dbSetup.notes.push( + "Turso requires Drizzle ORM. It will be selected.", + ); + notes.orm.notes.push( + "Turso DB setup requires Drizzle ORM. It will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "drizzle"; + changed = true; + changes.push({ + category: "dbSetup", + message: "ORM set to 'Drizzle' (required by Turso)", + }); + } + } else if (nextStack.dbSetup === "prisma-postgres") { + if (nextStack.database !== "postgres") { + notes.dbSetup.notes.push("Requires PostgreSQL. It will be selected."); + notes.database.notes.push( + "Prisma PostgreSQL setup requires PostgreSQL. It will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.database.hasIssue = true; + nextStack.database = "postgres"; + changed = true; + changes.push({ + category: "dbSetup", + message: + "Database set to 'PostgreSQL' (required by Prisma PostgreSQL setup)", + }); + } + if (nextStack.orm !== "prisma") { + notes.dbSetup.notes.push("Requires Prisma ORM. It will be selected."); + notes.orm.notes.push( + "Prisma PostgreSQL setup requires Prisma ORM. It will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "prisma"; + changed = true; + changes.push({ + category: "dbSetup", + message: "ORM set to 'Prisma' (required by Prisma PostgreSQL setup)", + }); + } + } else if (nextStack.dbSetup === "mongodb-atlas") { + if (nextStack.database !== "mongodb") { + notes.dbSetup.notes.push("Requires MongoDB. It will be selected."); + notes.database.notes.push( + "MongoDB Atlas setup requires MongoDB. It will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.database.hasIssue = true; + nextStack.database = "mongodb"; + changed = true; + changes.push({ + category: "dbSetup", + message: + "Database set to 'MongoDB' (required by MongoDB Atlas setup)", + }); + } + if (nextStack.orm !== "prisma" && nextStack.orm !== "mongoose") { + notes.dbSetup.notes.push( + "Requires Prisma or Mongoose ORM. Prisma will be selected.", + ); + notes.orm.notes.push( + "MongoDB Atlas setup requires Prisma or Mongoose ORM. Prisma will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "prisma"; + changed = true; + changes.push({ + category: "dbSetup", + message: + "ORM set to 'Prisma' (MongoDB Atlas requires Prisma or Mongoose)", + }); + } + } else if (nextStack.dbSetup === "neon") { + if (nextStack.database !== "postgres") { + notes.dbSetup.notes.push( + "Neon requires PostgreSQL. It will be selected.", + ); + notes.database.notes.push( + "Neon DB setup requires PostgreSQL. It will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.database.hasIssue = true; + nextStack.database = "postgres"; + changed = true; + changes.push({ + category: "dbSetup", + message: "Database set to 'PostgreSQL' (required by Neon)", + }); + } } - if (nextStack.orm !== "drizzle") { - notes.dbSetup.notes.push( - "Turso requires Drizzle ORM. It will be selected.", - ); - notes.orm.notes.push( - "Turso DB setup requires Drizzle ORM. It will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.orm.hasIssue = true; - nextStack.orm = "drizzle"; - changed = true; - } - } else if (nextStack.dbSetup === "prisma-postgres") { - if (nextStack.database !== "postgres") { - notes.dbSetup.notes.push("Requires PostgreSQL. It will be selected."); - notes.database.notes.push( - "Prisma PostgreSQL setup requires PostgreSQL. It will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.database.hasIssue = true; - nextStack.database = "postgres"; - changed = true; - } - if (nextStack.orm !== "prisma") { - notes.dbSetup.notes.push("Requires Prisma ORM. It will be selected."); - notes.orm.notes.push( - "Prisma PostgreSQL setup requires Prisma ORM. It will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.orm.hasIssue = true; - nextStack.orm = "prisma"; - changed = true; - } - } else if (nextStack.dbSetup === "mongodb-atlas") { - if (nextStack.database !== "mongodb") { - notes.dbSetup.notes.push("Requires MongoDB. It will be selected."); - notes.database.notes.push( - "MongoDB Atlas setup requires MongoDB. It will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.database.hasIssue = true; - nextStack.database = "mongodb"; - changed = true; - } - if (nextStack.orm !== "prisma" && nextStack.orm !== "mongoose") { - notes.dbSetup.notes.push("Requires Prisma or Mongoose ORM. Prisma will be selected."); - notes.orm.notes.push( - "MongoDB Atlas setup requires Prisma or Mongoose ORM. Prisma will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.orm.hasIssue = true; - nextStack.orm = "prisma"; - changed = true; - } - } else if (nextStack.dbSetup === "neon") { - if (nextStack.database !== "postgres") { - notes.dbSetup.notes.push( - "Neon requires PostgreSQL. It will be selected.", - ); - notes.database.notes.push( - "Neon DB setup requires PostgreSQL. It will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.database.hasIssue = true; - nextStack.database = "postgres"; - changed = true; - } - } const isNuxt = nextStack.frontend.includes("nuxt"); const isSvelte = nextStack.frontend.includes("svelte"); @@ -391,6 +469,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { notes.frontend.hasIssue = true; nextStack.api = "orpc"; changed = true; + changes.push({ + category: "api", + message: `API set to 'oRPC' (required by ${frontendName})`, + }); } const incompatibleAddons: string[] = []; @@ -407,6 +489,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { ); notes.frontend.hasIssue = true; notes.addons.hasIssue = true; + changes.push({ + category: "addons", + message: "PWA addon removed (requires TanStack or React Router)", + }); } if (!isTauriCompat && nextStack.addons.includes("tauri")) { incompatibleAddons.push("tauri"); @@ -418,6 +504,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { ); notes.frontend.hasIssue = true; notes.addons.hasIssue = true; + changes.push({ + category: "addons", + message: "Tauri addon removed (requires compatible frontend)", + }); } const originalAddonsLength = nextStack.addons.length; @@ -453,66 +543,89 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { notes.frontend.hasIssue = true; notes.examples.hasIssue = true; incompatibleExamples.push(...nextStack.examples); + changes.push({ + category: "examples", + message: "Examples removed (not supported with Native-only frontend)", + }); } } else { if (!isWeb) { - if (nextStack.examples.includes("todo")) + if (nextStack.examples.includes("todo")) { incompatibleExamples.push("todo"); - if (nextStack.examples.includes("ai")) incompatibleExamples.push("ai"); + changes.push({ + category: "examples", + message: "Todo example removed (requires web frontend)", + }); + } + if (nextStack.examples.includes("ai")) { + incompatibleExamples.push("ai"); + changes.push({ + category: "examples", + message: "AI example removed (requires web frontend)", + }); + } } if ( nextStack.database === "none" && nextStack.examples.includes("todo") ) { incompatibleExamples.push("todo"); + changes.push({ + category: "examples", + message: "Todo example removed (requires a database)", + }); } if (nextStack.backend === "elysia" && nextStack.examples.includes("ai")) { incompatibleExamples.push("ai"); + changes.push({ + category: "examples", + message: "AI example removed (not compatible with Elysia)", + }); } } - const uniqueIncompatibleExamples = [...new Set(incompatibleExamples)]; - if (uniqueIncompatibleExamples.length > 0) { - if ( - !isWeb && - (uniqueIncompatibleExamples.includes("todo") || - uniqueIncompatibleExamples.includes("ai")) - ) { - notes.frontend.notes.push( - "Examples require a web frontend. Incompatible examples will be removed.", - ); - notes.examples.notes.push( - "Requires a web frontend. Incompatible examples will be removed.", - ); - notes.frontend.hasIssue = true; - notes.examples.hasIssue = true; - } - if ( - nextStack.database === "none" && - uniqueIncompatibleExamples.includes("todo") - ) { - notes.database.notes.push( - "Todo example requires a database. It will be removed.", - ); - notes.examples.notes.push( - "Todo example requires a database. It will be removed.", - ); - notes.database.hasIssue = true; - notes.examples.hasIssue = true; - } - if ( - nextStack.backend === "elysia" && - uniqueIncompatibleExamples.includes("ai") - ) { - notes.backendFramework.notes.push( - "AI example is not compatible with Elysia. It will be removed.", - ); - notes.examples.notes.push( - "AI example is not compatible with Elysia. It will be removed.", - ); - notes.backendFramework.hasIssue = true; - notes.examples.hasIssue = true; - } + const uniqueIncompatibleExamples = [...new Set(incompatibleExamples)]; + if (uniqueIncompatibleExamples.length > 0) { + if ( + !isWeb && + (uniqueIncompatibleExamples.includes("todo") || + uniqueIncompatibleExamples.includes("ai")) + ) { + notes.frontend.notes.push( + "Examples require a web frontend. Incompatible examples will be removed.", + ); + notes.examples.notes.push( + "Requires a web frontend. Incompatible examples will be removed.", + ); + notes.frontend.hasIssue = true; + notes.examples.hasIssue = true; + } + if ( + nextStack.database === "none" && + uniqueIncompatibleExamples.includes("todo") + ) { + notes.database.notes.push( + "Todo example requires a database. It will be removed.", + ); + notes.examples.notes.push( + "Todo example requires a database. It will be removed.", + ); + notes.database.hasIssue = true; + notes.examples.hasIssue = true; + } + if ( + nextStack.backend === "elysia" && + uniqueIncompatibleExamples.includes("ai") + ) { + notes.backend.notes.push( + "AI example is not compatible with Elysia. It will be removed.", + ); + notes.examples.notes.push( + "AI example is not compatible with Elysia. It will be removed.", + ); + notes.backend.hasIssue = true; + notes.examples.hasIssue = true; + } const originalExamplesLength = nextStack.examples.length; nextStack.examples = nextStack.examples.filter( @@ -525,6 +638,25 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { return { adjustedStack: changed ? nextStack : null, notes, + changes, + }; +}; + +const getCompatibilityRules = (stack: StackState) => { + const isConvex = stack.backend === "convex"; + const hasWebFrontendSelected = hasWebFrontend(stack.frontend); + const hasNativeOnly = + hasNativeFrontend(stack.frontend) && !hasWebFrontendSelected; + + return { + isConvex, + hasWebFrontend: hasWebFrontendSelected, + hasNativeFrontend: hasNativeFrontend(stack.frontend), + hasNativeOnly, + hasPWACompatible: hasPWACompatibleFrontend(stack.frontend), + hasTauriCompatible: hasTauriCompatibleFrontend(stack.frontend), + hasNuxtOrSvelte: + stack.frontend.includes("nuxt") || stack.frontend.includes("svelte"), }; }; @@ -676,215 +808,286 @@ const StackArchitect = () => { const [activeCategory, setActiveCategory] = useState( CATEGORY_ORDER[0], ); + const [lastChanges, setLastChanges] = useState< + Array<{ category: string; message: string }> + >([]); const sectionRefs = useRef>({}); const contentRef = useRef(null); - const compatibilityAnalysis = analyzeStackCompatibility(stack); - const isConvexSelected = stack.backend === "convex"; - const currentHasWebFrontend = hasWebFrontend(stack.frontend); - const currentHasNativeFrontend = hasNativeFrontend(stack.frontend); - const currentHasPWACompatibleFrontend = hasPWACompatibleFrontend( - stack.frontend, - ); - const currentHasTauriCompatibleFrontend = hasTauriCompatibleFrontend( - stack.frontend, + const compatibilityAnalysis = useMemo( + () => analyzeStackCompatibility(stack), + [stack], ); - const disabledReasons = (() => { + const rules = useMemo(() => getCompatibilityRules(stack), [stack]); + + const disabledReasons = useMemo(() => { const reasons = new Map(); - const currentStack = stack; + + const addRule = (category: string, techId: string, reason: string) => { + reasons.set(`${category}-${techId}`, reason); + }; for (const category of CATEGORY_ORDER) { - const categoryOptions = TECH_OPTIONS[category] || []; + const options = TECH_OPTIONS[category] || []; const catKey = category as keyof StackState; - for (const tech of categoryOptions) { - let reason: string | null = null; + for (const tech of options) { const techId = tech.id; - if (isConvexSelected) { + if (rules.isConvex) { if ( - [ - "runtime", - "database", - "orm", - "api", - "auth", - "dbSetup", - "examples", - ].includes(catKey) + ["runtime", "database", "orm", "api", "auth", "dbSetup"].includes( + catKey, + ) ) { - const convexDefaults: Record = { + const convexDefaults: Record = { runtime: "none", database: "none", orm: "none", api: "none", auth: "false", dbSetup: "none", - examples: ["todo"], }; - const requiredValue = convexDefaults[catKey]; - if ( - typeof requiredValue === "string" && - techId !== requiredValue && - techId !== "none" - ) { - if (!(catKey === "dbSetup" && techId === "none")) { - reason = `Convex backend requires ${getCategoryDisplayName( - catKey, - )} to be '${requiredValue}'.`; - } - } else if (Array.isArray(requiredValue)) { - if (catKey === "examples" && techId !== "todo") { - reason = "Convex backend only supports the 'Todo' example."; - } - } - } - } else { - if (catKey === "runtime" && techId === "none") - reason = - "Runtime 'None' is only available with the Convex backend."; - if (catKey === "api" && techId === "none") - reason = "API 'None' is only available with the Convex backend."; - if (catKey === "api") { - if ( - techId === "trpc" && - (currentStack.frontend.includes("nuxt") || - currentStack.frontend.includes("svelte")) - ) { - reason = `tRPC is not supported with ${ - currentStack.frontend.includes("nuxt") ? "Nuxt" : "Svelte" - }. Use oRPC instead.`; + const requiredValue = convexDefaults[catKey as string]; + if (techId !== requiredValue && techId !== "none") { + addRule( + category, + techId, + `Convex backend requires ${getCategoryDisplayName(catKey)} to be '${requiredValue}'.`, + ); } } - if (catKey === "orm") { - if (currentStack.database === "none" && techId !== "none") - reason = "Select a database to enable ORM options."; - if ( - currentStack.database === "mongodb" && - techId !== "prisma" && - techId !== "mongoose" && - techId !== "none" - ) - reason = "MongoDB requires the Prisma or Mongoose ORM."; - if ( - currentStack.dbSetup === "turso" && - techId !== "drizzle" && - techId !== "none" - ) - reason = "Turso DB setup requires the Drizzle ORM."; - if ( - currentStack.dbSetup === "prisma-postgres" && - techId !== "prisma" && - techId !== "none" - ) - reason = "Prisma PostgreSQL setup requires Prisma ORM."; - if ( - currentStack.dbSetup === "mongodb-atlas" && - techId !== "prisma" && - techId !== "mongoose" && - techId !== "none" - ) - reason = "MongoDB Atlas setup requires Prisma or Mongoose ORM."; + if (catKey === "examples" && techId !== "todo") { + addRule( + category, + techId, + "Convex backend only supports the 'Todo' example.", + ); + } + continue; + } - if (techId === "none") { - if (currentStack.database === "mongodb") - reason = "MongoDB requires Prisma or Mongoose ORM."; - if (currentStack.dbSetup === "turso") - reason = "Turso DB setup requires Drizzle ORM."; - if (currentStack.dbSetup === "prisma-postgres") - reason = "This DB setup requires Prisma ORM."; - } + if (catKey === "runtime" && techId === "none") { + addRule( + category, + techId, + "Runtime 'None' is only available with the Convex backend.", + ); + } - if (techId === "mongoose" && (currentStack.database !== "mongodb")) { - reason = "Mongoose ORM is not compatible with relational databases."; - } + if (catKey === "api") { + if (techId === "none") { + addRule( + category, + techId, + "API 'None' is only available with the Convex backend.", + ); } - if (catKey === "dbSetup" && techId !== "none") { - if (currentStack.database === "none") - reason = "Select a database before choosing a cloud setup."; + if (techId === "trpc" && rules.hasNuxtOrSvelte) { + const frontendName = stack.frontend.includes("nuxt") + ? "Nuxt" + : "Svelte"; + addRule( + category, + techId, + `tRPC is not supported with ${frontendName}. Use oRPC instead.`, + ); + } + } - if (techId === "turso") { - if ( - currentStack.database !== "sqlite" && - currentStack.database !== "none" - ) - reason = "Turso requires SQLite database."; - if (currentStack.orm !== "drizzle" && currentStack.orm !== "none") - reason = "Turso requires Drizzle ORM."; - } else if (techId === "prisma-postgres") { - if ( - currentStack.database !== "postgres" && - currentStack.database !== "none" - ) - reason = "Requires PostgreSQL database."; - if (currentStack.orm !== "prisma" && currentStack.orm !== "none") - reason = "Requires Prisma ORM."; - } else if (techId === "mongodb-atlas") { - if ( - currentStack.database !== "mongodb" && - currentStack.database !== "none" - ) - reason = "Requires MongoDB database."; - if (currentStack.orm !== "prisma" && currentStack.orm !== "mongoose" && currentStack.orm !== "none") - reason = "Requires Prisma or Mongoose ORM."; - } else if (techId === "neon") { - if ( - currentStack.database !== "postgres" && - currentStack.database !== "none" - ) - reason = "Requires PostgreSQL database."; - } + if (catKey === "orm") { + if (stack.database === "none" && techId !== "none") { + addRule( + category, + techId, + "Select a database to enable ORM options.", + ); } if ( - catKey === "auth" && - techId === "true" && - currentStack.database === "none" + stack.database === "mongodb" && + techId !== "prisma" && + techId !== "mongoose" && + techId !== "none" ) { - reason = "Authentication requires a database."; + addRule( + category, + techId, + "MongoDB requires the Prisma or Mongoose ORM.", + ); } - if (catKey === "addons") { - if (techId === "pwa" && !currentHasPWACompatibleFrontend) { - reason = "Requires TanStack Router or React Router frontend."; + if ( + stack.dbSetup === "turso" && + techId !== "drizzle" && + techId !== "none" + ) { + addRule( + category, + techId, + "Turso DB setup requires the Drizzle ORM.", + ); + } + + if ( + stack.dbSetup === "prisma-postgres" && + techId !== "prisma" && + techId !== "none" + ) { + addRule( + category, + techId, + "Prisma PostgreSQL setup requires Prisma ORM.", + ); + } + + if ( + stack.dbSetup === "mongodb-atlas" && + techId !== "prisma" && + techId !== "mongoose" && + techId !== "none" + ) { + addRule( + category, + techId, + "MongoDB Atlas setup requires Prisma or Mongoose ORM.", + ); + } + + if (techId === "none") { + if (stack.database === "mongodb") { + addRule( + category, + techId, + "MongoDB requires Prisma or Mongoose ORM.", + ); } - if (techId === "tauri" && !currentHasTauriCompatibleFrontend) { - reason = - "Requires TanStack Router, React Router, Nuxt or Svelte frontend."; + if (stack.dbSetup === "turso") { + addRule(category, techId, "Turso DB setup requires Drizzle ORM."); + } + if (stack.dbSetup === "prisma-postgres") { + addRule(category, techId, "This DB setup requires Prisma ORM."); } } - if (catKey === "examples") { - const isNativeOnly = - currentHasNativeFrontend && !currentHasWebFrontend; - if (isNativeOnly) { - reason = "Examples are not supported with Native-only frontend."; - } else if ( - (techId === "todo" || techId === "ai") && - !currentHasWebFrontend + if (techId === "mongoose" && stack.database !== "mongodb") { + addRule( + category, + techId, + "Mongoose ORM is not compatible with relational databases.", + ); + } + } + + if (catKey === "dbSetup" && techId !== "none") { + if (stack.database === "none") { + addRule( + category, + techId, + "Select a database before choosing a cloud setup.", + ); + } + + if (techId === "turso") { + if (stack.database !== "sqlite" && stack.database !== "none") { + addRule(category, techId, "Turso requires SQLite database."); + } + if (stack.orm !== "drizzle" && stack.orm !== "none") { + addRule(category, techId, "Turso requires Drizzle ORM."); + } + } else if (techId === "prisma-postgres") { + if (stack.database !== "postgres" && stack.database !== "none") { + addRule(category, techId, "Requires PostgreSQL database."); + } + if (stack.orm !== "prisma" && stack.orm !== "none") { + addRule(category, techId, "Requires Prisma ORM."); + } + } else if (techId === "mongodb-atlas") { + if (stack.database !== "mongodb" && stack.database !== "none") { + addRule(category, techId, "Requires MongoDB database."); + } + if ( + stack.orm !== "prisma" && + stack.orm !== "mongoose" && + stack.orm !== "none" ) { - reason = - "Requires a web frontend (TanStack Router, React Router, etc.)."; - } else if (techId === "todo" && currentStack.database === "none") { - reason = "Todo example requires a database."; - } else if (techId === "ai" && currentStack.backend === "elysia") { - reason = "AI example is not compatible with Elysia backend."; + addRule(category, techId, "Requires Prisma or Mongoose ORM."); + } + } else if (techId === "neon") { + if (stack.database !== "postgres" && stack.database !== "none") { + addRule(category, techId, "Requires PostgreSQL database."); } } } - if (reason) { - reasons.set(`${category}-${techId}`, reason); + if ( + catKey === "auth" && + techId === "true" && + stack.database === "none" + ) { + addRule(category, techId, "Authentication requires a database."); + } + + if (catKey === "addons") { + if (techId === "pwa" && !rules.hasPWACompatible) { + addRule( + category, + techId, + "Requires TanStack Router or React Router frontend.", + ); + } + + if (techId === "tauri" && !rules.hasTauriCompatible) { + addRule( + category, + techId, + "Requires TanStack Router, React Router, Nuxt or Svelte frontend.", + ); + } + } + + if (catKey === "examples") { + if (rules.hasNativeOnly) { + addRule( + category, + techId, + "Examples are not supported with Native-only frontend.", + ); + } else { + if ( + (techId === "todo" || techId === "ai") && + !rules.hasWebFrontend + ) { + addRule( + category, + techId, + "Requires a web frontend (TanStack Router, React Router, etc.).", + ); + } + + if (techId === "todo" && stack.database === "none") { + addRule(category, techId, "Todo example requires a database."); + } + + if (techId === "ai" && stack.backend === "elysia") { + addRule( + category, + techId, + "AI example is not compatible with Elysia backend.", + ); + } + } } } } + return reasons; - })(); + }, [stack, rules]); const selectedBadges = (() => { const badges: React.ReactNode[] = []; @@ -968,8 +1171,10 @@ const StackArchitect = () => { } }, []); + // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { if (compatibilityAnalysis.adjustedStack) { + setLastChanges(compatibilityAnalysis.changes); setStack(compatibilityAnalysis.adjustedStack); } }, [compatibilityAnalysis.adjustedStack, setStack]); @@ -1072,7 +1277,7 @@ const StackArchitect = () => { }); }; - const copyToClipboard = () => { + const copyToClipboard = () => { navigator.clipboard.writeText(command); setCopied(true); setTimeout(() => setCopied(false), 2000); @@ -1089,6 +1294,7 @@ const StackArchitect = () => { const saveCurrentStack = () => { localStorage.setItem("betterTStackPreference", JSON.stringify(stack)); setLastSavedStack(stack); + toast.success("Your stack configuration has been saved"); }; const loadSavedStack = () => { @@ -1098,6 +1304,7 @@ const StackArchitect = () => { setShowPresets(false); setActiveCategory(CATEGORY_ORDER[0]); contentRef.current?.scrollTo(0, 0); + toast.success("Saved configuration loaded"); } }; @@ -1111,6 +1318,7 @@ const StackArchitect = () => { setShowHelp(false); setActiveCategory(CATEGORY_ORDER[0]); contentRef.current?.scrollTo(0, 0); + toast.success(`Applied preset: ${preset.name}`); } }; @@ -1377,7 +1585,7 @@ const StackArchitect = () => { const filteredOptions = categoryOptions.filter((tech) => { if ( - isConvexSelected && + rules.isConvex && tech.id === "none" && ["runtime", "database", "orm", "api", "dbSetup"].includes( categoryKey, @@ -1386,14 +1594,14 @@ const StackArchitect = () => { return false; } if ( - isConvexSelected && + rules.isConvex && categoryKey === "auth" && tech.id === "false" ) { return false; } if ( - isConvexSelected && + rules.isConvex && categoryKey === "examples" && tech.id !== "todo" ) { diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 65a3e2f..5549c89 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -4,6 +4,7 @@ import { Poppins } from "next/font/google"; import { NuqsAdapter } from "nuqs/adapters/next/app"; import type { ReactNode } from "react"; import "./global.css"; +import { Toaster } from "@/components/ui/sonner"; const poppins = Poppins({ subsets: ["latin"], @@ -96,6 +97,7 @@ export default function Layout({ children }: { children: ReactNode }) { }} > {children} + diff --git a/apps/web/src/components/ui/sonner.tsx b/apps/web/src/components/ui/sonner.tsx new file mode 100644 index 0000000..b08c2e8 --- /dev/null +++ b/apps/web/src/components/ui/sonner.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { useTheme } from "next-themes"; +import { Toaster as Sonner, type ToasterProps } from "sonner"; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/bun.lock b/bun.lock index 7786d21..ef8662f 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ }, "apps/cli": { "name": "create-better-t-stack", - "version": "2.5.1", + "version": "2.6.1", "bin": { "create-better-t-stack": "dist/index.js", }, @@ -54,10 +54,12 @@ "lucide-react": "^0.503.0", "motion": "^12.8.0", "next": "15.3.1", + "next-themes": "^0.4.6", "nuqs": "^2.4.3", "react": "^19.1.0", "react-dom": "^19.1.0", "react-tweet": "^3.2.2", + "sonner": "^2.0.3", "tailwind-merge": "^3.2.0", }, "devDependencies": { @@ -1524,6 +1526,8 @@ "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], + "sonner": ["sonner@2.0.3", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],