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 =
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";
}
}

View File

@@ -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);

View File

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

View File

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

View File

@@ -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}}

View File

@@ -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);
});
const client = mongoose.connection.getClient().db("myDB");
export { client };

View File

@@ -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({

View File

@@ -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": {

View File

@@ -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<string, { notes: string[]; hasIssue: boolean }>;
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,6 +235,10 @@ 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") {
@@ -236,6 +253,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
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(
@@ -248,6 +269,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
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(
@@ -260,28 +285,44 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
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.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.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(
@@ -294,6 +335,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.dbSetup.hasIssue = true;
nextStack.dbSetup = "none";
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;
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(
@@ -319,6 +368,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
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") {
@@ -330,6 +383,11 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
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.");
@@ -340,6 +398,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
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") {
@@ -351,9 +413,16 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
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.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.",
);
@@ -361,6 +430,11 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
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") {
@@ -374,6 +448,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.database.hasIssue = true;
nextStack.database = "postgres";
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;
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,21 +543,44 @@ 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)",
});
}
}
@@ -504,13 +617,13 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
nextStack.backend === "elysia" &&
uniqueIncompatibleExamples.includes("ai")
) {
notes.backendFramework.notes.push(
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.backendFramework.hasIssue = true;
notes.backend.hasIssue = true;
notes.examples.hasIssue = true;
}
@@ -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<string | null>(
CATEGORY_ORDER[0],
);
const [lastChanges, setLastChanges] = useState<
Array<{ category: string; message: string }>
>([]);
const sectionRefs = useRef<Record<string, HTMLElement | null>>({});
const contentRef = useRef<HTMLDivElement>(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<string, string>();
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<string, string | string[]> = {
const convexDefaults: Record<string, string> = {
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}'.`;
const requiredValue = convexDefaults[catKey as string];
if (techId !== requiredValue && techId !== "none") {
addRule(
category,
techId,
`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.";
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 (
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.`;
if (techId === "none") {
addRule(
category,
techId,
"API 'None' is only available with the Convex backend.",
);
}
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 (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 (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 (stack.database === "none" && techId !== "none") {
addRule(
category,
techId,
"Select a database to enable ORM options.",
);
}
if (techId === "mongoose" && (currentStack.database !== "mongodb")) {
reason = "Mongoose ORM is not compatible with relational databases.";
if (
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 (currentStack.database === "none")
reason = "Select a database before choosing a cloud setup.";
if (stack.database === "none") {
addRule(
category,
techId,
"Select a database before choosing a cloud setup.",
);
}
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.";
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 (
currentStack.database !== "postgres" &&
currentStack.database !== "none"
)
reason = "Requires PostgreSQL database.";
if (currentStack.orm !== "prisma" && currentStack.orm !== "none")
reason = "Requires Prisma ORM.";
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 (
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.";
stack.orm !== "prisma" &&
stack.orm !== "mongoose" &&
stack.orm !== "none"
) {
addRule(category, techId, "Requires Prisma or Mongoose ORM.");
}
} else if (techId === "neon") {
if (
currentStack.database !== "postgres" &&
currentStack.database !== "none"
)
reason = "Requires PostgreSQL database.";
if (stack.database !== "postgres" && stack.database !== "none") {
addRule(category, techId, "Requires PostgreSQL database.");
}
}
}
if (
catKey === "auth" &&
techId === "true" &&
currentStack.database === "none"
stack.database === "none"
) {
reason = "Authentication requires a database.";
addRule(category, techId, "Authentication requires a database.");
}
if (catKey === "addons") {
if (techId === "pwa" && !currentHasPWACompatibleFrontend) {
reason = "Requires TanStack Router or React Router frontend.";
if (techId === "pwa" && !rules.hasPWACompatible) {
addRule(
category,
techId,
"Requires TanStack Router or React Router frontend.",
);
}
if (techId === "tauri" && !currentHasTauriCompatibleFrontend) {
reason =
"Requires TanStack Router, React Router, Nuxt or Svelte frontend.";
if (techId === "tauri" && !rules.hasTauriCompatible) {
addRule(
category,
techId,
"Requires TanStack Router, React Router, Nuxt or Svelte frontend.",
);
}
}
if (catKey === "examples") {
const isNativeOnly =
currentHasNativeFrontend && !currentHasWebFrontend;
if (isNativeOnly) {
reason = "Examples are not supported with Native-only frontend.";
} else if (
if (rules.hasNativeOnly) {
addRule(
category,
techId,
"Examples are not supported with Native-only frontend.",
);
} else {
if (
(techId === "todo" || techId === "ai") &&
!currentHasWebFrontend
!rules.hasWebFrontend
) {
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 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.",
);
}
}
}
}
}
if (reason) {
reasons.set(`${category}-${techId}`, reason);
}
}
}
return reasons;
})();
}, [stack, rules]);
const selectedBadges = (() => {
const badges: React.ReactNode[] = [];
@@ -968,8 +1171,10 @@ const StackArchitect = () => {
}
}, []);
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
if (compatibilityAnalysis.adjustedStack) {
setLastChanges(compatibilityAnalysis.changes);
setStack(compatibilityAnalysis.adjustedStack);
}
}, [compatibilityAnalysis.adjustedStack, setStack]);
@@ -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"
) {

View File

@@ -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 }) {
}}
>
<NuqsAdapter>{children}</NuqsAdapter>
<Toaster />
</RootProvider>
</body>
</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": {
"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=="],