mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
fix mongodb templates and add migrate and generate scripts
This commit is contained in:
5
.changeset/slow-melons-shine.md
Normal file
5
.changeset/slow-melons-shine.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-better-t-stack": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
add migrate and generate scripts
|
||||||
@@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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}}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}}
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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"
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
25
apps/web/src/components/ui/sonner.tsx
Normal file
25
apps/web/src/components/ui/sonner.tsx
Normal 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 };
|
||||||
6
bun.lock
6
bun.lock
@@ -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=="],
|
||||||
|
|||||||
Reference in New Issue
Block a user