fix mongodb templates and add migrate and generate scripts

This commit is contained in:
Aman Varshney
2025-05-02 22:17:51 +05:30
parent 437cf9a45a
commit 0cb24b1494
13 changed files with 765 additions and 443 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": patch
---
add migrate and generate scripts

View File

@@ -58,8 +58,8 @@ async function updateRootPackageJson(
const needsDbScripts = const needsDbScripts =
options.backend !== "convex" && options.backend !== "convex" &&
options.database !== "none" && options.database !== "none" &&
options.orm !== "none"; options.orm !== "none" &&
options.orm !== "mongoose";
if (options.addons.includes("turborepo")) { if (options.addons.includes("turborepo")) {
scripts.dev = "turbo dev"; scripts.dev = "turbo dev";
scripts.build = "turbo build"; scripts.build = "turbo build";
@@ -73,6 +73,13 @@ async function updateRootPackageJson(
if (needsDbScripts) { if (needsDbScripts) {
scripts["db:push"] = `turbo -F ${backendPackageName} db:push`; scripts["db:push"] = `turbo -F ${backendPackageName} db:push`;
scripts["db:studio"] = `turbo -F ${backendPackageName} db:studio`; 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") { } else if (options.packageManager === "pnpm") {
scripts.dev = devScript; scripts.dev = devScript;
@@ -87,6 +94,17 @@ async function updateRootPackageJson(
if (needsDbScripts) { if (needsDbScripts) {
scripts["db:push"] = `pnpm --filter ${backendPackageName} db:push`; scripts["db:push"] = `pnpm --filter ${backendPackageName} db:push`;
scripts["db:studio"] = `pnpm --filter ${backendPackageName} db:studio`; 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") { } else if (options.packageManager === "npm") {
scripts.dev = devScript; scripts.dev = devScript;
@@ -102,6 +120,17 @@ async function updateRootPackageJson(
scripts["db:push"] = `npm run db:push --workspace ${backendPackageName}`; scripts["db:push"] = `npm run db:push --workspace ${backendPackageName}`;
scripts["db:studio"] = scripts["db:studio"] =
`npm run db:studio --workspace ${backendPackageName}`; `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") { } else if (options.packageManager === "bun") {
scripts.dev = devScript; scripts.dev = devScript;
@@ -116,6 +145,17 @@ async function updateRootPackageJson(
if (needsDbScripts) { if (needsDbScripts) {
scripts["db:push"] = `bun run --filter ${backendPackageName} db:push`; scripts["db:push"] = `bun run --filter ${backendPackageName} db:push`;
scripts["db:studio"] = `bun run --filter ${backendPackageName} db:studio`; 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") { if (options.orm === "prisma") {
scripts["db:push"] = "prisma db push --schema ./prisma/schema"; scripts["db:push"] = "prisma db push --schema ./prisma/schema";
scripts["db:studio"] = "prisma studio"; scripts["db:studio"] = "prisma studio";
scripts["db:generate"] = "prisma generate --schema ./prisma/schema";
scripts["db:migrate"] = "prisma migrate dev";
} else if (options.orm === "drizzle") { } else if (options.orm === "drizzle") {
scripts["db:push"] = "drizzle-kit push"; scripts["db:push"] = "drizzle-kit push";
scripts["db:studio"] = "drizzle-kit studio"; scripts["db:studio"] = "drizzle-kit studio";
scripts["db:generate"] = "drizzle-kit generate";
scripts["db:migrate"] = "drizzle-kit migrate";
} }
} }

View File

@@ -371,7 +371,7 @@ export async function setupAuthTemplate(
authDbSrc = path.join( authDbSrc = path.join(
PKG_ROOT, PKG_ROOT,
`templates/auth/server/db/mongoose/${db}`, `templates/auth/server/db/mongoose/${db}`,
) );
} }
if (authDbSrc && (await fs.pathExists(authDbSrc))) { if (authDbSrc && (await fs.pathExists(authDbSrc))) {
await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context); await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);

View File

@@ -446,6 +446,10 @@ function processAndValidateFlags(
config.dbSetup = "none"; config.dbSetup = "none";
} }
if (config.orm === "mongoose" && !providedFlags.has("database")) {
config.database = "mongodb";
}
if (effectiveDatabase === "mongodb" && effectiveOrm === "drizzle") { if (effectiveDatabase === "mongodb" && effectiveOrm === "drizzle") {
consola.fatal( consola.fatal(
"Drizzle ORM is not compatible with MongoDB. Please use --orm prisma or --orm mongoose.", "Drizzle ORM is not compatible with MongoDB. Please use --orm prisma or --orm mongoose.",

View File

@@ -29,6 +29,14 @@
"db:studio": { "db:studio": {
"cache": false, "cache": false,
"persistent": true "persistent": true
},
"db:migrate": {
"cache": false,
"persistent": true
},
"db:generate": {
"cache": false,
"persistent": true
} }
{{/unless}}{{/if}} {{/unless}}{{/if}}
} }

View File

@@ -11,6 +11,7 @@ export const auth = betterAuth({
{{#if (eq database "postgres")}}provider: "postgresql"{{/if}} {{#if (eq database "postgres")}}provider: "postgresql"{{/if}}
{{#if (eq database "sqlite")}}provider: "sqlite"{{/if}} {{#if (eq database "sqlite")}}provider: "sqlite"{{/if}}
{{#if (eq database "mysql")}}provider: "mysql"{{/if}} {{#if (eq database "mysql")}}provider: "mysql"{{/if}}
{{#if (eq database "mongodb")}}provider: "mongodb"{{/if}}
}), }),
trustedOrigins: [ trustedOrigins: [
process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}} process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}}
@@ -67,7 +68,7 @@ import { expo } from "@better-auth/expo";
import { client } from "../db"; import { client } from "../db";
export const auth = betterAuth({ export const auth = betterAuth({
database: mongodbAdapter(client.db()), database: mongodbAdapter(client),
trustedOrigins: [ trustedOrigins: [
process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}} process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}}
"my-better-t-app://",{{/if}} "my-better-t-app://",{{/if}}

View File

@@ -1,6 +1,9 @@
import mongoose from 'mongoose'; import mongoose from "mongoose";
await mongoose.connect(process.env.DATABASE_URL || ""); await mongoose.connect(process.env.DATABASE_URL || "").catch((error) => {
const client = mongoose.connection.getClient(); console.log("Error connecting to database:", error);
});
const client = mongoose.connection.getClient().db("myDB");
export { client }; export { client };

View File

@@ -23,7 +23,11 @@ export const todoRouter = {
}), }),
toggle: publicProcedure 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() })) .input(z.object({ id: z.number(), completed: z.boolean() }))
{{/if}}
.handler(async ({ input }) => { .handler(async ({ input }) => {
await prisma.todo.update({ await prisma.todo.update({
where: { id: input.id }, where: { id: input.id },
@@ -33,7 +37,11 @@ export const todoRouter = {
}), }),
delete: publicProcedure delete: publicProcedure
{{#if (eq database "mongodb")}}
.input(z.object({ id: z.string() }))
{{else}}
.input(z.object({ id: z.number() })) .input(z.object({ id: z.number() }))
{{/if}}
.handler(async ({ input }) => { .handler(async ({ input }) => {
await prisma.todo.delete({ await prisma.todo.delete({
where: { id: input.id }, where: { id: input.id },
@@ -69,7 +77,11 @@ export const todoRouter = router({
}), }),
toggle: publicProcedure 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() })) .input(z.object({ id: z.number(), completed: z.boolean() }))
{{/if}}
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
return await prisma.todo.update({ return await prisma.todo.update({
@@ -85,7 +97,11 @@ export const todoRouter = router({
}), }),
delete: publicProcedure delete: publicProcedure
{{#if (eq database "mongodb")}}
.input(z.object({ id: z.string() }))
{{else}}
.input(z.object({ id: z.number() })) .input(z.object({ id: z.number() }))
{{/if}}
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
try { try {
return await prisma.todo.delete({ return await prisma.todo.delete({

View File

@@ -22,10 +22,12 @@
"lucide-react": "^0.503.0", "lucide-react": "^0.503.0",
"motion": "^12.8.0", "motion": "^12.8.0",
"next": "15.3.1", "next": "15.3.1",
"next-themes": "^0.4.6",
"nuqs": "^2.4.3", "nuqs": "^2.4.3",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-tweet": "^3.2.2", "react-tweet": "^3.2.2",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0" "tailwind-merge": "^3.2.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -34,7 +34,8 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useQueryStates } from "nuqs"; import { useQueryStates } from "nuqs";
import type React from "react"; 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 validateProjectName = (name: string): string | undefined => {
const INVALID_CHARS = ["<", ">", ":", '"', "|", "?", "*"]; const INVALID_CHARS = ["<", ">", ":", '"', "|", "?", "*"];
@@ -164,12 +165,15 @@ const getCategoryDisplayName = (categoryKey: string): string => {
interface CompatibilityResult { interface CompatibilityResult {
adjustedStack: StackState | null; adjustedStack: StackState | null;
notes: Record<string, { notes: string[]; hasIssue: boolean }>; notes: Record<string, { notes: string[]; hasIssue: boolean }>;
changes: Array<{ category: string; message: string }>;
} }
const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
const nextStack = { ...stack }; const nextStack = { ...stack };
let changed = false; let changed = false;
const notes: CompatibilityResult["notes"] = {}; const notes: CompatibilityResult["notes"] = {};
const changes: Array<{ category: string; message: string }> = [];
for (const cat of CATEGORY_ORDER) { for (const cat of CATEGORY_ORDER) {
notes[cat] = { notes: [], hasIssue: false }; notes[cat] = { notes: [], hasIssue: false };
} }
@@ -190,20 +194,25 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
for (const [key, value] of Object.entries(convexOverrides)) { for (const [key, value] of Object.entries(convexOverrides)) {
const catKey = key as keyof StackState; const catKey = key as keyof StackState;
if (JSON.stringify(nextStack[catKey]) !== JSON.stringify(value)) { 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( notes[catKey].notes.push(
`Convex backend selected: ${getCategoryDisplayName( `Convex backend selected: ${displayName} will be set to '${valueDisplay}'.`,
catKey,
)} will be set to '${Array.isArray(value) ? value.join(", ") : value}'.`,
); );
notes.backend.notes.push( notes.backend.notes.push(
`Convex requires ${getCategoryDisplayName(catKey)} to be '${ `Convex requires ${displayName} to be '${valueDisplay}'.`,
Array.isArray(value) ? value.join(", ") : value
}'.`,
); );
notes[catKey].hasIssue = true; notes[catKey].hasIssue = true;
notes.backend.hasIssue = true; notes.backend.hasIssue = true;
(nextStack[catKey] as string | string[]) = value; (nextStack[catKey] as string | string[]) = value;
changed = true; changed = true;
changes.push({
category: "convex",
message,
});
} }
} }
} else { } else {
@@ -214,6 +223,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.runtime.hasIssue = true; notes.runtime.hasIssue = true;
nextStack.runtime = DEFAULT_STACK.runtime; nextStack.runtime = DEFAULT_STACK.runtime;
changed = true; changed = true;
changes.push({
category: "runtime",
message: "Runtime set to 'Bun' (None is only for Convex)",
});
} }
if (nextStack.api === "none") { if (nextStack.api === "none") {
notes.api.notes.push( notes.api.notes.push(
@@ -222,6 +235,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.api.hasIssue = true; notes.api.hasIssue = true;
nextStack.api = DEFAULT_STACK.api; nextStack.api = DEFAULT_STACK.api;
changed = true; changed = true;
changes.push({
category: "api",
message: "API set to 'tRPC' (None is only for Convex)",
});
} }
if (nextStack.database === "none") { if (nextStack.database === "none") {
@@ -236,6 +253,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.orm.hasIssue = true; notes.orm.hasIssue = true;
nextStack.orm = "none"; nextStack.orm = "none";
changed = true; changed = true;
changes.push({
category: "database",
message: "ORM set to 'None' (requires a database)",
});
} }
if (nextStack.auth === "true") { if (nextStack.auth === "true") {
notes.database.notes.push( notes.database.notes.push(
@@ -248,6 +269,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.auth.hasIssue = true; notes.auth.hasIssue = true;
nextStack.auth = "false"; nextStack.auth = "false";
changed = true; changed = true;
changes.push({
category: "database",
message: "Authentication disabled (requires a database)",
});
} }
if (nextStack.dbSetup !== "none") { if (nextStack.dbSetup !== "none") {
notes.database.notes.push( notes.database.notes.push(
@@ -260,28 +285,44 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.dbSetup.hasIssue = true; notes.dbSetup.hasIssue = true;
nextStack.dbSetup = "none"; nextStack.dbSetup = "none";
changed = true; changed = true;
changes.push({
category: "database",
message: "DB Setup set to 'None' (requires a database)",
});
} }
} else if (nextStack.database === "mongodb") { } else if (nextStack.database === "mongodb") {
if (nextStack.orm !== "prisma" && nextStack.orm !== "mongoose") { if (nextStack.orm !== "prisma" && nextStack.orm !== "mongoose") {
notes.database.notes.push( notes.database.notes.push(
"MongoDB requires Prisma or Mongoose ORM. Prisma will be selected.", "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.orm.notes.push(
"MongoDB requires Prisma or Mongoose ORM. Prisma will be selected.",
);
notes.database.hasIssue = true; notes.database.hasIssue = true;
notes.orm.hasIssue = true; notes.orm.hasIssue = true;
nextStack.orm = "prisma"; nextStack.orm = "prisma";
changed = true; changed = true;
changes.push({
category: "database",
message: "ORM set to 'Prisma' (MongoDB requires Prisma or Mongoose)",
});
} }
} else { } else {
if (nextStack.orm === "mongoose") { if (nextStack.orm === "mongoose") {
notes.database.notes.push( notes.database.notes.push(
"Relational databases are not compatible with Mongoose ORM", "Relational databases are not compatible with Mongoose ORM",
); );
notes.orm.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.database.hasIssue = true;
notes.orm.hasIssue = true; notes.orm.hasIssue = true;
nextStack.orm = "prisma"; nextStack.orm = "prisma";
changed = true; changed = true;
changes.push({
category: "database",
message: "ORM set to 'Prisma' (Mongoose only works with MongoDB)",
});
} }
if (nextStack.dbSetup === "mongodb-atlas") { if (nextStack.dbSetup === "mongodb-atlas") {
notes.database.notes.push( notes.database.notes.push(
@@ -294,6 +335,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.dbSetup.hasIssue = true; notes.dbSetup.hasIssue = true;
nextStack.dbSetup = "none"; nextStack.dbSetup = "none";
changed = true; changed = true;
changes.push({
category: "database",
message: "DB Setup reset to 'None' (MongoDB Atlas requires MongoDB)",
});
} }
} }
@@ -307,6 +352,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.database.hasIssue = true; notes.database.hasIssue = true;
nextStack.database = "sqlite"; nextStack.database = "sqlite";
changed = true; changed = true;
changes.push({
category: "dbSetup",
message: "Database set to 'SQLite' (required by Turso)",
});
} }
if (nextStack.orm !== "drizzle") { if (nextStack.orm !== "drizzle") {
notes.dbSetup.notes.push( notes.dbSetup.notes.push(
@@ -319,6 +368,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.orm.hasIssue = true; notes.orm.hasIssue = true;
nextStack.orm = "drizzle"; nextStack.orm = "drizzle";
changed = true; changed = true;
changes.push({
category: "dbSetup",
message: "ORM set to 'Drizzle' (required by Turso)",
});
} }
} else if (nextStack.dbSetup === "prisma-postgres") { } else if (nextStack.dbSetup === "prisma-postgres") {
if (nextStack.database !== "postgres") { if (nextStack.database !== "postgres") {
@@ -330,6 +383,11 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.database.hasIssue = true; notes.database.hasIssue = true;
nextStack.database = "postgres"; nextStack.database = "postgres";
changed = true; changed = true;
changes.push({
category: "dbSetup",
message:
"Database set to 'PostgreSQL' (required by Prisma PostgreSQL setup)",
});
} }
if (nextStack.orm !== "prisma") { if (nextStack.orm !== "prisma") {
notes.dbSetup.notes.push("Requires Prisma ORM. It will be selected."); notes.dbSetup.notes.push("Requires Prisma ORM. It will be selected.");
@@ -340,6 +398,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.orm.hasIssue = true; notes.orm.hasIssue = true;
nextStack.orm = "prisma"; nextStack.orm = "prisma";
changed = true; changed = true;
changes.push({
category: "dbSetup",
message: "ORM set to 'Prisma' (required by Prisma PostgreSQL setup)",
});
} }
} else if (nextStack.dbSetup === "mongodb-atlas") { } else if (nextStack.dbSetup === "mongodb-atlas") {
if (nextStack.database !== "mongodb") { if (nextStack.database !== "mongodb") {
@@ -351,9 +413,16 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.database.hasIssue = true; notes.database.hasIssue = true;
nextStack.database = "mongodb"; nextStack.database = "mongodb";
changed = true; changed = true;
changes.push({
category: "dbSetup",
message:
"Database set to 'MongoDB' (required by MongoDB Atlas setup)",
});
} }
if (nextStack.orm !== "prisma" && nextStack.orm !== "mongoose") { if (nextStack.orm !== "prisma" && nextStack.orm !== "mongoose") {
notes.dbSetup.notes.push("Requires Prisma or Mongoose ORM. Prisma will be selected."); notes.dbSetup.notes.push(
"Requires Prisma or Mongoose ORM. Prisma will be selected.",
);
notes.orm.notes.push( notes.orm.notes.push(
"MongoDB Atlas setup requires Prisma or Mongoose ORM. Prisma will be selected.", "MongoDB Atlas setup requires Prisma or Mongoose ORM. Prisma will be selected.",
); );
@@ -361,6 +430,11 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.orm.hasIssue = true; notes.orm.hasIssue = true;
nextStack.orm = "prisma"; nextStack.orm = "prisma";
changed = true; changed = true;
changes.push({
category: "dbSetup",
message:
"ORM set to 'Prisma' (MongoDB Atlas requires Prisma or Mongoose)",
});
} }
} else if (nextStack.dbSetup === "neon") { } else if (nextStack.dbSetup === "neon") {
if (nextStack.database !== "postgres") { if (nextStack.database !== "postgres") {
@@ -374,6 +448,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.database.hasIssue = true; notes.database.hasIssue = true;
nextStack.database = "postgres"; nextStack.database = "postgres";
changed = true; changed = true;
changes.push({
category: "dbSetup",
message: "Database set to 'PostgreSQL' (required by Neon)",
});
} }
} }
@@ -391,6 +469,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.frontend.hasIssue = true; notes.frontend.hasIssue = true;
nextStack.api = "orpc"; nextStack.api = "orpc";
changed = true; changed = true;
changes.push({
category: "api",
message: `API set to 'oRPC' (required by ${frontendName})`,
});
} }
const incompatibleAddons: string[] = []; const incompatibleAddons: string[] = [];
@@ -407,6 +489,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
); );
notes.frontend.hasIssue = true; notes.frontend.hasIssue = true;
notes.addons.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")) { if (!isTauriCompat && nextStack.addons.includes("tauri")) {
incompatibleAddons.push("tauri"); incompatibleAddons.push("tauri");
@@ -418,6 +504,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
); );
notes.frontend.hasIssue = true; notes.frontend.hasIssue = true;
notes.addons.hasIssue = true; notes.addons.hasIssue = true;
changes.push({
category: "addons",
message: "Tauri addon removed (requires compatible frontend)",
});
} }
const originalAddonsLength = nextStack.addons.length; const originalAddonsLength = nextStack.addons.length;
@@ -453,21 +543,44 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.frontend.hasIssue = true; notes.frontend.hasIssue = true;
notes.examples.hasIssue = true; notes.examples.hasIssue = true;
incompatibleExamples.push(...nextStack.examples); incompatibleExamples.push(...nextStack.examples);
changes.push({
category: "examples",
message: "Examples removed (not supported with Native-only frontend)",
});
} }
} else { } else {
if (!isWeb) { if (!isWeb) {
if (nextStack.examples.includes("todo")) if (nextStack.examples.includes("todo")) {
incompatibleExamples.push("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 ( if (
nextStack.database === "none" && nextStack.database === "none" &&
nextStack.examples.includes("todo") nextStack.examples.includes("todo")
) { ) {
incompatibleExamples.push("todo"); incompatibleExamples.push("todo");
changes.push({
category: "examples",
message: "Todo example removed (requires a database)",
});
} }
if (nextStack.backend === "elysia" && nextStack.examples.includes("ai")) { if (nextStack.backend === "elysia" && nextStack.examples.includes("ai")) {
incompatibleExamples.push("ai"); incompatibleExamples.push("ai");
changes.push({
category: "examples",
message: "AI example removed (not compatible with Elysia)",
});
} }
} }
@@ -504,13 +617,13 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
nextStack.backend === "elysia" && nextStack.backend === "elysia" &&
uniqueIncompatibleExamples.includes("ai") uniqueIncompatibleExamples.includes("ai")
) { ) {
notes.backendFramework.notes.push( notes.backend.notes.push(
"AI example is not compatible with Elysia. It will be removed.", "AI example is not compatible with Elysia. It will be removed.",
); );
notes.examples.notes.push( notes.examples.notes.push(
"AI example is not compatible with Elysia. It will be removed.", "AI example is not compatible with Elysia. It will be removed.",
); );
notes.backendFramework.hasIssue = true; notes.backend.hasIssue = true;
notes.examples.hasIssue = true; notes.examples.hasIssue = true;
} }
@@ -525,6 +638,25 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
return { return {
adjustedStack: changed ? nextStack : null, adjustedStack: changed ? nextStack : null,
notes, 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<string | null>( const [activeCategory, setActiveCategory] = useState<string | null>(
CATEGORY_ORDER[0], CATEGORY_ORDER[0],
); );
const [lastChanges, setLastChanges] = useState<
Array<{ category: string; message: string }>
>([]);
const sectionRefs = useRef<Record<string, HTMLElement | null>>({}); const sectionRefs = useRef<Record<string, HTMLElement | null>>({});
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const compatibilityAnalysis = analyzeStackCompatibility(stack); const compatibilityAnalysis = useMemo(
const isConvexSelected = stack.backend === "convex"; () => analyzeStackCompatibility(stack),
const currentHasWebFrontend = hasWebFrontend(stack.frontend); [stack],
const currentHasNativeFrontend = hasNativeFrontend(stack.frontend);
const currentHasPWACompatibleFrontend = hasPWACompatibleFrontend(
stack.frontend,
);
const currentHasTauriCompatibleFrontend = hasTauriCompatibleFrontend(
stack.frontend,
); );
const disabledReasons = (() => { const rules = useMemo(() => getCompatibilityRules(stack), [stack]);
const disabledReasons = useMemo(() => {
const reasons = new Map<string, string>(); const reasons = new Map<string, string>();
const currentStack = stack;
const addRule = (category: string, techId: string, reason: string) => {
reasons.set(`${category}-${techId}`, reason);
};
for (const category of CATEGORY_ORDER) { for (const category of CATEGORY_ORDER) {
const categoryOptions = TECH_OPTIONS[category] || []; const options = TECH_OPTIONS[category] || [];
const catKey = category as keyof StackState; const catKey = category as keyof StackState;
for (const tech of categoryOptions) { for (const tech of options) {
let reason: string | null = null;
const techId = tech.id; const techId = tech.id;
if (isConvexSelected) { if (rules.isConvex) {
if ( if (
[ ["runtime", "database", "orm", "api", "auth", "dbSetup"].includes(
"runtime", catKey,
"database", )
"orm",
"api",
"auth",
"dbSetup",
"examples",
].includes(catKey)
) { ) {
const convexDefaults: Record<string, string | string[]> = { const convexDefaults: Record<string, string> = {
runtime: "none", runtime: "none",
database: "none", database: "none",
orm: "none", orm: "none",
api: "none", api: "none",
auth: "false", auth: "false",
dbSetup: "none", dbSetup: "none",
examples: ["todo"],
}; };
const requiredValue = convexDefaults[catKey];
if ( const requiredValue = convexDefaults[catKey as string];
typeof requiredValue === "string" && if (techId !== requiredValue && techId !== "none") {
techId !== requiredValue && addRule(
techId !== "none" category,
) { techId,
if (!(catKey === "dbSetup" && techId === "none")) { `Convex backend requires ${getCategoryDisplayName(catKey)} to be '${requiredValue}'.`,
reason = `Convex backend requires ${getCategoryDisplayName( );
catKey,
)} to be '${requiredValue}'.`;
} }
} else if (Array.isArray(requiredValue)) { }
if (catKey === "examples" && techId !== "todo") { if (catKey === "examples" && techId !== "todo") {
reason = "Convex backend only supports the 'Todo' example."; addRule(
category,
techId,
"Convex backend only supports the 'Todo' example.",
);
} }
continue;
} }
if (catKey === "runtime" && techId === "none") {
addRule(
category,
techId,
"Runtime 'None' is only available with the Convex backend.",
);
} }
} 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 (catKey === "api") {
if ( if (techId === "none") {
techId === "trpc" && addRule(
(currentStack.frontend.includes("nuxt") || category,
currentStack.frontend.includes("svelte")) techId,
) { "API 'None' is only available with the Convex backend.",
reason = `tRPC is not supported with ${ );
currentStack.frontend.includes("nuxt") ? "Nuxt" : "Svelte" }
}. Use oRPC instead.`;
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 (catKey === "orm") { if (catKey === "orm") {
if (currentStack.database === "none" && techId !== "none") if (stack.database === "none" && techId !== "none") {
reason = "Select a database to enable ORM options."; addRule(
if ( category,
currentStack.database === "mongodb" && techId,
techId !== "prisma" && "Select a database to enable ORM options.",
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 (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 (techId === "mongoose" && (currentStack.database !== "mongodb")) { if (
reason = "Mongoose ORM is not compatible with relational databases."; stack.database === "mongodb" &&
techId !== "prisma" &&
techId !== "mongoose" &&
techId !== "none"
) {
addRule(
category,
techId,
"MongoDB requires the Prisma or Mongoose ORM.",
);
}
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 (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 (techId === "mongoose" && stack.database !== "mongodb") {
addRule(
category,
techId,
"Mongoose ORM is not compatible with relational databases.",
);
} }
} }
if (catKey === "dbSetup" && techId !== "none") { if (catKey === "dbSetup" && techId !== "none") {
if (currentStack.database === "none") if (stack.database === "none") {
reason = "Select a database before choosing a cloud setup."; addRule(
category,
techId,
"Select a database before choosing a cloud setup.",
);
}
if (techId === "turso") { if (techId === "turso") {
if ( if (stack.database !== "sqlite" && stack.database !== "none") {
currentStack.database !== "sqlite" && addRule(category, techId, "Turso requires SQLite database.");
currentStack.database !== "none" }
) if (stack.orm !== "drizzle" && stack.orm !== "none") {
reason = "Turso requires SQLite database."; addRule(category, techId, "Turso requires Drizzle ORM.");
if (currentStack.orm !== "drizzle" && currentStack.orm !== "none") }
reason = "Turso requires Drizzle ORM.";
} else if (techId === "prisma-postgres") { } else if (techId === "prisma-postgres") {
if ( if (stack.database !== "postgres" && stack.database !== "none") {
currentStack.database !== "postgres" && addRule(category, techId, "Requires PostgreSQL database.");
currentStack.database !== "none" }
) if (stack.orm !== "prisma" && stack.orm !== "none") {
reason = "Requires PostgreSQL database."; addRule(category, techId, "Requires Prisma ORM.");
if (currentStack.orm !== "prisma" && currentStack.orm !== "none") }
reason = "Requires Prisma ORM.";
} else if (techId === "mongodb-atlas") { } else if (techId === "mongodb-atlas") {
if (stack.database !== "mongodb" && stack.database !== "none") {
addRule(category, techId, "Requires MongoDB database.");
}
if ( if (
currentStack.database !== "mongodb" && stack.orm !== "prisma" &&
currentStack.database !== "none" stack.orm !== "mongoose" &&
) stack.orm !== "none"
reason = "Requires MongoDB database."; ) {
if (currentStack.orm !== "prisma" && currentStack.orm !== "mongoose" && currentStack.orm !== "none") addRule(category, techId, "Requires Prisma or Mongoose ORM.");
reason = "Requires Prisma or Mongoose ORM."; }
} else if (techId === "neon") { } else if (techId === "neon") {
if ( if (stack.database !== "postgres" && stack.database !== "none") {
currentStack.database !== "postgres" && addRule(category, techId, "Requires PostgreSQL database.");
currentStack.database !== "none" }
)
reason = "Requires PostgreSQL database.";
} }
} }
if ( if (
catKey === "auth" && catKey === "auth" &&
techId === "true" && techId === "true" &&
currentStack.database === "none" stack.database === "none"
) { ) {
reason = "Authentication requires a database."; addRule(category, techId, "Authentication requires a database.");
} }
if (catKey === "addons") { if (catKey === "addons") {
if (techId === "pwa" && !currentHasPWACompatibleFrontend) { if (techId === "pwa" && !rules.hasPWACompatible) {
reason = "Requires TanStack Router or React Router frontend."; addRule(
category,
techId,
"Requires TanStack Router or React Router frontend.",
);
} }
if (techId === "tauri" && !currentHasTauriCompatibleFrontend) {
reason = if (techId === "tauri" && !rules.hasTauriCompatible) {
"Requires TanStack Router, React Router, Nuxt or Svelte frontend."; addRule(
category,
techId,
"Requires TanStack Router, React Router, Nuxt or Svelte frontend.",
);
} }
} }
if (catKey === "examples") { if (catKey === "examples") {
const isNativeOnly = if (rules.hasNativeOnly) {
currentHasNativeFrontend && !currentHasWebFrontend; addRule(
if (isNativeOnly) { category,
reason = "Examples are not supported with Native-only frontend."; techId,
} else if ( "Examples are not supported with Native-only frontend.",
);
} else {
if (
(techId === "todo" || techId === "ai") && (techId === "todo" || techId === "ai") &&
!currentHasWebFrontend !rules.hasWebFrontend
) { ) {
reason = addRule(
"Requires a web frontend (TanStack Router, React Router, etc.)."; category,
} else if (techId === "todo" && currentStack.database === "none") { techId,
reason = "Todo example requires a database."; "Requires a web frontend (TanStack Router, React Router, etc.).",
} else if (techId === "ai" && currentStack.backend === "elysia") { );
reason = "AI example is not compatible with Elysia backend."; }
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.",
);
}
}
} }
} }
} }
if (reason) {
reasons.set(`${category}-${techId}`, reason);
}
}
}
return reasons; return reasons;
})(); }, [stack, rules]);
const selectedBadges = (() => { const selectedBadges = (() => {
const badges: React.ReactNode[] = []; const badges: React.ReactNode[] = [];
@@ -968,8 +1171,10 @@ const StackArchitect = () => {
} }
}, []); }, []);
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => { useEffect(() => {
if (compatibilityAnalysis.adjustedStack) { if (compatibilityAnalysis.adjustedStack) {
setLastChanges(compatibilityAnalysis.changes);
setStack(compatibilityAnalysis.adjustedStack); setStack(compatibilityAnalysis.adjustedStack);
} }
}, [compatibilityAnalysis.adjustedStack, setStack]); }, [compatibilityAnalysis.adjustedStack, setStack]);
@@ -1089,6 +1294,7 @@ const StackArchitect = () => {
const saveCurrentStack = () => { const saveCurrentStack = () => {
localStorage.setItem("betterTStackPreference", JSON.stringify(stack)); localStorage.setItem("betterTStackPreference", JSON.stringify(stack));
setLastSavedStack(stack); setLastSavedStack(stack);
toast.success("Your stack configuration has been saved");
}; };
const loadSavedStack = () => { const loadSavedStack = () => {
@@ -1098,6 +1304,7 @@ const StackArchitect = () => {
setShowPresets(false); setShowPresets(false);
setActiveCategory(CATEGORY_ORDER[0]); setActiveCategory(CATEGORY_ORDER[0]);
contentRef.current?.scrollTo(0, 0); contentRef.current?.scrollTo(0, 0);
toast.success("Saved configuration loaded");
} }
}; };
@@ -1111,6 +1318,7 @@ const StackArchitect = () => {
setShowHelp(false); setShowHelp(false);
setActiveCategory(CATEGORY_ORDER[0]); setActiveCategory(CATEGORY_ORDER[0]);
contentRef.current?.scrollTo(0, 0); contentRef.current?.scrollTo(0, 0);
toast.success(`Applied preset: ${preset.name}`);
} }
}; };
@@ -1377,7 +1585,7 @@ const StackArchitect = () => {
const filteredOptions = categoryOptions.filter((tech) => { const filteredOptions = categoryOptions.filter((tech) => {
if ( if (
isConvexSelected && rules.isConvex &&
tech.id === "none" && tech.id === "none" &&
["runtime", "database", "orm", "api", "dbSetup"].includes( ["runtime", "database", "orm", "api", "dbSetup"].includes(
categoryKey, categoryKey,
@@ -1386,14 +1594,14 @@ const StackArchitect = () => {
return false; return false;
} }
if ( if (
isConvexSelected && rules.isConvex &&
categoryKey === "auth" && categoryKey === "auth" &&
tech.id === "false" tech.id === "false"
) { ) {
return false; return false;
} }
if ( if (
isConvexSelected && rules.isConvex &&
categoryKey === "examples" && categoryKey === "examples" &&
tech.id !== "todo" tech.id !== "todo"
) { ) {

View File

@@ -4,6 +4,7 @@ import { Poppins } from "next/font/google";
import { NuqsAdapter } from "nuqs/adapters/next/app"; import { NuqsAdapter } from "nuqs/adapters/next/app";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import "./global.css"; import "./global.css";
import { Toaster } from "@/components/ui/sonner";
const poppins = Poppins({ const poppins = Poppins({
subsets: ["latin"], subsets: ["latin"],
@@ -96,6 +97,7 @@ export default function Layout({ children }: { children: ReactNode }) {
}} }}
> >
<NuqsAdapter>{children}</NuqsAdapter> <NuqsAdapter>{children}</NuqsAdapter>
<Toaster />
</RootProvider> </RootProvider>
</body> </body>
</html> </html>

View File

@@ -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 (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
);
};
export { Toaster };

View File

@@ -14,7 +14,7 @@
}, },
"apps/cli": { "apps/cli": {
"name": "create-better-t-stack", "name": "create-better-t-stack",
"version": "2.5.1", "version": "2.6.1",
"bin": { "bin": {
"create-better-t-stack": "dist/index.js", "create-better-t-stack": "dist/index.js",
}, },
@@ -54,10 +54,12 @@
"lucide-react": "^0.503.0", "lucide-react": "^0.503.0",
"motion": "^12.8.0", "motion": "^12.8.0",
"next": "15.3.1", "next": "15.3.1",
"next-themes": "^0.4.6",
"nuqs": "^2.4.3", "nuqs": "^2.4.3",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-tweet": "^3.2.2", "react-tweet": "^3.2.2",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0", "tailwind-merge": "^3.2.0",
}, },
"devDependencies": { "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=="], "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": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],