add express, automated mongodb atlas setup, fix stack architech

This commit is contained in:
Aman Varshney
2025-04-07 21:32:22 +05:30
parent c6c73fce76
commit 2cf01d155b
38 changed files with 902 additions and 393 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---
Add express backend, mongodb database and automated mongodb atlas setup

View File

@@ -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 - **Monorepo**: Turborepo for optimized build system and workspace management
- **Frontend**: React, TanStack Router, TanStack Query, Tailwind CSS with shadcn/ui components - **Frontend**: React, TanStack Router, TanStack Query, Tailwind CSS with shadcn/ui components
- **Native Apps**: Create React Native apps with Expo for iOS and Android - **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 - **API Layer**: End-to-end type safety with tRPC
- **Runtime Options**: Choose between Bun or Node.js for your server - **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 - **ORM Selection**: Choose between Drizzle ORM or Prisma
- **Authentication**: Optional auth setup with Better-Auth - **Authentication**: Optional auth setup with Better-Auth
- **Progressive Web App**: Add PWA support with service workers and installable apps - **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: Options:
-V, --version Output the version number -V, --version Output the version number
-y, --yes Use default configuration -y, --yes Use default configuration
--database <type> Database type (none, sqlite, postgres) --database <type> Database type (none, sqlite, postgres, mongodb)
--orm <type> ORM type (none, drizzle, prisma) --orm <type> ORM type (none, drizzle, prisma)
--auth Include authentication --auth Include authentication
--no-auth Exclude authentication --no-auth Exclude authentication
@@ -58,11 +58,8 @@ Options:
--package-manager <pm> Package manager (npm, pnpm, bun) --package-manager <pm> Package manager (npm, pnpm, bun)
--install Install dependencies --install Install dependencies
--no-install Skip installing dependencies --no-install Skip installing dependencies
--turso Set up Turso for SQLite database --db-setup <setup> Database setup (turso, prisma-postgres, mongodb-atlas, none)
--no-turso Skip Turso setup --backend <framework> Backend framework (hono, express, elysia)
--prisma-postgres Set up Prisma Postgres
--no-prisma-postgres Skip Prisma Postgres setup
--backend <framework> Backend framework (hono, elysia)
--runtime <runtime> Runtime (bun, node) --runtime <runtime> Runtime (bun, node)
-h, --help Display help -h, --help Display help
``` ```
@@ -93,3 +90,8 @@ Create a project with examples:
```bash ```bash
npx create-better-t-stack my-app --examples todo ai 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
```

View File

@@ -7,10 +7,7 @@
"bin": { "bin": {
"create-better-t-stack": "dist/index.js" "create-better-t-stack": "dist/index.js"
}, },
"files": [ "files": ["template", "dist"],
"template",
"dist"
],
"keywords": [ "keywords": [
"better-t-stack", "better-t-stack",
"typescript", "typescript",

View File

@@ -18,8 +18,7 @@ export const DEFAULT_CONFIG: ProjectConfig = {
git: true, git: true,
packageManager: getUserPkgManager(), packageManager: getUserPkgManager(),
noInstall: false, noInstall: false,
turso: false, dbSetup: "none",
prismaPostgres: false,
backend: "hono", backend: "hono",
runtime: "bun", runtime: "bun",
}; };
@@ -62,6 +61,11 @@ export const dependencyVersionMap = {
"@hono/trpc-server": "^0.3.4", "@hono/trpc-server": "^0.3.4",
hono: "^4.7.5", 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: "^4.2.8",
"@ai-sdk/google": "^1.2.3", "@ai-sdk/google": "^1.2.3",

View File

@@ -27,6 +27,13 @@ export async function setupBackendDependencies(
dependencies.push("@elysiajs/node"); dependencies.push("@elysiajs/node");
devDependencies.push("tsx", "@types/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") { if (runtime === "bun") {

View File

@@ -54,8 +54,9 @@ export async function createProject(options: ProjectConfig): Promise<string> {
options.database, options.database,
options.orm, options.orm,
options.packageManager, options.packageManager,
options.turso ?? options.database === "sqlite", options.dbSetup === "turso",
options.prismaPostgres, options.dbSetup === "prisma-postgres",
options.dbSetup === "mongodb-atlas",
); );
await setupAuthTemplate( await setupAuthTemplate(

View File

@@ -8,6 +8,7 @@ import type {
ProjectPackageManager, ProjectPackageManager,
} from "../types"; } from "../types";
import { addPackageDependency } from "../utils/add-package-deps"; import { addPackageDependency } from "../utils/add-package-deps";
import { setupMongoDBAtlas } from "./mongodb-atlas-setup";
import { setupPrismaPostgres } from "./prisma-postgres-setup"; import { setupPrismaPostgres } from "./prisma-postgres-setup";
import { setupTurso } from "./turso-setup"; import { setupTurso } from "./turso-setup";
@@ -18,6 +19,7 @@ export async function setupDatabase(
packageManager: ProjectPackageManager, packageManager: ProjectPackageManager,
setupTursoDb: boolean, setupTursoDb: boolean,
setupPrismaPostgresDb: boolean, setupPrismaPostgresDb: boolean,
setupMongoDBAtlasDb: boolean,
): Promise<void> { ): Promise<void> {
const s = spinner(); const s = spinner();
const serverDir = path.join(projectDir, "apps/server"); const serverDir = path.join(projectDir, "apps/server");
@@ -68,6 +70,18 @@ export async function setupDatabase(
await setupPrismaPostgres(projectDir, packageManager); 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) { } catch (error) {
s.stop(pc.red("Failed to set up database")); s.stop(pc.red("Failed to set up database"));

View File

@@ -44,14 +44,18 @@ export async function setupEnvironmentVariables(
if (options.database !== "none") { if (options.database !== "none") {
if (options.orm === "prisma" && !envContent.includes("DATABASE_URL")) { if (options.orm === "prisma" && !envContent.includes("DATABASE_URL")) {
const databaseUrlLine = let databaseUrlLine = "";
options.database === "sqlite" if (options.database === "sqlite") {
? "" databaseUrlLine = "";
: `\nDATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"`; } 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; envContent += databaseUrlLine;
} }
if (options.database === "sqlite" && !options.turso) { if (options.database === "sqlite" && options.dbSetup !== "turso") {
if (!envContent.includes("TURSO_CONNECTION_URL")) { if (!envContent.includes("TURSO_CONNECTION_URL")) {
envContent += "\nTURSO_CONNECTION_URL=http://127.0.0.1:8080"; envContent += "\nTURSO_CONNECTION_URL=http://127.0.0.1:8080";
} }

View File

@@ -37,7 +37,7 @@ export async function setupExamples(
if ( if (
examples.includes("ai") && examples.includes("ai") &&
backend === "hono" && (backend === "hono" || backend === "express") &&
hasWebFrontend && hasWebFrontend &&
webAppExists webAppExists
) { ) {
@@ -89,6 +89,7 @@ async function updateServerIndexWithAIRoute(projectDir: string): Promise<void> {
if (await fs.pathExists(serverIndexPath)) { if (await fs.pathExists(serverIndexPath)) {
let indexContent = await fs.readFile(serverIndexPath, "utf8"); let indexContent = await fs.readFile(serverIndexPath, "utf8");
const isHono = indexContent.includes("hono"); const isHono = indexContent.includes("hono");
const isExpress = indexContent.includes("express");
if (isHono) { if (isHono) {
const importSection = `import { streamText } from "ai";\nimport { google } from "@ai-sdk/google";\nimport { stream } from "hono/streaming";`; 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())); return stream(c, (stream) => stream.pipe(result.toDataStream()));
});`; });`;
// Add imports and route handler for Hono
if (indexContent.includes("import {")) { if (indexContent.includes("import {")) {
const lastImportIndex = indexContent.lastIndexOf("import"); const lastImportIndex = indexContent.lastIndexOf("import");
const endOfLastImport = indexContent.indexOf("\n", lastImportIndex); const endOfLastImport = indexContent.indexOf("\n", lastImportIndex);
@@ -141,11 +143,68 @@ ${indexContent.substring(exportIndex)}`;
${aiRouteHandler}`; ${aiRouteHandler}`;
} }
} }
} else if (isExpress) {
// Express implementation
const importSection = `import { streamText } from "ai";\nimport { google } from "@ai-sdk/google";`;
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); await fs.writeFile(serverIndexPath, indexContent);
} }
} }
}
async function updateHeaderWithAILink( async function updateHeaderWithAILink(
projectDir: string, projectDir: string,

View File

@@ -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<boolean> {
return commandExists("atlas");
}
async function initMongoDBAtlas(
serverDir: string,
): Promise<MongoDBConfig | null> {
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();
}
}

View File

@@ -2,6 +2,7 @@ import { note } from "@clack/prompts";
import pc from "picocolors"; import pc from "picocolors";
import type { import type {
ProjectAddons, ProjectAddons,
ProjectDBSetup,
ProjectDatabase, ProjectDatabase,
ProjectFrontend, ProjectFrontend,
ProjectOrm, ProjectOrm,
@@ -18,6 +19,7 @@ export function displayPostInstallInstructions(
addons: ProjectAddons[], addons: ProjectAddons[],
runtime: ProjectRuntime, runtime: ProjectRuntime,
frontends: ProjectFrontend[], frontends: ProjectFrontend[],
dbSetup?: ProjectDBSetup,
) { ) {
const runCmd = packageManager === "npm" ? "npm run" : packageManager; const runCmd = packageManager === "npm" ? "npm run" : packageManager;
const cdCmd = `cd ${projectName}`; const cdCmd = `cd ${projectName}`;

View File

@@ -372,9 +372,9 @@ function getOrmTemplateDir(orm: ProjectOrm, database: ProjectDatabase): string {
} }
if (orm === "prisma") { if (orm === "prisma") {
return database === "sqlite" if (database === "sqlite") return "template/with-prisma-sqlite";
? "template/with-prisma-sqlite" if (database === "postgres") return "template/with-prisma-postgres";
: "template/with-prisma-postgres"; if (database === "mongodb") return "template/with-prisma-mongodb";
} }
return "template/base"; return "template/base";
@@ -388,9 +388,9 @@ function getAuthLibDir(orm: ProjectOrm, database: ProjectDatabase): string {
} }
if (orm === "prisma") { if (orm === "prisma") {
return database === "sqlite" if (database === "sqlite") return "with-prisma-sqlite-lib";
? "with-prisma-sqlite-lib" if (database === "postgres") return "with-prisma-postgres-lib";
: "with-prisma-postgres-lib"; if (database === "mongodb") return "with-prisma-mongodb-lib";
} }
throw new Error("Invalid ORM or database configuration for auth setup"); throw new Error("Invalid ORM or database configuration for auth setup");

View File

@@ -12,6 +12,7 @@ import {
import { $ } from "execa"; import { $ } from "execa";
import fs from "fs-extra"; import fs from "fs-extra";
import pc from "picocolors"; import pc from "picocolors";
import { commandExists } from "../utils/command-exists";
type TursoConfig = { type TursoConfig = {
dbUrl: string; dbUrl: string;
@@ -26,12 +27,7 @@ type TursoGroup = {
}; };
async function isTursoInstalled() { async function isTursoInstalled() {
try { return commandExists("turso");
const result = await $`turso --version`;
return result.exitCode === 0;
} catch (error) {
return false;
}
} }
async function isTursoLoggedIn() { async function isTursoLoggedIn() {

View File

@@ -9,6 +9,7 @@ import type {
ProjectAddons, ProjectAddons,
ProjectBackend, ProjectBackend,
ProjectConfig, ProjectConfig,
ProjectDBSetup,
ProjectDatabase, ProjectDatabase,
ProjectExamples, ProjectExamples,
ProjectFrontend, ProjectFrontend,
@@ -37,8 +38,11 @@ async function main() {
.version(getLatestCLIVersion()) .version(getLatestCLIVersion())
.argument("[project-directory]", "Project name/directory") .argument("[project-directory]", "Project name/directory")
.option("-y, --yes", "Use default configuration") .option("-y, --yes", "Use default configuration")
.option("--database <type>", "Database type (none, sqlite, postgres)") .option(
.option("--orm <type>", "ORM type (none, drizzle, prisma)") "--database <type>",
"Database type (none, sqlite, postgres, mongodb)",
)
.option("--orm <type>", "ORM type (drizzle, prisma)")
.option("--auth", "Include authentication") .option("--auth", "Include authentication")
.option("--no-auth", "Exclude authentication") .option("--no-auth", "Exclude authentication")
.option( .option(
@@ -56,11 +60,14 @@ async function main() {
.option("--package-manager <pm>", "Package manager (npm, pnpm, bun)") .option("--package-manager <pm>", "Package manager (npm, pnpm, bun)")
.option("--install", "Install dependencies") .option("--install", "Install dependencies")
.option("--no-install", "Skip installing dependencies") .option("--no-install", "Skip installing dependencies")
.option("--turso", "Set up Turso for SQLite database") .option(
.option("--no-turso", "Skip Turso setup") "--db-setup <setup>",
.option("--prisma-postgres", "Set up Prisma Postgres") "Database setup (turso, prisma-postgres, mongodb-atlas, none)",
.option("--no-prisma-postgres", "Skip Prisma Postgres setup") )
.option("--backend <framework>", "Backend framework (hono, elysia)") .option(
"--backend <framework>",
"Backend framework (hono, express, elysia)",
)
.option("--runtime <runtime>", "Runtime (bun, node)") .option("--runtime <runtime>", "Runtime (bun, node)")
.parse(); .parse();
@@ -125,20 +132,41 @@ async function main() {
function validateOptions(options: CLIOptions): void { function validateOptions(options: CLIOptions): void {
if ( if (
options.database && options.database &&
!["none", "sqlite", "postgres"].includes(options.database) !["none", "sqlite", "postgres", "mongodb"].includes(options.database)
) { ) {
cancel( cancel(
pc.red( 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); 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( cancel(
pc.red( 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); process.exit(1);
@@ -163,51 +191,62 @@ function validateOptions(options: CLIOptions): void {
process.exit(1); process.exit(1);
} }
if ("turso" in options && options.turso === true) { if (options.dbSetup && options.dbSetup !== "none") {
cancel( cancel(
pc.red( 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); process.exit(1);
} }
} }
if ( // Check for database setup compatibility
"turso" in options && if (options.dbSetup === "turso") {
options.turso === true && if (options.database && options.database !== "sqlite") {
options.database &&
options.database !== "sqlite"
) {
cancel( cancel(
pc.red( pc.red(
`Turso setup requires a SQLite database. Cannot use --turso with --database ${options.database}`, `Turso setup requires a SQLite database. Cannot use --db-setup turso with --database ${options.database}`,
), ),
); );
process.exit(1); process.exit(1);
} }
if ( if (options.orm === "prisma") {
"turso" in options &&
options.turso === true &&
options.orm === "prisma"
) {
cancel( cancel(
pc.red( pc.red(
"Turso setup is not compatible with Prisma. Cannot use --turso with --orm prisma", "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); process.exit(1);
} }
if ("prismaPostgres" in options && options.prismaPostgres === true) { if (options.orm && options.orm !== "prisma") {
if (
(options.database && options.database !== "postgres") ||
(options.orm && options.orm !== "prisma")
) {
cancel( cancel(
pc.red( pc.red(
"Prisma PostgreSQL setup requires PostgreSQL database with Prisma ORM. Cannot use --prisma-postgres with incompatible database or ORM options.", "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); process.exit(1);
@@ -226,10 +265,13 @@ function validateOptions(options: CLIOptions): void {
process.exit(1); process.exit(1);
} }
if (options.backend && !["hono", "elysia"].includes(options.backend)) { if (
options.backend &&
!["hono", "elysia", "express"].includes(options.backend)
) {
cancel( cancel(
pc.red( 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); 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; let examples: ProjectExamples[] | undefined;
if ("examples" in options) { if ("examples" in options) {
if (options.examples === false) { 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 backend = options.backend as ProjectBackend | undefined;
const runtime = options.runtime as ProjectRuntime | undefined; const runtime = options.runtime as ProjectRuntime | undefined;
const packageManager = options.packageManager as const packageManager = options.packageManager as
| ProjectPackageManager | ProjectPackageManager
| undefined; | 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<ProjectConfig> = {}; const config: Partial<ProjectConfig> = {};
if (projectDirectory) config.projectName = projectDirectory; if (projectDirectory) config.projectName = projectDirectory;
@@ -532,9 +570,7 @@ function processFlags(
if (packageManager) config.packageManager = packageManager; if (packageManager) config.packageManager = packageManager;
if ("git" in options) config.git = options.git; if ("git" in options) config.git = options.git;
if ("install" in options) config.noInstall = !options.install; if ("install" in options) config.noInstall = !options.install;
if (tursoOption !== undefined) config.turso = tursoOption; if (dbSetup !== undefined) config.dbSetup = dbSetup;
if (prismaPostgresOption !== undefined)
config.prismaPostgres = prismaPostgresOption;
if (backend) config.backend = backend; if (backend) config.backend = backend;
if (runtime) config.runtime = runtime; if (runtime) config.runtime = runtime;
if (frontend !== undefined) config.frontend = frontend; if (frontend !== undefined) config.frontend = frontend;

View File

@@ -16,6 +16,11 @@ export async function getBackendFrameworkChoice(
label: "Hono", label: "Hono",
hint: "Lightweight, ultrafast web framework", hint: "Lightweight, ultrafast web framework",
}, },
{
value: "express",
label: "Express",
hint: "Fast, unopinionated, minimalist web framework for Node.js",
},
{ {
value: "elysia", value: "elysia",
label: "Elysia", label: "Elysia",

View File

@@ -1,9 +1,10 @@
import { cancel, group } from "@clack/prompts"; import { cancel, group, log } from "@clack/prompts";
import pc from "picocolors"; import pc from "picocolors";
import type { import type {
ProjectAddons, ProjectAddons,
ProjectBackend, ProjectBackend,
ProjectConfig, ProjectConfig,
ProjectDBSetup,
ProjectDatabase, ProjectDatabase,
ProjectExamples, ProjectExamples,
ProjectFrontend, ProjectFrontend,
@@ -15,16 +16,15 @@ import { getAddonsChoice } from "./addons";
import { getAuthChoice } from "./auth"; import { getAuthChoice } from "./auth";
import { getBackendFrameworkChoice } from "./backend-framework"; import { getBackendFrameworkChoice } from "./backend-framework";
import { getDatabaseChoice } from "./database"; import { getDatabaseChoice } from "./database";
import { getDBSetupChoice } from "./db-setup";
import { getExamplesChoice } from "./examples"; import { getExamplesChoice } from "./examples";
import { getFrontendChoice } from "./frontend-option"; import { getFrontendChoice } from "./frontend-option";
import { getGitChoice } from "./git"; import { getGitChoice } from "./git";
import { getNoInstallChoice } from "./install"; import { getNoInstallChoice } from "./install";
import { getORMChoice } from "./orm"; import { getORMChoice } from "./orm";
import { getPackageManagerChoice } from "./package-manager"; import { getPackageManagerChoice } from "./package-manager";
import { getPrismaSetupChoice } from "./prisma-postgres";
import { getProjectName } from "./project-name"; import { getProjectName } from "./project-name";
import { getRuntimeChoice } from "./runtime"; import { getRuntimeChoice } from "./runtime";
import { getTursoSetupChoice } from "./turso";
type PromptGroupResults = { type PromptGroupResults = {
projectName: string; projectName: string;
@@ -36,8 +36,7 @@ type PromptGroupResults = {
git: boolean; git: boolean;
packageManager: ProjectPackageManager; packageManager: ProjectPackageManager;
noInstall: boolean; noInstall: boolean;
turso: boolean; dbSetup: ProjectDBSetup;
prismaPostgres: boolean;
backend: ProjectBackend; backend: ProjectBackend;
runtime: ProjectRuntime; runtime: ProjectRuntime;
frontend: ProjectFrontend[]; frontend: ProjectFrontend[];
@@ -46,6 +45,32 @@ type PromptGroupResults = {
export async function gatherConfig( export async function gatherConfig(
flags: Partial<ProjectConfig>, flags: Partial<ProjectConfig>,
): Promise<ProjectConfig> { ): Promise<ProjectConfig> {
// 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<PromptGroupResults>( const result = await group<PromptGroupResults>(
{ {
projectName: async () => { projectName: async () => {
@@ -56,21 +81,15 @@ export async function gatherConfig(
runtime: () => getRuntimeChoice(flags.runtime), runtime: () => getRuntimeChoice(flags.runtime),
database: () => getDatabaseChoice(flags.database), database: () => getDatabaseChoice(flags.database),
orm: ({ results }) => orm: ({ results }) =>
getORMChoice(flags.orm, results.database !== "none"), getORMChoice(flags.orm, results.database !== "none", results.database),
auth: ({ results }) => auth: ({ results }) =>
getAuthChoice( getAuthChoice(
flags.auth, flags.auth,
results.database !== "none", results.database !== "none",
results.frontend, results.frontend,
), ),
turso: ({ results }) => dbSetup: ({ results }) =>
results.database === "sqlite" && results.orm !== "prisma" getDBSetupChoice(results.database ?? "none", flags.dbSetup),
? getTursoSetupChoice(flags.turso)
: Promise.resolve(false),
prismaPostgres: ({ results }) =>
results.database === "postgres" && results.orm === "prisma"
? getPrismaSetupChoice(flags.prismaPostgres)
: Promise.resolve(false),
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend), addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
examples: ({ results }) => examples: ({ results }) =>
getExamplesChoice( getExamplesChoice(
@@ -102,8 +121,7 @@ export async function gatherConfig(
git: result.git, git: result.git,
packageManager: result.packageManager, packageManager: result.packageManager,
noInstall: result.noInstall, noInstall: result.noInstall,
turso: result.turso, dbSetup: result.dbSetup,
prismaPostgres: result.prismaPostgres,
backend: result.backend, backend: result.backend,
runtime: result.runtime, runtime: result.runtime,
}; };

View File

@@ -26,6 +26,11 @@ export async function getDatabaseChoice(
label: "PostgreSQL", label: "PostgreSQL",
hint: "Traditional relational database", hint: "Traditional relational database",
}, },
{
value: "mongodb",
label: "MongoDB",
hint: "NoSQL document-oriented database",
},
], ],
initialValue: DEFAULT_CONFIG.database, initialValue: DEFAULT_CONFIG.database,
}); });

View File

@@ -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<ProjectDBSetup> {
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<ProjectDBSetup>({
message: `Select ${databaseType} setup option`,
options,
initialValue: "none",
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}

View File

@@ -42,7 +42,7 @@ export async function getExamplesChoice(
}); });
} }
if (backend === "hono") { if (backend === "hono" || backend === "express") {
response = await multiselect<ProjectExamples>({ response = await multiselect<ProjectExamples>({
message: "Include examples", message: "Include examples",
options: [ options: [

View File

@@ -1,15 +1,21 @@
import { cancel, isCancel, select } from "@clack/prompts"; import { cancel, isCancel, log, select } from "@clack/prompts";
import pc from "picocolors"; import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants"; import { DEFAULT_CONFIG } from "../constants";
import type { ProjectOrm } from "../types"; import type { ProjectDatabase, ProjectOrm } from "../types";
export async function getORMChoice( export async function getORMChoice(
orm: ProjectOrm | undefined, orm: ProjectOrm | undefined,
hasDatabase: boolean, hasDatabase: boolean,
database?: ProjectDatabase,
): Promise<ProjectOrm> { ): Promise<ProjectOrm> {
if (!hasDatabase) return "none"; if (!hasDatabase) return "none";
if (orm !== undefined) return orm; if (orm !== undefined) return orm;
if (database === "mongodb") {
log.info("Only Prisma is supported with MongoDB.");
return "prisma";
}
const response = await select<ProjectOrm>({ const response = await select<ProjectOrm>({
message: "Select ORM", message: "Select ORM",
options: [ options: [

View File

@@ -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<boolean> {
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;
}

View File

@@ -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<boolean> {
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;
}

View File

@@ -1,8 +1,8 @@
export type ProjectDatabase = "sqlite" | "postgres" | "none"; export type ProjectDatabase = "sqlite" | "postgres" | "mongodb" | "none";
export type ProjectOrm = "drizzle" | "prisma" | "none"; export type ProjectOrm = "drizzle" | "prisma";
export type ProjectPackageManager = "npm" | "pnpm" | "bun"; export type ProjectPackageManager = "npm" | "pnpm" | "bun";
export type ProjectAddons = "pwa" | "biome" | "tauri" | "husky"; 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 ProjectRuntime = "node" | "bun";
export type ProjectExamples = "todo" | "ai"; export type ProjectExamples = "todo" | "ai";
export type ProjectFrontend = export type ProjectFrontend =
@@ -10,6 +10,11 @@ export type ProjectFrontend =
| "tanstack-router" | "tanstack-router"
| "tanstack-start" | "tanstack-start"
| "native"; | "native";
export type ProjectDBSetup =
| "turso"
| "prisma-postgres"
| "mongodb-atlas"
| "none";
export interface ProjectConfig { export interface ProjectConfig {
projectName: string; projectName: string;
@@ -22,9 +27,8 @@ export interface ProjectConfig {
examples: ProjectExamples[]; examples: ProjectExamples[];
git: boolean; git: boolean;
packageManager: ProjectPackageManager; packageManager: ProjectPackageManager;
noInstall?: boolean; noInstall: boolean;
turso?: boolean; dbSetup: ProjectDBSetup;
prismaPostgres: boolean;
frontend: ProjectFrontend[]; frontend: ProjectFrontend[];
} }
@@ -39,8 +43,7 @@ export type CLIOptions = {
git?: boolean; git?: boolean;
packageManager?: string; packageManager?: string;
install?: boolean; install?: boolean;
turso?: boolean; dbSetup?: string;
prismaPostgres?: boolean;
backend?: string; backend?: string;
runtime?: string; runtime?: string;
}; };

View File

@@ -0,0 +1,16 @@
import { execa } from "execa";
export async function commandExists(command: string): Promise<boolean> {
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;
}
}

View File

@@ -60,14 +60,8 @@ export function displayConfig(config: Partial<ProjectConfig>) {
configDisplay.push(`${pc.blue("Skip Install:")} ${config.noInstall}`); configDisplay.push(`${pc.blue("Skip Install:")} ${config.noInstall}`);
} }
if (config.turso !== undefined) { if (config.dbSetup !== undefined) {
configDisplay.push(`${pc.blue("Turso Setup:")} ${config.turso}`); configDisplay.push(`${pc.blue("Database Setup:")} ${config.dbSetup}`);
}
if (config.prismaPostgres !== undefined) {
configDisplay.push(
`${pc.blue("Prisma Postgres Setup:")} ${config.prismaPostgres ? "Yes" : "No"}`,
);
} }
return configDisplay.join("\n"); return configDisplay.join("\n");

View File

@@ -12,8 +12,8 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
flags.push(`--orm ${config.orm}`); flags.push(`--orm ${config.orm}`);
} }
if (config.database === "sqlite") { if (config.dbSetup && config.dbSetup !== "none") {
flags.push(config.turso ? "--turso" : "--no-turso"); flags.push(`--db-setup ${config.dbSetup}`);
} }
} }

View File

@@ -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<ReturnType<typeof createContext>>;

View File

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

View File

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

View File

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

View File

@@ -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<ReturnType<typeof createContext>>;

View File

@@ -0,0 +1,5 @@
import { PrismaClient } from "@prisma/client";
let prisma = new PrismaClient();
export default prisma;

View File

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

View File

@@ -0,0 +1,9 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["prismaSchemaFolder"]
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}

View File

@@ -0,0 +1,7 @@
model Todo {
id String @id @default(auto()) @map("_id") @db.ObjectId
text String
completed Boolean @default(false)
@@map("todo")
}

View File

@@ -4,7 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"build": "next build", "build": "next build",
"dev": "next dev", "dev": "next dev --turbopack",
"start": "next start", "start": "next start",
"check": "biome check --write .", "check": "biome check --write .",
"postinstall": "fumadocs-mdx" "postinstall": "fumadocs-mdx"

View File

@@ -74,32 +74,45 @@ const StackArchitect = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (stack.database === "none" && stack.orm !== "none") { if (stack.database === "none") {
if (stack.orm !== "none") {
setStack((prev) => ({ ...prev, orm: "none" })); setStack((prev) => ({ ...prev, orm: "none" }));
} }
if (stack.auth === "true") {
if (stack.database !== "postgres" || stack.orm !== "prisma") {
if (stack.prismaPostgres === "true") {
setStack((prev) => ({ ...prev, prismaPostgres: "false" }));
}
}
if (stack.database !== "sqlite" || stack.orm === "prisma") {
if (stack.turso === "true") {
setStack((prev) => ({ ...prev, turso: "false" }));
}
}
if (stack.database === "none" && stack.auth === "true") {
setStack((prev) => ({ ...prev, auth: "false" })); setStack((prev) => ({ ...prev, auth: "false" }));
} }
}, [ if (stack.dbSetup !== "none") {
stack.database, setStack((prev) => ({ ...prev, dbSetup: "none" }));
stack.orm, }
stack.prismaPostgres, }
stack.turso,
stack.auth, if (stack.database === "mongodb" && stack.orm === "drizzle") {
]); setStack((prev) => ({ ...prev, orm: "prisma" }));
}
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.dbSetup, stack.auth]);
useEffect(() => { useEffect(() => {
const cmd = generateCommand(stack); const cmd = generateCommand(stack);
@@ -113,6 +126,33 @@ const StackArchitect = () => {
notes.frontend = []; 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 = []; notes.addons = [];
if (!hasWebFrontend) { if (!hasWebFrontend) {
notes.addons.push("PWA and Tauri are only available with React Web."); notes.addons.push("PWA and Tauri are only available with React Web.");
@@ -125,6 +165,8 @@ const StackArchitect = () => {
notes.orm.push( notes.orm.push(
"ORM options are only available when a database is selected.", "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 = []; notes.auth = [];
@@ -132,23 +174,6 @@ const StackArchitect = () => {
notes.auth.push("Authentication requires a database."); 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 = []; notes.examples = [];
if (!hasWebFrontend) { if (!hasWebFrontend) {
notes.examples.push( notes.examples.push(
@@ -198,12 +223,8 @@ const StackArchitect = () => {
flags.push("--no-auth"); flags.push("--no-auth");
} }
if (stackState.turso === "true") { if (stackState.dbSetup !== "none") {
flags.push("--turso"); flags.push(`--db-setup ${stackState.dbSetup}`);
}
if (stackState.prismaPostgres === "true") {
flags.push("--prisma-postgres");
} }
if (stackState.backendFramework !== "hono") { if (stackState.backendFramework !== "hono") {
@@ -263,7 +284,6 @@ const StackArchitect = () => {
if (currentSelection.length === 1) { if (currentSelection.length === 1) {
return prev; return prev;
} }
return { return {
...prev, ...prev,
frontend: currentSelection.filter((id) => id !== techId), frontend: currentSelection.filter((id) => id !== techId),
@@ -296,6 +316,10 @@ const StackArchitect = () => {
prev.frontend.includes("react-router") || prev.frontend.includes("react-router") ||
prev.frontend.includes("tanstack-start"); prev.frontend.includes("tanstack-start");
const hasPWACompatibleFrontend =
prev.frontend.includes("tanstack-router") ||
prev.frontend.includes("react-router");
if (index >= 0) { if (index >= 0) {
currentArray.splice(index, 1); currentArray.splice(index, 1);
} else { } else {
@@ -318,8 +342,7 @@ const StackArchitect = () => {
if ( if (
category === "addons" && category === "addons" &&
(techId === "pwa" || techId === "tauri") && (techId === "pwa" || techId === "tauri") &&
!prev.frontend.includes("tanstack-router") && !hasPWACompatibleFrontend
!prev.frontend.includes("react-router")
) { ) {
return prev; return prev;
} }
@@ -342,45 +365,40 @@ const StackArchitect = () => {
} }
if (category === "database") { if (category === "database") {
let updatedState = { ...prev, database: techId };
if (techId === "none") { if (techId === "none") {
return { updatedState = {
...prev, ...updatedState,
database: techId,
orm: "none", orm: "none",
turso: "false", dbSetup: "none",
prismaPostgres: "false",
auth: "false", auth: "false",
}; };
} else if (prev.database === "none") {
updatedState.orm = techId === "mongodb" ? "prisma" : "drizzle";
updatedState.dbSetup = "none";
const hasCompatibleFrontend =
prev.frontend.length > 0 && !prev.frontend.includes("none");
if (hasCompatibleFrontend) {
updatedState.auth = "true";
} }
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") {
} else { } 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; return updatedState;
@@ -396,31 +414,41 @@ const StackArchitect = () => {
orm: techId, orm: techId,
}; };
if (techId === "prisma") { if (updatedState.dbSetup !== "none") {
updatedState.turso = "false"; if (
if (prev.database === "postgres") { (updatedState.dbSetup === "turso" && techId === "prisma") ||
} else { (updatedState.dbSetup === "prisma-postgres" &&
updatedState.prismaPostgres = "false"; techId !== "prisma")
) {
updatedState.dbSetup = "none";
} }
} else if (techId === "drizzle" || techId === "none") {
updatedState.prismaPostgres = "false";
} }
return updatedState; return updatedState;
} }
if ( if (category === "dbSetup") {
category === "turso" && if (prev.database === "none" && techId !== "none") {
(prev.database !== "sqlite" || prev.orm === "prisma")
) {
return prev; return prev;
} }
if ( const updatedState = {
category === "prismaPostgres" && ...prev,
(prev.database !== "postgres" || prev.orm !== "prisma") dbSetup: techId,
) { };
return prev;
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 ( 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(() => { const copyToClipboard = useCallback(() => {
navigator.clipboard.writeText(command); navigator.clipboard.writeText(command);
setCopied(true); setCopied(true);
@@ -698,12 +717,16 @@ const StackArchitect = () => {
const isDisabled = const isDisabled =
(activeTab === "orm" && stack.database === "none") || (activeTab === "orm" && stack.database === "none") ||
(activeTab === "turso" && (activeTab === "dbSetup" &&
((tech.id !== "none" && stack.database === "none") ||
(tech.id === "turso" &&
(stack.database !== "sqlite" || (stack.database !== "sqlite" ||
stack.orm === "prisma")) || stack.orm === "prisma")) ||
(activeTab === "prismaPostgres" && (tech.id === "prisma-postgres" &&
(stack.database !== "postgres" || (stack.database !== "postgres" ||
stack.orm !== "prisma")) || stack.orm !== "prisma")) ||
(tech.id === "mongodb-atlas" &&
stack.database !== "mongodb"))) ||
(activeTab === "examples" && (activeTab === "examples" &&
(((tech.id === "todo" || tech.id === "ai") && (((tech.id === "todo" || tech.id === "ai") &&
!hasWebFrontendSelected) || !hasWebFrontendSelected) ||
@@ -866,7 +889,7 @@ const StackArchitect = () => {
} }
</span> </span>
{stack.orm && stack.database !== "none" && ( {stack.orm !== "none" && stack.database !== "none" && (
<span className="inline-flex items-center rounded border border-cyan-300 bg-cyan-100 px-1.5 py-0.5 text-cyan-800 text-xs dark:border-cyan-700/30 dark:bg-cyan-900/30 dark:text-cyan-300"> <span className="inline-flex items-center rounded border border-cyan-300 bg-cyan-100 px-1.5 py-0.5 text-cyan-800 text-xs dark:border-cyan-700/30 dark:bg-cyan-900/30 dark:text-cyan-300">
{TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.icon}{" "} {TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.icon}{" "}
{TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.name} {TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.name}
@@ -882,38 +905,19 @@ const StackArchitect = () => {
</span> </span>
)} )}
{stack.turso === "true" && {stack.dbSetup !== "none" && (
stack.database === "sqlite" &&
stack.orm !== "prisma" && (
<span className="inline-flex items-center rounded border border-pink-300 bg-pink-100 px-1.5 py-0.5 text-pink-800 text-xs dark:border-pink-700/30 dark:bg-pink-900/30 dark:text-pink-300"> <span className="inline-flex items-center rounded border border-pink-300 bg-pink-100 px-1.5 py-0.5 text-pink-800 text-xs dark:border-pink-700/30 dark:bg-pink-900/30 dark:text-pink-300">
{ {
TECH_OPTIONS.turso.find((t) => t.id === stack.turso) TECH_OPTIONS.dbSetup.find((t) => t.id === stack.dbSetup)
?.icon ?.icon
}{" "} }{" "}
{ {
TECH_OPTIONS.turso.find((t) => t.id === stack.turso) TECH_OPTIONS.dbSetup.find((t) => t.id === stack.dbSetup)
?.name ?.name
} }
</span> </span>
)} )}
{stack.prismaPostgres === "true" &&
stack.database === "postgres" &&
stack.orm === "prisma" && (
<span className="inline-flex items-center rounded border border-indigo-300 bg-indigo-100 px-1.5 py-0.5 text-indigo-800 text-xs dark:border-indigo-700/30 dark:bg-indigo-900/30 dark:text-indigo-300">
{
TECH_OPTIONS.prismaPostgres.find(
(t) => t.id === stack.prismaPostgres,
)?.icon
}{" "}
{
TECH_OPTIONS.prismaPostgres.find(
(t) => t.id === stack.prismaPostgres,
)?.name
}
</span>
)}
{stack.addons.map((addonId) => { {stack.addons.map((addonId) => {
const addon = TECH_OPTIONS.addons.find( const addon = TECH_OPTIONS.addons.find(
(a) => a.id === addonId, (a) => a.id === addonId,

View File

@@ -74,6 +74,13 @@ export const TECH_OPTIONS = {
icon: "🦊", icon: "🦊",
color: "from-purple-500 to-purple-700", 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: [ database: [
{ {
@@ -91,6 +98,13 @@ export const TECH_OPTIONS = {
icon: "🐘", icon: "🐘",
color: "from-indigo-400 to-indigo-600", 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", id: "none",
name: "No Database", name: "No Database",
@@ -115,6 +129,44 @@ export const TECH_OPTIONS = {
icon: "◮", icon: "◮",
color: "from-purple-400 to-purple-600", 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: [ auth: [
{ {
@@ -133,42 +185,6 @@ export const TECH_OPTIONS = {
color: "from-red-400 to-red-600", 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: [ packageManager: [
{ {
id: "npm", id: "npm",
@@ -293,9 +309,8 @@ export const PRESET_TEMPLATES = [
backendFramework: "hono", backendFramework: "hono",
database: "sqlite", database: "sqlite",
orm: "drizzle", orm: "drizzle",
dbSetup: "none",
auth: "true", auth: "true",
turso: "false",
prismaPostgres: "false",
packageManager: "bun", packageManager: "bun",
addons: [], addons: [],
examples: [], examples: [],
@@ -314,9 +329,8 @@ export const PRESET_TEMPLATES = [
backendFramework: "hono", backendFramework: "hono",
database: "sqlite", database: "sqlite",
orm: "drizzle", orm: "drizzle",
dbSetup: "none",
auth: "true", auth: "true",
turso: "false",
prismaPostgres: "false",
packageManager: "bun", packageManager: "bun",
addons: [], addons: [],
examples: [], examples: [],
@@ -335,9 +349,8 @@ export const PRESET_TEMPLATES = [
backendFramework: "hono", backendFramework: "hono",
database: "postgres", database: "postgres",
orm: "drizzle", orm: "drizzle",
dbSetup: "none",
auth: "false", auth: "false",
turso: "false",
prismaPostgres: "false",
packageManager: "bun", packageManager: "bun",
addons: [], addons: [],
examples: [], examples: [],
@@ -356,9 +369,8 @@ export const PRESET_TEMPLATES = [
backendFramework: "hono", backendFramework: "hono",
database: "sqlite", database: "sqlite",
orm: "drizzle", orm: "drizzle",
dbSetup: "turso",
auth: "true", auth: "true",
turso: "true",
prismaPostgres: "false",
packageManager: "bun", packageManager: "bun",
addons: ["pwa", "biome", "husky", "tauri"], addons: ["pwa", "biome", "husky", "tauri"],
examples: ["todo", "ai"], examples: ["todo", "ai"],
@@ -374,10 +386,9 @@ export type StackState = {
runtime: string; runtime: string;
backendFramework: string; backendFramework: string;
database: string; database: string;
orm: string | null; orm: string;
dbSetup: string;
auth: string; auth: string;
turso: string;
prismaPostgres: string;
packageManager: string; packageManager: string;
addons: string[]; addons: string[];
examples: string[]; examples: string[];
@@ -392,9 +403,8 @@ export const DEFAULT_STACK: StackState = {
backendFramework: "hono", backendFramework: "hono",
database: "sqlite", database: "sqlite",
orm: "drizzle", orm: "drizzle",
dbSetup: "none",
auth: "true", auth: "true",
turso: "false",
prismaPostgres: "false",
packageManager: "bun", packageManager: "bun",
addons: [], addons: [],
examples: [], examples: [],