diff --git a/.changeset/plain-symbols-hide.md b/.changeset/plain-symbols-hide.md new file mode 100644 index 0000000..f391742 --- /dev/null +++ b/.changeset/plain-symbols-hide.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": minor +--- + +Add express backend, mongodb database and automated mongodb atlas setup diff --git a/apps/cli/README.md b/apps/cli/README.md index 3b23bed..9117d5c 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -24,10 +24,10 @@ Follow the prompts to configure your project or use the `--yes` flag for default - **Monorepo**: Turborepo for optimized build system and workspace management - **Frontend**: React, TanStack Router, TanStack Query, Tailwind CSS with shadcn/ui components - **Native Apps**: Create React Native apps with Expo for iOS and Android -- **Backend Frameworks**: Choose between Hono or Elysia +- **Backend Frameworks**: Choose between Hono, Express, or Elysia - **API Layer**: End-to-end type safety with tRPC - **Runtime Options**: Choose between Bun or Node.js for your server -- **Database Options**: SQLite (via Turso), PostgreSQL, or no database +- **Database Options**: SQLite (via Turso), PostgreSQL, MongoDB, or no database - **ORM Selection**: Choose between Drizzle ORM or Prisma - **Authentication**: Optional auth setup with Better-Auth - **Progressive Web App**: Add PWA support with service workers and installable apps @@ -45,7 +45,7 @@ Usage: create-better-t-stack [project-directory] [options] Options: -V, --version Output the version number -y, --yes Use default configuration - --database Database type (none, sqlite, postgres) + --database Database type (none, sqlite, postgres, mongodb) --orm ORM type (none, drizzle, prisma) --auth Include authentication --no-auth Exclude authentication @@ -58,11 +58,8 @@ Options: --package-manager Package manager (npm, pnpm, bun) --install Install dependencies --no-install Skip installing dependencies - --turso Set up Turso for SQLite database - --no-turso Skip Turso setup - --prisma-postgres Set up Prisma Postgres - --no-prisma-postgres Skip Prisma Postgres setup - --backend Backend framework (hono, elysia) + --db-setup Database setup (turso, prisma-postgres, mongodb-atlas, none) + --backend Backend framework (hono, express, elysia) --runtime Runtime (bun, node) -h, --help Display help ``` @@ -93,3 +90,8 @@ Create a project with examples: ```bash npx create-better-t-stack my-app --examples todo ai ``` + +Create a project with Turso database setup: +```bash +npx create-better-t-stack my-app --db-setup turso +``` diff --git a/apps/cli/package.json b/apps/cli/package.json index d114f75..31288ca 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -7,10 +7,7 @@ "bin": { "create-better-t-stack": "dist/index.js" }, - "files": [ - "template", - "dist" - ], + "files": ["template", "dist"], "keywords": [ "better-t-stack", "typescript", diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index 52f300e..2b503f3 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -18,8 +18,7 @@ export const DEFAULT_CONFIG: ProjectConfig = { git: true, packageManager: getUserPkgManager(), noInstall: false, - turso: false, - prismaPostgres: false, + dbSetup: "none", backend: "hono", runtime: "bun", }; @@ -62,6 +61,11 @@ export const dependencyVersionMap = { "@hono/trpc-server": "^0.3.4", hono: "^4.7.5", + cors: "^2.8.5", + express: "^5.1.0", + "@types/express": "^5.0.1", + "@types/cors": "^2.8.17", + ai: "^4.2.8", "@ai-sdk/google": "^1.2.3", diff --git a/apps/cli/src/helpers/backend-framework-setup.ts b/apps/cli/src/helpers/backend-framework-setup.ts index fa611f6..6347951 100644 --- a/apps/cli/src/helpers/backend-framework-setup.ts +++ b/apps/cli/src/helpers/backend-framework-setup.ts @@ -27,6 +27,13 @@ export async function setupBackendDependencies( dependencies.push("@elysiajs/node"); devDependencies.push("tsx", "@types/node"); } + } else if (framework === "express") { + dependencies.push("express", "cors"); + devDependencies.push("@types/express", "@types/cors"); + + if (runtime === "node") { + devDependencies.push("tsx", "@types/node"); + } } if (runtime === "bun") { diff --git a/apps/cli/src/helpers/create-project.ts b/apps/cli/src/helpers/create-project.ts index 61e2193..76ac0d9 100644 --- a/apps/cli/src/helpers/create-project.ts +++ b/apps/cli/src/helpers/create-project.ts @@ -54,8 +54,9 @@ export async function createProject(options: ProjectConfig): Promise { options.database, options.orm, options.packageManager, - options.turso ?? options.database === "sqlite", - options.prismaPostgres, + options.dbSetup === "turso", + options.dbSetup === "prisma-postgres", + options.dbSetup === "mongodb-atlas", ); await setupAuthTemplate( diff --git a/apps/cli/src/helpers/db-setup.ts b/apps/cli/src/helpers/db-setup.ts index c67b57a..c1d6fd0 100644 --- a/apps/cli/src/helpers/db-setup.ts +++ b/apps/cli/src/helpers/db-setup.ts @@ -8,6 +8,7 @@ import type { ProjectPackageManager, } from "../types"; import { addPackageDependency } from "../utils/add-package-deps"; +import { setupMongoDBAtlas } from "./mongodb-atlas-setup"; import { setupPrismaPostgres } from "./prisma-postgres-setup"; import { setupTurso } from "./turso-setup"; @@ -18,6 +19,7 @@ export async function setupDatabase( packageManager: ProjectPackageManager, setupTursoDb: boolean, setupPrismaPostgresDb: boolean, + setupMongoDBAtlasDb: boolean, ): Promise { const s = spinner(); const serverDir = path.join(projectDir, "apps/server"); @@ -68,6 +70,18 @@ export async function setupDatabase( await setupPrismaPostgres(projectDir, packageManager); } } + } else if (databaseType === "mongodb") { + if (orm === "prisma") { + addPackageDependency({ + dependencies: ["@prisma/client"], + devDependencies: ["prisma"], + projectDir: serverDir, + }); + } + + if (setupMongoDBAtlasDb) { + await setupMongoDBAtlas(projectDir); + } } } catch (error) { s.stop(pc.red("Failed to set up database")); diff --git a/apps/cli/src/helpers/env-setup.ts b/apps/cli/src/helpers/env-setup.ts index 42974f5..68dacd5 100644 --- a/apps/cli/src/helpers/env-setup.ts +++ b/apps/cli/src/helpers/env-setup.ts @@ -44,14 +44,18 @@ export async function setupEnvironmentVariables( if (options.database !== "none") { if (options.orm === "prisma" && !envContent.includes("DATABASE_URL")) { - const databaseUrlLine = - options.database === "sqlite" - ? "" - : `\nDATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"`; + let databaseUrlLine = ""; + if (options.database === "sqlite") { + databaseUrlLine = ""; + } else if (options.database === "postgres") { + databaseUrlLine = `\nDATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"`; + } else if (options.database === "mongodb") { + databaseUrlLine = `\nDATABASE_URL="mongodb://localhost:27017/mydatabase"`; + } envContent += databaseUrlLine; } - if (options.database === "sqlite" && !options.turso) { + if (options.database === "sqlite" && options.dbSetup !== "turso") { if (!envContent.includes("TURSO_CONNECTION_URL")) { envContent += "\nTURSO_CONNECTION_URL=http://127.0.0.1:8080"; } diff --git a/apps/cli/src/helpers/examples-setup.ts b/apps/cli/src/helpers/examples-setup.ts index a6fb2be..901ca32 100644 --- a/apps/cli/src/helpers/examples-setup.ts +++ b/apps/cli/src/helpers/examples-setup.ts @@ -37,7 +37,7 @@ export async function setupExamples( if ( examples.includes("ai") && - backend === "hono" && + (backend === "hono" || backend === "express") && hasWebFrontend && webAppExists ) { @@ -89,6 +89,7 @@ async function updateServerIndexWithAIRoute(projectDir: string): Promise { if (await fs.pathExists(serverIndexPath)) { let indexContent = await fs.readFile(serverIndexPath, "utf8"); const isHono = indexContent.includes("hono"); + const isExpress = indexContent.includes("express"); if (isHono) { const importSection = `import { streamText } from "ai";\nimport { google } from "@ai-sdk/google";\nimport { stream } from "hono/streaming";`; @@ -110,6 +111,7 @@ app.post("/ai", async (c) => { return stream(c, (stream) => stream.pipe(result.toDataStream())); });`; + // Add imports and route handler for Hono if (indexContent.includes("import {")) { const lastImportIndex = indexContent.lastIndexOf("import"); const endOfLastImport = indexContent.indexOf("\n", lastImportIndex); @@ -141,9 +143,66 @@ ${indexContent.substring(exportIndex)}`; ${aiRouteHandler}`; } } + } else if (isExpress) { + // Express implementation + const importSection = `import { streamText } from "ai";\nimport { google } from "@ai-sdk/google";`; - await fs.writeFile(serverIndexPath, indexContent); + const aiRouteHandler = ` +// AI chat endpoint +app.post("/ai", async (req, res) => { + const { messages = [] } = req.body; + + const result = streamText({ + model: google("gemini-1.5-flash"), + messages, + }); + + result.pipeDataStreamToResponse(res); +});`; + + // Add imports for Express + if ( + indexContent.includes("import {") || + indexContent.includes("import ") + ) { + const lastImportIndex = indexContent.lastIndexOf("import"); + const endOfLastImport = indexContent.indexOf("\n", lastImportIndex); + indexContent = `${indexContent.substring(0, endOfLastImport + 1)} +${importSection} +${indexContent.substring(endOfLastImport + 1)}`; + } else { + indexContent = `${importSection} + +${indexContent}`; + } + + // Add route handler for Express + const trpcHandlerIndex = indexContent.indexOf('app.use("/trpc"'); + if (trpcHandlerIndex !== -1) { + indexContent = `${indexContent.substring(0, trpcHandlerIndex)}${aiRouteHandler} + +${indexContent.substring(trpcHandlerIndex)}`; + } else { + const appListenIndex = indexContent.indexOf("app.listen("); + if (appListenIndex !== -1) { + // Find the line before app.listen + const prevNewlineIndex = indexContent.lastIndexOf( + "\n", + appListenIndex, + ); + indexContent = `${indexContent.substring(0, prevNewlineIndex)}${aiRouteHandler} + +${indexContent.substring(prevNewlineIndex)}`; + } else { + // Fallback: append to the end + indexContent = `${indexContent} + +${aiRouteHandler}`; + } + } } + + await fs.writeFile(serverIndexPath, indexContent); } } diff --git a/apps/cli/src/helpers/mongodb-atlas-setup.ts b/apps/cli/src/helpers/mongodb-atlas-setup.ts new file mode 100644 index 0000000..f4ae0df --- /dev/null +++ b/apps/cli/src/helpers/mongodb-atlas-setup.ts @@ -0,0 +1,121 @@ +import path from "node:path"; +import { cancel, isCancel, log, text } from "@clack/prompts"; +import { execa } from "execa"; +import fs from "fs-extra"; +import pc from "picocolors"; +import { commandExists } from "../utils/command-exists"; + +type MongoDBConfig = { + connectionString: string; +}; + +async function checkAtlasCLI(): Promise { + return commandExists("atlas"); +} + +async function initMongoDBAtlas( + serverDir: string, +): Promise { + try { + const hasAtlas = await checkAtlasCLI(); + + if (!hasAtlas) { + log.error(pc.red("MongoDB Atlas CLI not found.")); + log.info( + pc.yellow( + "Please install it from: https://www.mongodb.com/docs/atlas/cli/current/install-atlas-cli/", + ), + ); + return null; + } + + log.info(pc.yellow("Setting up MongoDB Atlas...")); + + await execa("atlas", ["deployments", "setup"], { + cwd: serverDir, + stdio: "inherit", + }); + + log.info(pc.yellow("Please enter your connection string")); + + const connectionString = await text({ + message: "Paste your complete MongoDB connection string:", + validate(value) { + if (!value) return "Please enter a connection string"; + if (!value.startsWith("mongodb")) { + return "URL should start with mongodb"; + } + }, + }); + + if (isCancel(connectionString)) { + cancel("MongoDB setup cancelled"); + return null; + } + + return { + connectionString: connectionString as string, + }; + } catch (error) { + if (error instanceof Error) { + log.error(pc.red(error.message)); + } + return null; + } +} + +async function writeEnvFile(projectDir: string, config?: MongoDBConfig) { + const envPath = path.join(projectDir, "apps/server", ".env"); + let envContent = ""; + + if (await fs.pathExists(envPath)) { + envContent = await fs.readFile(envPath, "utf8"); + } + + const mongoUrlLine = config + ? `DATABASE_URL="${config.connectionString}"` + : `DATABASE_URL="mongodb://localhost:27017/mydb"`; + + if (!envContent.includes("DATABASE_URL=")) { + envContent += `\n${mongoUrlLine}`; + } else { + envContent = envContent.replace( + /DATABASE_URL=.*(\r?\n|$)/, + `${mongoUrlLine}$1`, + ); + } + + await fs.writeFile(envPath, envContent.trim()); +} + +function displayManualSetupInstructions() { + log.info(`MongoDB Atlas Setup: + +1. Install Atlas CLI: https://www.mongodb.com/docs/atlas/cli/stable/install-atlas-cli/ +2. Run 'atlas deployments setup' and follow prompts +3. Get your connection string from the output +4. Format: mongodb+srv://USERNAME:PASSWORD@CLUSTER.mongodb.net/DATABASE_NAME +5. Add to .env as DATABASE_URL="your_connection_string"`); +} + +export async function setupMongoDBAtlas(projectDir: string) { + const serverDir = path.join(projectDir, "apps/server"); + + try { + const config = await initMongoDBAtlas(serverDir); + + if (config) { + await writeEnvFile(projectDir, config); + log.success( + pc.green("MongoDB Atlas connection string saved to .env file!"), + ); + } else { + await writeEnvFile(projectDir); + displayManualSetupInstructions(); + } + } catch (error) { + log.error(pc.red(`Error during MongoDB Atlas setup: ${error}`)); + await writeEnvFile(projectDir); + displayManualSetupInstructions(); + } +} diff --git a/apps/cli/src/helpers/post-installation.ts b/apps/cli/src/helpers/post-installation.ts index 3c0d255..1e43c5f 100644 --- a/apps/cli/src/helpers/post-installation.ts +++ b/apps/cli/src/helpers/post-installation.ts @@ -2,6 +2,7 @@ import { note } from "@clack/prompts"; import pc from "picocolors"; import type { ProjectAddons, + ProjectDBSetup, ProjectDatabase, ProjectFrontend, ProjectOrm, @@ -18,6 +19,7 @@ export function displayPostInstallInstructions( addons: ProjectAddons[], runtime: ProjectRuntime, frontends: ProjectFrontend[], + dbSetup?: ProjectDBSetup, ) { const runCmd = packageManager === "npm" ? "npm run" : packageManager; const cdCmd = `cd ${projectName}`; diff --git a/apps/cli/src/helpers/template-manager.ts b/apps/cli/src/helpers/template-manager.ts index f694b33..4e1665d 100644 --- a/apps/cli/src/helpers/template-manager.ts +++ b/apps/cli/src/helpers/template-manager.ts @@ -372,9 +372,9 @@ function getOrmTemplateDir(orm: ProjectOrm, database: ProjectDatabase): string { } if (orm === "prisma") { - return database === "sqlite" - ? "template/with-prisma-sqlite" - : "template/with-prisma-postgres"; + if (database === "sqlite") return "template/with-prisma-sqlite"; + if (database === "postgres") return "template/with-prisma-postgres"; + if (database === "mongodb") return "template/with-prisma-mongodb"; } return "template/base"; @@ -388,9 +388,9 @@ function getAuthLibDir(orm: ProjectOrm, database: ProjectDatabase): string { } if (orm === "prisma") { - return database === "sqlite" - ? "with-prisma-sqlite-lib" - : "with-prisma-postgres-lib"; + if (database === "sqlite") return "with-prisma-sqlite-lib"; + if (database === "postgres") return "with-prisma-postgres-lib"; + if (database === "mongodb") return "with-prisma-mongodb-lib"; } throw new Error("Invalid ORM or database configuration for auth setup"); diff --git a/apps/cli/src/helpers/turso-setup.ts b/apps/cli/src/helpers/turso-setup.ts index 60a3760..71f9af5 100644 --- a/apps/cli/src/helpers/turso-setup.ts +++ b/apps/cli/src/helpers/turso-setup.ts @@ -12,6 +12,7 @@ import { import { $ } from "execa"; import fs from "fs-extra"; import pc from "picocolors"; +import { commandExists } from "../utils/command-exists"; type TursoConfig = { dbUrl: string; @@ -26,12 +27,7 @@ type TursoGroup = { }; async function isTursoInstalled() { - try { - const result = await $`turso --version`; - return result.exitCode === 0; - } catch (error) { - return false; - } + return commandExists("turso"); } async function isTursoLoggedIn() { diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 1f054bc..0a58f05 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -9,6 +9,7 @@ import type { ProjectAddons, ProjectBackend, ProjectConfig, + ProjectDBSetup, ProjectDatabase, ProjectExamples, ProjectFrontend, @@ -37,8 +38,11 @@ async function main() { .version(getLatestCLIVersion()) .argument("[project-directory]", "Project name/directory") .option("-y, --yes", "Use default configuration") - .option("--database ", "Database type (none, sqlite, postgres)") - .option("--orm ", "ORM type (none, drizzle, prisma)") + .option( + "--database ", + "Database type (none, sqlite, postgres, mongodb)", + ) + .option("--orm ", "ORM type (drizzle, prisma)") .option("--auth", "Include authentication") .option("--no-auth", "Exclude authentication") .option( @@ -56,11 +60,14 @@ async function main() { .option("--package-manager ", "Package manager (npm, pnpm, bun)") .option("--install", "Install dependencies") .option("--no-install", "Skip installing dependencies") - .option("--turso", "Set up Turso for SQLite database") - .option("--no-turso", "Skip Turso setup") - .option("--prisma-postgres", "Set up Prisma Postgres") - .option("--no-prisma-postgres", "Skip Prisma Postgres setup") - .option("--backend ", "Backend framework (hono, elysia)") + .option( + "--db-setup ", + "Database setup (turso, prisma-postgres, mongodb-atlas, none)", + ) + .option( + "--backend ", + "Backend framework (hono, express, elysia)", + ) .option("--runtime ", "Runtime (bun, node)") .parse(); @@ -125,20 +132,41 @@ async function main() { function validateOptions(options: CLIOptions): void { if ( options.database && - !["none", "sqlite", "postgres"].includes(options.database) + !["none", "sqlite", "postgres", "mongodb"].includes(options.database) ) { cancel( pc.red( - `Invalid database type: ${options.database}. Must be none, sqlite, or postgres.`, + `Invalid database type: ${options.database}. Must be none, sqlite, postgres, or mongodb.`, ), ); process.exit(1); } - if (options.orm && !["none", "drizzle", "prisma"].includes(options.orm)) { + if (options.orm && !["drizzle", "prisma"].includes(options.orm)) { + cancel( + pc.red(`Invalid ORM type: ${options.orm}. Must be drizzle or prisma.`), + ); + process.exit(1); + } + + if ( + options.dbSetup && + !["turso", "prisma-postgres", "mongodb-atlas", "none"].includes( + options.dbSetup, + ) + ) { cancel( pc.red( - `Invalid ORM type: ${options.orm}. Must be none, drizzle, or prisma.`, + `Invalid database setup: ${options.dbSetup}. Must be turso, prisma-postgres, mongodb-atlas, or none.`, + ), + ); + process.exit(1); + } + + if (options.database === "mongodb" && options.orm === "drizzle") { + cancel( + pc.red( + "MongoDB is only available with Prisma. Cannot use --database mongodb with --orm drizzle", ), ); process.exit(1); @@ -163,51 +191,62 @@ function validateOptions(options: CLIOptions): void { process.exit(1); } - if ("turso" in options && options.turso === true) { + if (options.dbSetup && options.dbSetup !== "none") { cancel( pc.red( - "Turso setup requires a SQLite database. Cannot use --turso with --database none.", + `Database setup requires a database. Cannot use --db-setup ${options.dbSetup} with --database none.`, ), ); process.exit(1); } } - if ( - "turso" in options && - options.turso === true && - options.database && - options.database !== "sqlite" - ) { - cancel( - pc.red( - `Turso setup requires a SQLite database. Cannot use --turso with --database ${options.database}`, - ), - ); - process.exit(1); - } - - if ( - "turso" in options && - options.turso === true && - options.orm === "prisma" - ) { - cancel( - pc.red( - "Turso setup is not compatible with Prisma. Cannot use --turso with --orm prisma", - ), - ); - process.exit(1); - } - - if ("prismaPostgres" in options && options.prismaPostgres === true) { - if ( - (options.database && options.database !== "postgres") || - (options.orm && options.orm !== "prisma") - ) { + // Check for database setup compatibility + if (options.dbSetup === "turso") { + if (options.database && options.database !== "sqlite") { cancel( pc.red( - "Prisma PostgreSQL setup requires PostgreSQL database with Prisma ORM. Cannot use --prisma-postgres with incompatible database or ORM options.", + `Turso setup requires a SQLite database. Cannot use --db-setup turso with --database ${options.database}`, + ), + ); + process.exit(1); + } + + if (options.orm === "prisma") { + cancel( + pc.red( + "Turso setup is not compatible with Prisma. Cannot use --db-setup turso with --orm prisma", + ), + ); + process.exit(1); + } + } + + if (options.dbSetup === "prisma-postgres") { + if (options.database && options.database !== "postgres") { + cancel( + pc.red( + "Prisma PostgreSQL setup requires PostgreSQL database. Cannot use --db-setup prisma-postgres with a different database type.", + ), + ); + process.exit(1); + } + + if (options.orm && options.orm !== "prisma") { + cancel( + pc.red( + "Prisma PostgreSQL setup requires Prisma ORM. Cannot use --db-setup prisma-postgres with a different ORM.", + ), + ); + process.exit(1); + } + } + + if (options.dbSetup === "mongodb-atlas") { + if (options.database && options.database !== "mongodb") { + cancel( + pc.red( + "MongoDB Atlas setup requires MongoDB database. Cannot use --db-setup mongodb-atlas with a different database type.", ), ); process.exit(1); @@ -226,10 +265,13 @@ function validateOptions(options: CLIOptions): void { process.exit(1); } - if (options.backend && !["hono", "elysia"].includes(options.backend)) { + if ( + options.backend && + !["hono", "elysia", "express"].includes(options.backend) + ) { cancel( pc.red( - `Invalid backend framework: ${options.backend}. Must be hono or elysia.`, + `Invalid backend framework: ${options.backend}. Must be hono, elysia, or express.`, ), ); process.exit(1); @@ -409,42 +451,6 @@ function processFlags( } } - let database = options.database as ProjectDatabase | undefined; - let orm: ProjectOrm | undefined; - if (options.orm) { - orm = options.orm as ProjectOrm; - } - - if ("prismaPostgres" in options && options.prismaPostgres === true) { - if (!database) { - database = "postgres" as ProjectDatabase; - } - if (!orm) { - orm = "prisma" as ProjectOrm; - } - } - - let auth: boolean | undefined = "auth" in options ? options.auth : undefined; - let tursoOption: boolean | undefined = - "turso" in options ? options.turso : undefined; - - let prismaPostgresOption: boolean | undefined = - "prismaPostgres" in options ? options.prismaPostgres : undefined; - - if ( - database === "none" || - (database === "sqlite" && database !== undefined) || - (orm !== undefined && orm !== "prisma") - ) { - prismaPostgresOption = false; - } - - if (database === "none") { - orm = "none"; - auth = false; - tursoOption = false; - } - let examples: ProjectExamples[] | undefined; if ("examples" in options) { if (options.examples === false) { @@ -517,12 +523,44 @@ function processFlags( } } + let database = options.database as ProjectDatabase | undefined; + let orm = options.orm as ProjectOrm | undefined; + const auth = "auth" in options ? options.auth : undefined; + const backend = options.backend as ProjectBackend | undefined; const runtime = options.runtime as ProjectRuntime | undefined; const packageManager = options.packageManager as | ProjectPackageManager | undefined; + let dbSetup: ProjectDBSetup | undefined = undefined; + if (options.dbSetup) { + if (options.dbSetup === "none") { + dbSetup = "none"; + } else { + dbSetup = options.dbSetup as ProjectDBSetup; + + if (dbSetup === "turso") { + database = "sqlite"; + + if (orm === "prisma") { + log.warn( + pc.yellow( + "Turso is not compatible with Prisma - switching to Drizzle", + ), + ); + orm = "drizzle"; + } + } else if (dbSetup === "prisma-postgres") { + database = "postgres"; + orm = "prisma"; + } else if (dbSetup === "mongodb-atlas") { + database = "mongodb"; + orm = "prisma"; + } + } + } + const config: Partial = {}; if (projectDirectory) config.projectName = projectDirectory; @@ -532,9 +570,7 @@ function processFlags( if (packageManager) config.packageManager = packageManager; if ("git" in options) config.git = options.git; if ("install" in options) config.noInstall = !options.install; - if (tursoOption !== undefined) config.turso = tursoOption; - if (prismaPostgresOption !== undefined) - config.prismaPostgres = prismaPostgresOption; + if (dbSetup !== undefined) config.dbSetup = dbSetup; if (backend) config.backend = backend; if (runtime) config.runtime = runtime; if (frontend !== undefined) config.frontend = frontend; diff --git a/apps/cli/src/prompts/backend-framework.ts b/apps/cli/src/prompts/backend-framework.ts index 6699ffe..3de5b61 100644 --- a/apps/cli/src/prompts/backend-framework.ts +++ b/apps/cli/src/prompts/backend-framework.ts @@ -16,6 +16,11 @@ export async function getBackendFrameworkChoice( label: "Hono", hint: "Lightweight, ultrafast web framework", }, + { + value: "express", + label: "Express", + hint: "Fast, unopinionated, minimalist web framework for Node.js", + }, { value: "elysia", label: "Elysia", diff --git a/apps/cli/src/prompts/config-prompts.ts b/apps/cli/src/prompts/config-prompts.ts index db98d98..3f05f48 100644 --- a/apps/cli/src/prompts/config-prompts.ts +++ b/apps/cli/src/prompts/config-prompts.ts @@ -1,9 +1,10 @@ -import { cancel, group } from "@clack/prompts"; +import { cancel, group, log } from "@clack/prompts"; import pc from "picocolors"; import type { ProjectAddons, ProjectBackend, ProjectConfig, + ProjectDBSetup, ProjectDatabase, ProjectExamples, ProjectFrontend, @@ -15,16 +16,15 @@ import { getAddonsChoice } from "./addons"; import { getAuthChoice } from "./auth"; import { getBackendFrameworkChoice } from "./backend-framework"; import { getDatabaseChoice } from "./database"; +import { getDBSetupChoice } from "./db-setup"; import { getExamplesChoice } from "./examples"; import { getFrontendChoice } from "./frontend-option"; import { getGitChoice } from "./git"; import { getNoInstallChoice } from "./install"; import { getORMChoice } from "./orm"; import { getPackageManagerChoice } from "./package-manager"; -import { getPrismaSetupChoice } from "./prisma-postgres"; import { getProjectName } from "./project-name"; import { getRuntimeChoice } from "./runtime"; -import { getTursoSetupChoice } from "./turso"; type PromptGroupResults = { projectName: string; @@ -36,8 +36,7 @@ type PromptGroupResults = { git: boolean; packageManager: ProjectPackageManager; noInstall: boolean; - turso: boolean; - prismaPostgres: boolean; + dbSetup: ProjectDBSetup; backend: ProjectBackend; runtime: ProjectRuntime; frontend: ProjectFrontend[]; @@ -46,6 +45,32 @@ type PromptGroupResults = { export async function gatherConfig( flags: Partial, ): Promise { + // Handle specific dbSetup scenarios to adjust database and ORM before prompts + if (flags.dbSetup) { + if (flags.dbSetup === "turso") { + // Force database to be sqlite when turso is selected + flags.database = "sqlite"; + + // If orm is explicitly set to prisma, warn and switch to drizzle + if (flags.orm === "prisma") { + log.warn( + pc.yellow( + "Turso is not compatible with Prisma - switching to Drizzle", + ), + ); + flags.orm = "drizzle"; + } + } else if (flags.dbSetup === "prisma-postgres") { + // Force database and orm for prisma-postgres + flags.database = "postgres"; + flags.orm = "prisma"; + } else if (flags.dbSetup === "mongodb-atlas") { + // Force database for mongodb-atlas + flags.database = "mongodb"; + flags.orm = "prisma"; // MongoDB only works with Prisma + } + } + const result = await group( { projectName: async () => { @@ -56,21 +81,15 @@ export async function gatherConfig( runtime: () => getRuntimeChoice(flags.runtime), database: () => getDatabaseChoice(flags.database), orm: ({ results }) => - getORMChoice(flags.orm, results.database !== "none"), + getORMChoice(flags.orm, results.database !== "none", results.database), auth: ({ results }) => getAuthChoice( flags.auth, results.database !== "none", results.frontend, ), - turso: ({ results }) => - results.database === "sqlite" && results.orm !== "prisma" - ? getTursoSetupChoice(flags.turso) - : Promise.resolve(false), - prismaPostgres: ({ results }) => - results.database === "postgres" && results.orm === "prisma" - ? getPrismaSetupChoice(flags.prismaPostgres) - : Promise.resolve(false), + dbSetup: ({ results }) => + getDBSetupChoice(results.database ?? "none", flags.dbSetup), addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend), examples: ({ results }) => getExamplesChoice( @@ -102,8 +121,7 @@ export async function gatherConfig( git: result.git, packageManager: result.packageManager, noInstall: result.noInstall, - turso: result.turso, - prismaPostgres: result.prismaPostgres, + dbSetup: result.dbSetup, backend: result.backend, runtime: result.runtime, }; diff --git a/apps/cli/src/prompts/database.ts b/apps/cli/src/prompts/database.ts index faecb0c..7f79cf7 100644 --- a/apps/cli/src/prompts/database.ts +++ b/apps/cli/src/prompts/database.ts @@ -26,6 +26,11 @@ export async function getDatabaseChoice( label: "PostgreSQL", hint: "Traditional relational database", }, + { + value: "mongodb", + label: "MongoDB", + hint: "NoSQL document-oriented database", + }, ], initialValue: DEFAULT_CONFIG.database, }); diff --git a/apps/cli/src/prompts/db-setup.ts b/apps/cli/src/prompts/db-setup.ts new file mode 100644 index 0000000..1507c8f --- /dev/null +++ b/apps/cli/src/prompts/db-setup.ts @@ -0,0 +1,57 @@ +import { cancel, isCancel, select } from "@clack/prompts"; +import pc from "picocolors"; +import type { ProjectDBSetup } from "../types"; + +export async function getDBSetupChoice( + databaseType: string, + dbSetup: ProjectDBSetup | undefined, +): Promise { + if (dbSetup !== undefined) return dbSetup as ProjectDBSetup; + + let options: Array<{ value: ProjectDBSetup; label: string; hint: string }> = + []; + + if (databaseType === "sqlite") { + options = [ + { + value: "turso" as const, + label: "Turso", + hint: "Cloud SQLite with libSQL", + }, + { value: "none" as const, label: "None", hint: "Manual setup" }, + ]; + } else if (databaseType === "postgres") { + options = [ + { + value: "prisma-postgres" as const, + label: "Prisma Postgres", + hint: "Managed by Prisma", + }, + { value: "none" as const, label: "None", hint: "Manual setup" }, + ]; + } else if (databaseType === "mongodb") { + options = [ + { + value: "mongodb-atlas" as const, + label: "MongoDB Atlas", + hint: "Cloud MongoDB service", + }, + { value: "none" as const, label: "None", hint: "Manual setup" }, + ]; + } else { + return "none"; + } + + const response = await select({ + message: `Select ${databaseType} setup option`, + options, + initialValue: "none", + }); + + if (isCancel(response)) { + cancel(pc.red("Operation cancelled")); + process.exit(0); + } + + return response; +} diff --git a/apps/cli/src/prompts/examples.ts b/apps/cli/src/prompts/examples.ts index 8d6ef41..555ce42 100644 --- a/apps/cli/src/prompts/examples.ts +++ b/apps/cli/src/prompts/examples.ts @@ -42,7 +42,7 @@ export async function getExamplesChoice( }); } - if (backend === "hono") { + if (backend === "hono" || backend === "express") { response = await multiselect({ message: "Include examples", options: [ diff --git a/apps/cli/src/prompts/orm.ts b/apps/cli/src/prompts/orm.ts index 43885c0..0daabf0 100644 --- a/apps/cli/src/prompts/orm.ts +++ b/apps/cli/src/prompts/orm.ts @@ -1,15 +1,21 @@ -import { cancel, isCancel, select } from "@clack/prompts"; +import { cancel, isCancel, log, select } from "@clack/prompts"; import pc from "picocolors"; import { DEFAULT_CONFIG } from "../constants"; -import type { ProjectOrm } from "../types"; +import type { ProjectDatabase, ProjectOrm } from "../types"; export async function getORMChoice( orm: ProjectOrm | undefined, hasDatabase: boolean, + database?: ProjectDatabase, ): Promise { if (!hasDatabase) return "none"; if (orm !== undefined) return orm; + if (database === "mongodb") { + log.info("Only Prisma is supported with MongoDB."); + return "prisma"; + } + const response = await select({ message: "Select ORM", options: [ diff --git a/apps/cli/src/prompts/prisma-postgres.ts b/apps/cli/src/prompts/prisma-postgres.ts deleted file mode 100644 index 9cc30e5..0000000 --- a/apps/cli/src/prompts/prisma-postgres.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { cancel, confirm, isCancel } from "@clack/prompts"; -import pc from "picocolors"; -import { DEFAULT_CONFIG } from "../constants"; - -export async function getPrismaSetupChoice( - prismaSetup?: boolean, -): Promise { - if (prismaSetup !== undefined) return prismaSetup; - - const response = await confirm({ - message: "Set up Prisma Postgres database?", - initialValue: DEFAULT_CONFIG.prismaPostgres, - }); - - if (isCancel(response)) { - cancel(pc.red("Operation cancelled")); - process.exit(0); - } - - return response; -} diff --git a/apps/cli/src/prompts/turso.ts b/apps/cli/src/prompts/turso.ts deleted file mode 100644 index d0beb66..0000000 --- a/apps/cli/src/prompts/turso.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { cancel, confirm, isCancel } from "@clack/prompts"; -import pc from "picocolors"; -import { DEFAULT_CONFIG } from "../constants"; - -export async function getTursoSetupChoice(turso?: boolean): Promise { - if (turso !== undefined) return turso; - - const response = await confirm({ - message: "Set up Turso database?", - initialValue: DEFAULT_CONFIG.turso, - }); - - if (isCancel(response)) { - cancel(pc.red("Operation cancelled")); - process.exit(0); - } - - return response; -} diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index be8ed13..5b32ad2 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -1,8 +1,8 @@ -export type ProjectDatabase = "sqlite" | "postgres" | "none"; -export type ProjectOrm = "drizzle" | "prisma" | "none"; +export type ProjectDatabase = "sqlite" | "postgres" | "mongodb" | "none"; +export type ProjectOrm = "drizzle" | "prisma"; export type ProjectPackageManager = "npm" | "pnpm" | "bun"; export type ProjectAddons = "pwa" | "biome" | "tauri" | "husky"; -export type ProjectBackend = "hono" | "elysia"; +export type ProjectBackend = "hono" | "elysia" | "express"; export type ProjectRuntime = "node" | "bun"; export type ProjectExamples = "todo" | "ai"; export type ProjectFrontend = @@ -10,6 +10,11 @@ export type ProjectFrontend = | "tanstack-router" | "tanstack-start" | "native"; +export type ProjectDBSetup = + | "turso" + | "prisma-postgres" + | "mongodb-atlas" + | "none"; export interface ProjectConfig { projectName: string; @@ -22,9 +27,8 @@ export interface ProjectConfig { examples: ProjectExamples[]; git: boolean; packageManager: ProjectPackageManager; - noInstall?: boolean; - turso?: boolean; - prismaPostgres: boolean; + noInstall: boolean; + dbSetup: ProjectDBSetup; frontend: ProjectFrontend[]; } @@ -39,8 +43,7 @@ export type CLIOptions = { git?: boolean; packageManager?: string; install?: boolean; - turso?: boolean; - prismaPostgres?: boolean; + dbSetup?: string; backend?: string; runtime?: string; }; diff --git a/apps/cli/src/utils/command-exists.ts b/apps/cli/src/utils/command-exists.ts new file mode 100644 index 0000000..c363c4e --- /dev/null +++ b/apps/cli/src/utils/command-exists.ts @@ -0,0 +1,16 @@ +import { execa } from "execa"; + +export async function commandExists(command: string): Promise { + try { + const isWindows = process.platform === "win32"; + if (isWindows) { + const result = await execa("where", [command]); + return result.exitCode === 0; + } + + const result = await execa("which", [command]); + return result.exitCode === 0; + } catch { + return false; + } +} diff --git a/apps/cli/src/utils/display-config.ts b/apps/cli/src/utils/display-config.ts index eb2a6d3..c2c4cee 100644 --- a/apps/cli/src/utils/display-config.ts +++ b/apps/cli/src/utils/display-config.ts @@ -60,14 +60,8 @@ export function displayConfig(config: Partial) { configDisplay.push(`${pc.blue("Skip Install:")} ${config.noInstall}`); } - if (config.turso !== undefined) { - configDisplay.push(`${pc.blue("Turso Setup:")} ${config.turso}`); - } - - if (config.prismaPostgres !== undefined) { - configDisplay.push( - `${pc.blue("Prisma Postgres Setup:")} ${config.prismaPostgres ? "Yes" : "No"}`, - ); + if (config.dbSetup !== undefined) { + configDisplay.push(`${pc.blue("Database Setup:")} ${config.dbSetup}`); } return configDisplay.join("\n"); diff --git a/apps/cli/src/utils/generate-reproducible-command.ts b/apps/cli/src/utils/generate-reproducible-command.ts index 9b73767..e04fc17 100644 --- a/apps/cli/src/utils/generate-reproducible-command.ts +++ b/apps/cli/src/utils/generate-reproducible-command.ts @@ -12,8 +12,8 @@ export function generateReproducibleCommand(config: ProjectConfig): string { flags.push(`--orm ${config.orm}`); } - if (config.database === "sqlite") { - flags.push(config.turso ? "--turso" : "--no-turso"); + if (config.dbSetup && config.dbSetup !== "none") { + flags.push(`--db-setup ${config.dbSetup}`); } } diff --git a/apps/cli/template/with-auth/apps/server/src/lib/with-express-context.ts b/apps/cli/template/with-auth/apps/server/src/lib/with-express-context.ts new file mode 100644 index 0000000..9b0a6d7 --- /dev/null +++ b/apps/cli/template/with-auth/apps/server/src/lib/with-express-context.ts @@ -0,0 +1,14 @@ +import type { CreateExpressContextOptions } from "@trpc/server/adapters/express"; +import { fromNodeHeaders } from "better-auth/node"; +import { auth } from "./auth"; + +export async function createContext(opts: CreateExpressContextOptions) { + const session = await auth.api.getSession({ + headers: fromNodeHeaders(opts.req.headers), + }); + return { + session, + }; +} + +export type Context = Awaited>; diff --git a/apps/cli/template/with-auth/apps/server/src/with-express-index.ts b/apps/cli/template/with-auth/apps/server/src/with-express-index.ts new file mode 100644 index 0000000..e6fb496 --- /dev/null +++ b/apps/cli/template/with-auth/apps/server/src/with-express-index.ts @@ -0,0 +1,34 @@ +import "dotenv/config"; +import { createExpressMiddleware } from "@trpc/server/adapters/express"; +import { toNodeHandler } from "better-auth/node"; +import cors from "cors"; +import express from "express"; +import { auth } from "./lib/auth"; +import { createContext } from "./lib/context"; +import { appRouter } from "./routers/index"; + +const app = express(); + +app.use( + cors({ + origin: process.env.CORS_ORIGIN || "", + methods: ["GET", "POST", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization"], + credentials: true, + }), +); + +app.all("/api/auth{/*path}", toNodeHandler(auth)); +app.use(express.json()); + + +app.use("/trpc", createExpressMiddleware({ router: appRouter, createContext })); + + +app.get("/", (_req, res) => { + res.status(200).send("OK"); +}); + +app.listen(3000, () => { + console.log("Server is running on port 3000"); +}); diff --git a/apps/cli/template/with-auth/apps/server/src/with-prisma-mongodb-lib/auth.ts b/apps/cli/template/with-auth/apps/server/src/with-prisma-mongodb-lib/auth.ts new file mode 100644 index 0000000..4ef6689 --- /dev/null +++ b/apps/cli/template/with-auth/apps/server/src/with-prisma-mongodb-lib/auth.ts @@ -0,0 +1,17 @@ +import { betterAuth } from "better-auth"; +import { prismaAdapter } from "better-auth/adapters/prisma"; +import prisma from "../../prisma"; + +export const auth = betterAuth({ + database: prismaAdapter(prisma, { + provider: "mongodb", + }), + trustedOrigins: [process.env.CORS_ORIGIN || ""], + emailAndPassword: { enabled: true }, + advanced: { + defaultCookieAttributes: { + sameSite: "none", + secure: true, + }, + }, +}); diff --git a/apps/cli/template/with-express/apps/server/src/index.ts b/apps/cli/template/with-express/apps/server/src/index.ts new file mode 100644 index 0000000..9a8f6f4 --- /dev/null +++ b/apps/cli/template/with-express/apps/server/src/index.ts @@ -0,0 +1,29 @@ +import "dotenv/config"; +import { createExpressMiddleware } from "@trpc/server/adapters/express"; +import cors from "cors"; +import express from "express"; +import { createContext } from "./lib/context"; +import { appRouter } from "./routers/index"; + +const app = express(); + +app.use( + cors({ + origin: process.env.CORS_ORIGIN || "", + methods: ["GET", "POST", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization"], + credentials: true, + }), +); + +app.use(express.json()); + +app.use("/trpc", createExpressMiddleware({ router: appRouter, createContext })); + +app.get("/", (_req, res) => { + res.status(200).send("OK"); +}); + +app.listen(3000, () => { + console.log("Server is running on port 3000"); +}); diff --git a/apps/cli/template/with-express/apps/server/src/lib/context.ts b/apps/cli/template/with-express/apps/server/src/lib/context.ts new file mode 100644 index 0000000..389af2b --- /dev/null +++ b/apps/cli/template/with-express/apps/server/src/lib/context.ts @@ -0,0 +1,9 @@ +import type { CreateExpressContextOptions } from "@trpc/server/adapters/express"; + +export async function createContext(opts: CreateExpressContextOptions) { + return { + session: null, + }; +} + +export type Context = Awaited>; diff --git a/apps/cli/template/with-prisma-mongodb/apps/server/prisma/index.ts b/apps/cli/template/with-prisma-mongodb/apps/server/prisma/index.ts new file mode 100644 index 0000000..34ab1b5 --- /dev/null +++ b/apps/cli/template/with-prisma-mongodb/apps/server/prisma/index.ts @@ -0,0 +1,5 @@ +import { PrismaClient } from "@prisma/client"; + +let prisma = new PrismaClient(); + +export default prisma; diff --git a/apps/cli/template/with-prisma-mongodb/apps/server/prisma/schema/auth.prisma b/apps/cli/template/with-prisma-mongodb/apps/server/prisma/schema/auth.prisma new file mode 100644 index 0000000..8e5fe38 --- /dev/null +++ b/apps/cli/template/with-prisma-mongodb/apps/server/prisma/schema/auth.prisma @@ -0,0 +1,59 @@ +model User { + id String @id @map("_id") + name String + email String + emailVerified Boolean + image String? + createdAt DateTime + updatedAt DateTime + sessions Session[] + accounts Account[] + + @@unique([email]) + @@map("user") +} + +model Session { + id String @id @map("_id") + expiresAt DateTime + token String + createdAt DateTime + updatedAt DateTime + ipAddress String? + userAgent String? + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([token]) + @@map("session") +} + +model Account { + id String @id @map("_id") + accountId String + providerId String + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + accessToken String? + refreshToken String? + idToken String? + accessTokenExpiresAt DateTime? + refreshTokenExpiresAt DateTime? + scope String? + password String? + createdAt DateTime + updatedAt DateTime + + @@map("account") +} + +model Verification { + id String @id @map("_id") + identifier String + value String + expiresAt DateTime + createdAt DateTime? + updatedAt DateTime? + + @@map("verification") +} diff --git a/apps/cli/template/with-prisma-mongodb/apps/server/prisma/schema/schema.prisma b/apps/cli/template/with-prisma-mongodb/apps/server/prisma/schema/schema.prisma new file mode 100644 index 0000000..4587ba8 --- /dev/null +++ b/apps/cli/template/with-prisma-mongodb/apps/server/prisma/schema/schema.prisma @@ -0,0 +1,9 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["prismaSchemaFolder"] +} + +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} diff --git a/apps/cli/template/with-prisma-mongodb/apps/server/prisma/schema/todo.prisma b/apps/cli/template/with-prisma-mongodb/apps/server/prisma/schema/todo.prisma new file mode 100644 index 0000000..9001d25 --- /dev/null +++ b/apps/cli/template/with-prisma-mongodb/apps/server/prisma/schema/todo.prisma @@ -0,0 +1,7 @@ +model Todo { + id String @id @default(auto()) @map("_id") @db.ObjectId + text String + completed Boolean @default(false) + + @@map("todo") +} diff --git a/apps/web/package.json b/apps/web/package.json index aae6617..540d245 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "build": "next build", - "dev": "next dev", + "dev": "next dev --turbopack", "start": "next start", "check": "biome check --write .", "postinstall": "fumadocs-mdx" diff --git a/apps/web/src/app/(home)/_components/StackArchitech.tsx b/apps/web/src/app/(home)/_components/StackArchitech.tsx index 85e4a4d..de62872 100644 --- a/apps/web/src/app/(home)/_components/StackArchitech.tsx +++ b/apps/web/src/app/(home)/_components/StackArchitech.tsx @@ -74,32 +74,45 @@ const StackArchitect = () => { }, []); useEffect(() => { - if (stack.database === "none" && stack.orm !== "none") { - setStack((prev) => ({ ...prev, orm: "none" })); - } - - if (stack.database !== "postgres" || stack.orm !== "prisma") { - if (stack.prismaPostgres === "true") { - setStack((prev) => ({ ...prev, prismaPostgres: "false" })); + if (stack.database === "none") { + if (stack.orm !== "none") { + setStack((prev) => ({ ...prev, orm: "none" })); + } + if (stack.auth === "true") { + setStack((prev) => ({ ...prev, auth: "false" })); + } + if (stack.dbSetup !== "none") { + setStack((prev) => ({ ...prev, dbSetup: "none" })); } } - if (stack.database !== "sqlite" || stack.orm === "prisma") { - if (stack.turso === "true") { - setStack((prev) => ({ ...prev, turso: "false" })); - } + if (stack.database === "mongodb" && stack.orm === "drizzle") { + setStack((prev) => ({ ...prev, orm: "prisma" })); } - if (stack.database === "none" && stack.auth === "true") { - setStack((prev) => ({ ...prev, auth: "false" })); + if (stack.dbSetup === "turso") { + if (stack.database !== "sqlite") { + setStack((prev) => ({ ...prev, database: "sqlite" })); + } + if (stack.orm === "prisma") { + setStack((prev) => ({ ...prev, orm: "drizzle" })); + } + } else if (stack.dbSetup === "prisma-postgres") { + if (stack.database !== "postgres") { + setStack((prev) => ({ ...prev, database: "postgres" })); + } + if (stack.orm !== "prisma") { + setStack((prev) => ({ ...prev, orm: "prisma" })); + } + } else if (stack.dbSetup === "mongodb-atlas") { + if (stack.database !== "mongodb") { + setStack((prev) => ({ ...prev, database: "mongodb" })); + } + if (stack.orm !== "prisma") { + setStack((prev) => ({ ...prev, orm: "prisma" })); + } } - }, [ - stack.database, - stack.orm, - stack.prismaPostgres, - stack.turso, - stack.auth, - ]); + }, [stack.database, stack.orm, stack.dbSetup, stack.auth]); useEffect(() => { const cmd = generateCommand(stack); @@ -113,6 +126,33 @@ const StackArchitect = () => { notes.frontend = []; + notes.dbSetup = []; + if (stack.database === "none") { + notes.dbSetup.push("Database setup requires a database."); + } else { + if (stack.dbSetup === "turso") { + if (stack.database !== "sqlite") { + notes.dbSetup.push("Turso setup requires SQLite database."); + } + if (stack.orm === "prisma") { + notes.dbSetup.push("Turso is not compatible with Prisma ORM."); + } + } else if (stack.dbSetup === "prisma-postgres") { + if (stack.database !== "postgres") { + notes.dbSetup.push( + "Prisma PostgreSQL setup requires PostgreSQL database.", + ); + } + if (stack.orm !== "prisma") { + notes.dbSetup.push("Prisma PostgreSQL setup requires Prisma ORM."); + } + } else if (stack.dbSetup === "mongodb-atlas") { + if (stack.database !== "mongodb") { + notes.dbSetup.push("MongoDB Atlas setup requires MongoDB database."); + } + } + } + notes.addons = []; if (!hasWebFrontend) { notes.addons.push("PWA and Tauri are only available with React Web."); @@ -125,6 +165,8 @@ const StackArchitect = () => { notes.orm.push( "ORM options are only available when a database is selected.", ); + } else if (stack.database === "mongodb" && stack.orm === "drizzle") { + notes.orm.push("MongoDB is only available with Prisma ORM."); } notes.auth = []; @@ -132,23 +174,6 @@ const StackArchitect = () => { notes.auth.push("Authentication requires a database."); } - notes.turso = []; - if (stack.database !== "sqlite") { - notes.turso.push( - "Turso integration is only available with SQLite database.", - ); - } - if (stack.orm === "prisma") { - notes.turso.push("Turso is not compatible with Prisma ORM."); - } - - notes.prismaPostgres = []; - if (stack.database !== "postgres" || stack.orm !== "prisma") { - notes.prismaPostgres.push( - "Prisma PostgreSQL setup requires PostgreSQL database with Prisma ORM.", - ); - } - notes.examples = []; if (!hasWebFrontend) { notes.examples.push( @@ -198,12 +223,8 @@ const StackArchitect = () => { flags.push("--no-auth"); } - if (stackState.turso === "true") { - flags.push("--turso"); - } - - if (stackState.prismaPostgres === "true") { - flags.push("--prisma-postgres"); + if (stackState.dbSetup !== "none") { + flags.push(`--db-setup ${stackState.dbSetup}`); } if (stackState.backendFramework !== "hono") { @@ -263,7 +284,6 @@ const StackArchitect = () => { if (currentSelection.length === 1) { return prev; } - return { ...prev, frontend: currentSelection.filter((id) => id !== techId), @@ -296,6 +316,10 @@ const StackArchitect = () => { prev.frontend.includes("react-router") || prev.frontend.includes("tanstack-start"); + const hasPWACompatibleFrontend = + prev.frontend.includes("tanstack-router") || + prev.frontend.includes("react-router"); + if (index >= 0) { currentArray.splice(index, 1); } else { @@ -318,8 +342,7 @@ const StackArchitect = () => { if ( category === "addons" && (techId === "pwa" || techId === "tauri") && - !prev.frontend.includes("tanstack-router") && - !prev.frontend.includes("react-router") + !hasPWACompatibleFrontend ) { return prev; } @@ -342,45 +365,40 @@ const StackArchitect = () => { } if (category === "database") { + let updatedState = { ...prev, database: techId }; + if (techId === "none") { - return { - ...prev, - database: techId, + updatedState = { + ...updatedState, orm: "none", - turso: "false", - prismaPostgres: "false", + dbSetup: "none", auth: "false", }; - } + } else if (prev.database === "none") { + updatedState.orm = techId === "mongodb" ? "prisma" : "drizzle"; + updatedState.dbSetup = "none"; - if (prev.database === "none") { - return { - ...prev, - database: techId, - orm: "drizzle", - turso: techId === "sqlite" ? prev.turso : "false", - prismaPostgres: - techId === "postgres" && prev.orm === "prisma" - ? prev.prismaPostgres - : "false", - auth: - hasWebFrontend(prev.frontend) || - prev.frontend.includes("native") - ? "true" - : "false", - }; - } - - const updatedState = { - ...prev, - database: techId, - }; - - if (techId === "sqlite") { - updatedState.prismaPostgres = "false"; - } else if (techId === "postgres" && prev.orm === "prisma") { + const hasCompatibleFrontend = + prev.frontend.length > 0 && !prev.frontend.includes("none"); + if (hasCompatibleFrontend) { + updatedState.auth = "true"; + } } else { - updatedState.turso = "false"; + if (techId === "mongodb" && updatedState.orm === "drizzle") { + updatedState.orm = "prisma"; + } + + if (updatedState.dbSetup !== "none") { + if ( + (updatedState.dbSetup === "turso" && techId !== "sqlite") || + (updatedState.dbSetup === "prisma-postgres" && + techId !== "postgres") || + (updatedState.dbSetup === "mongodb-atlas" && + techId !== "mongodb") + ) { + updatedState.dbSetup = "none"; + } + } } return updatedState; @@ -396,31 +414,41 @@ const StackArchitect = () => { orm: techId, }; - if (techId === "prisma") { - updatedState.turso = "false"; - if (prev.database === "postgres") { - } else { - updatedState.prismaPostgres = "false"; + if (updatedState.dbSetup !== "none") { + if ( + (updatedState.dbSetup === "turso" && techId === "prisma") || + (updatedState.dbSetup === "prisma-postgres" && + techId !== "prisma") + ) { + updatedState.dbSetup = "none"; } - } else if (techId === "drizzle" || techId === "none") { - updatedState.prismaPostgres = "false"; } return updatedState; } - if ( - category === "turso" && - (prev.database !== "sqlite" || prev.orm === "prisma") - ) { - return prev; - } + if (category === "dbSetup") { + if (prev.database === "none" && techId !== "none") { + return prev; + } - if ( - category === "prismaPostgres" && - (prev.database !== "postgres" || prev.orm !== "prisma") - ) { - return prev; + const updatedState = { + ...prev, + dbSetup: techId, + }; + + if (techId === "turso") { + updatedState.database = "sqlite"; + updatedState.orm = "drizzle"; + } else if (techId === "prisma-postgres") { + updatedState.database = "postgres"; + updatedState.orm = "prisma"; + } else if (techId === "mongodb-atlas") { + updatedState.database = "mongodb"; + updatedState.orm = "prisma"; + } + + return updatedState; } if ( @@ -440,15 +468,6 @@ const StackArchitect = () => { [], ); - const hasWebFrontend = useCallback((frontendOptions: string[]) => { - return ( - frontendOptions.includes("tanstack-router") || - frontendOptions.includes("react-router") || - frontendOptions.includes("tanstack-start") || - frontendOptions.includes("native") - ); - }, []); - const copyToClipboard = useCallback(() => { navigator.clipboard.writeText(command); setCopied(true); @@ -698,12 +717,16 @@ const StackArchitect = () => { const isDisabled = (activeTab === "orm" && stack.database === "none") || - (activeTab === "turso" && - (stack.database !== "sqlite" || - stack.orm === "prisma")) || - (activeTab === "prismaPostgres" && - (stack.database !== "postgres" || - stack.orm !== "prisma")) || + (activeTab === "dbSetup" && + ((tech.id !== "none" && stack.database === "none") || + (tech.id === "turso" && + (stack.database !== "sqlite" || + stack.orm === "prisma")) || + (tech.id === "prisma-postgres" && + (stack.database !== "postgres" || + stack.orm !== "prisma")) || + (tech.id === "mongodb-atlas" && + stack.database !== "mongodb"))) || (activeTab === "examples" && (((tech.id === "todo" || tech.id === "ai") && !hasWebFrontendSelected) || @@ -866,7 +889,7 @@ const StackArchitect = () => { } - {stack.orm && stack.database !== "none" && ( + {stack.orm !== "none" && stack.database !== "none" && ( {TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.icon}{" "} {TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.name} @@ -882,37 +905,18 @@ const StackArchitect = () => { )} - {stack.turso === "true" && - stack.database === "sqlite" && - stack.orm !== "prisma" && ( - - { - TECH_OPTIONS.turso.find((t) => t.id === stack.turso) - ?.icon - }{" "} - { - TECH_OPTIONS.turso.find((t) => t.id === stack.turso) - ?.name - } - - )} - - {stack.prismaPostgres === "true" && - stack.database === "postgres" && - stack.orm === "prisma" && ( - - { - TECH_OPTIONS.prismaPostgres.find( - (t) => t.id === stack.prismaPostgres, - )?.icon - }{" "} - { - TECH_OPTIONS.prismaPostgres.find( - (t) => t.id === stack.prismaPostgres, - )?.name - } - - )} + {stack.dbSetup !== "none" && ( + + { + TECH_OPTIONS.dbSetup.find((t) => t.id === stack.dbSetup) + ?.icon + }{" "} + { + TECH_OPTIONS.dbSetup.find((t) => t.id === stack.dbSetup) + ?.name + } + + )} {stack.addons.map((addonId) => { const addon = TECH_OPTIONS.addons.find( diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index 280b742..efb1f06 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -74,6 +74,13 @@ export const TECH_OPTIONS = { icon: "đŸĻŠ", color: "from-purple-500 to-purple-700", }, + { + id: "express", + name: "Express", + description: "Popular Node.js framework", + icon: "🚂", + color: "from-gray-500 to-gray-700", + }, ], database: [ { @@ -91,6 +98,13 @@ export const TECH_OPTIONS = { icon: "🐘", color: "from-indigo-400 to-indigo-600", }, + { + id: "mongodb", + name: "MongoDB", + description: "NoSQL document database", + icon: "🍃", + color: "from-green-400 to-green-600", + }, { id: "none", name: "No Database", @@ -115,6 +129,44 @@ export const TECH_OPTIONS = { icon: "◮", color: "from-purple-400 to-purple-600", }, + { + id: "none", + name: "No ORM", + description: "Skip ORM integration", + icon: "đŸšĢ", + color: "from-gray-400 to-gray-600", + }, + ], + dbSetup: [ + { + id: "turso", + name: "Turso", + description: "SQLite cloud database powered by libSQL", + icon: "â˜ī¸", + color: "from-pink-400 to-pink-600", + }, + { + id: "prisma-postgres", + name: "Prisma PostgreSQL", + description: "Set up PostgreSQL with Prisma", + icon: "🐘", + color: "from-indigo-400 to-indigo-600", + }, + { + id: "mongodb-atlas", + name: "MongoDB Atlas", + description: "Cloud MongoDB setup with Atlas", + icon: "đŸŒŠī¸", + color: "from-green-400 to-green-600", + }, + { + id: "none", + name: "Basic Setup", + description: "No cloud DB integration", + icon: "đŸ’ģ", + color: "from-gray-400 to-gray-600", + default: true, + }, ], auth: [ { @@ -133,42 +185,6 @@ export const TECH_OPTIONS = { color: "from-red-400 to-red-600", }, ], - turso: [ - { - id: "true", - name: "Turso", - description: "SQLite cloud database", - icon: "â˜ī¸", - color: "from-pink-400 to-pink-600", - default: false, - }, - { - id: "false", - name: "No Turso", - description: "Skip Turso integration", - icon: "đŸšĢ", - color: "from-gray-400 to-gray-600", - default: true, - }, - ], - prismaPostgres: [ - { - id: "true", - name: "Prisma PostgreSQL", - description: "Set up PostgreSQL with Prisma", - icon: "🐘", - color: "from-indigo-400 to-indigo-600", - default: false, - }, - { - id: "false", - name: "Skip Prisma PostgreSQL", - description: "Basic Prisma setup", - icon: "đŸšĢ", - color: "from-gray-400 to-gray-600", - default: true, - }, - ], packageManager: [ { id: "npm", @@ -293,9 +309,8 @@ export const PRESET_TEMPLATES = [ backendFramework: "hono", database: "sqlite", orm: "drizzle", + dbSetup: "none", auth: "true", - turso: "false", - prismaPostgres: "false", packageManager: "bun", addons: [], examples: [], @@ -314,9 +329,8 @@ export const PRESET_TEMPLATES = [ backendFramework: "hono", database: "sqlite", orm: "drizzle", + dbSetup: "none", auth: "true", - turso: "false", - prismaPostgres: "false", packageManager: "bun", addons: [], examples: [], @@ -335,9 +349,8 @@ export const PRESET_TEMPLATES = [ backendFramework: "hono", database: "postgres", orm: "drizzle", + dbSetup: "none", auth: "false", - turso: "false", - prismaPostgres: "false", packageManager: "bun", addons: [], examples: [], @@ -356,9 +369,8 @@ export const PRESET_TEMPLATES = [ backendFramework: "hono", database: "sqlite", orm: "drizzle", + dbSetup: "turso", auth: "true", - turso: "true", - prismaPostgres: "false", packageManager: "bun", addons: ["pwa", "biome", "husky", "tauri"], examples: ["todo", "ai"], @@ -374,10 +386,9 @@ export type StackState = { runtime: string; backendFramework: string; database: string; - orm: string | null; + orm: string; + dbSetup: string; auth: string; - turso: string; - prismaPostgres: string; packageManager: string; addons: string[]; examples: string[]; @@ -392,9 +403,8 @@ export const DEFAULT_STACK: StackState = { backendFramework: "hono", database: "sqlite", orm: "drizzle", + dbSetup: "none", auth: "true", - turso: "false", - prismaPostgres: "false", packageManager: "bun", addons: [], examples: [],