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 =
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -29,6 +29,14 @@
|
||||
"db:studio": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"db:migrate": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"db:generate": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
{{/unless}}{{/if}}
|
||||
}
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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({
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
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": {
|
||||
"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=="],
|
||||
|
||||
Reference in New Issue
Block a user