From 0c26578e8e9a10cc8e6224c3d1a79ad4c3d1eb30 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Wed, 23 Jul 2025 23:35:28 +0530 Subject: [PATCH] feat(cli): add prisma create-db setup (#419) --- .changeset/clear-buses-shave.md | 6 + apps/cli/src/constants.ts | 8 +- .../database-providers/mongodb-atlas-setup.ts | 2 +- .../prisma-postgres-setup.ts | 136 ++++++++++++++---- .../database-providers/supabase-setup.ts | 9 +- .../detect-project-config.ts | 4 +- apps/cli/src/helpers/setup/db-setup.ts | 2 +- apps/cli/src/prompts/auth.ts | 2 +- apps/cli/src/prompts/database-setup.ts | 14 +- apps/cli/src/prompts/git.ts | 2 +- apps/cli/src/prompts/install.ts | 2 +- apps/cli/src/utils/command-exists.ts | 2 +- apps/cli/src/utils/docker-utils.ts | 4 +- .../server/server-base/package.json.hbs | 2 +- .../app/(home)/_components/stack-builder.tsx | 15 -- 15 files changed, 139 insertions(+), 71 deletions(-) create mode 100644 .changeset/clear-buses-shave.md diff --git a/.changeset/clear-buses-shave.md b/.changeset/clear-buses-shave.md new file mode 100644 index 0000000..7dfa7d3 --- /dev/null +++ b/.changeset/clear-buses-shave.md @@ -0,0 +1,6 @@ +--- +"create-better-t-stack": minor +--- + +feat: Add quick setup option with create-db by Prisma +feat: Make Prisma Postgres available for both Prisma and Drizzle ORMs diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index 719d60e..8c630a0 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -35,6 +35,7 @@ export const dependencyVersionMap = { "drizzle-kit": "^0.31.2", "@libsql/client": "^0.15.9", + "@neondatabase/serverless": "^1.0.1", pg: "^8.14.1", "@types/pg": "^8.11.11", @@ -43,8 +44,9 @@ export const dependencyVersionMap = { mysql2: "^3.14.0", - "@prisma/client": "^6.9.0", - prisma: "^6.9.0", + "@prisma/client": "^6.12.0", + prisma: "^6.12.0", + "@prisma/extension-accelerate": "^2.0.2", mongoose: "^8.14.0", @@ -89,8 +91,6 @@ export const dependencyVersionMap = { "@ai-sdk/svelte": "^2.1.9", "@ai-sdk/react": "^1.2.12", - "@prisma/extension-accelerate": "^1.3.0", - "@orpc/server": "^1.5.0", "@orpc/client": "^1.5.0", "@orpc/tanstack-query": "^1.5.0", diff --git a/apps/cli/src/helpers/database-providers/mongodb-atlas-setup.ts b/apps/cli/src/helpers/database-providers/mongodb-atlas-setup.ts index 3303699..17a706f 100644 --- a/apps/cli/src/helpers/database-providers/mongodb-atlas-setup.ts +++ b/apps/cli/src/helpers/database-providers/mongodb-atlas-setup.ts @@ -15,7 +15,7 @@ type MongoDBConfig = { connectionString: string; }; -async function checkAtlasCLI(): Promise { +async function checkAtlasCLI() { const s = spinner(); s.start("Checking for MongoDB Atlas CLI..."); diff --git a/apps/cli/src/helpers/database-providers/prisma-postgres-setup.ts b/apps/cli/src/helpers/database-providers/prisma-postgres-setup.ts index 55412b2..a9674cd 100644 --- a/apps/cli/src/helpers/database-providers/prisma-postgres-setup.ts +++ b/apps/cli/src/helpers/database-providers/prisma-postgres-setup.ts @@ -1,10 +1,10 @@ import path from "node:path"; -import { cancel, isCancel, log, password, spinner } from "@clack/prompts"; +import { cancel, isCancel, log, select, spinner, text } from "@clack/prompts"; import { consola } from "consola"; import { execa } from "execa"; import fs from "fs-extra"; import pc from "picocolors"; -import type { PackageManager } from "../../types"; +import type { ORM, PackageManager, ProjectConfig } from "../../types"; import { addPackageDependency } from "../../utils/add-package-deps"; import { getPackageExecutionCommand } from "../../utils/package-runner"; import { @@ -16,18 +16,77 @@ type PrismaConfig = { databaseUrl: string; }; +async function setupWithCreateDb( + serverDir: string, + packageManager: PackageManager, + orm: ORM, +) { + try { + log.info( + "Starting Prisma PostgreSQL setup. Please follow the instructions below:", + ); + + const createDbCommand = getPackageExecutionCommand( + packageManager, + "create-db@latest -i", + ); + + await execa(createDbCommand, { + cwd: serverDir, + stdio: "inherit", + shell: true, + }); + + log.info( + orm === "drizzle" + ? pc.yellow( + "Please copy the database URL from the output above and append ?sslmode=require for Drizzle.", + ) + : pc.yellow( + "Please copy the Prisma Postgres URL from the output above.", + ), + ); + + const databaseUrl = await text({ + message: + orm === "drizzle" + ? "Paste your database URL (append ?sslmode=require for Drizzle):" + : "Paste your Prisma Postgres database URL:", + validate(value) { + if (!value) return "Please enter a database URL"; + if (orm === "drizzle" && !value.includes("?sslmode=require")) { + return "Please append ?sslmode=require to your database URL when using Drizzle"; + } + }, + }); + + if (isCancel(databaseUrl)) { + cancel("Database setup cancelled"); + return null; + } + + return { + databaseUrl: databaseUrl as string, + }; + } catch (error) { + if (error instanceof Error) { + consola.error(error.message); + } + return null; + } +} + async function initPrismaDatabase( serverDir: string, packageManager: PackageManager, -): Promise { - const s = spinner(); +) { try { - s.start("Initializing Prisma PostgreSQL..."); - const prismaDir = path.join(serverDir, "prisma"); await fs.ensureDir(prismaDir); - s.stop("Prisma PostgreSQL initialized. Follow the prompts below:"); + log.info( + "Starting Prisma PostgreSQL setup. Please follow the instructions below:", + ); const prismaInitCommand = getPackageExecutionCommand( packageManager, @@ -46,7 +105,7 @@ async function initPrismaDatabase( ), ); - const databaseUrl = await password({ + const databaseUrl = await text({ message: "Paste your Prisma Postgres database URL:", validate(value) { if (!value) return "Please enter a database URL"; @@ -65,7 +124,6 @@ async function initPrismaDatabase( databaseUrl: databaseUrl as string, }; } catch (error) { - s.stop(pc.red("Prisma PostgreSQL initialization failed")); if (error instanceof Error) { consola.error(error.message); } @@ -144,32 +202,61 @@ export default prisma; } } -import type { ProjectConfig } from "../../types"; - export async function setupPrismaPostgres(config: ProjectConfig) { - const { packageManager, projectDir } = config; + const { packageManager, projectDir, orm } = config; const serverDir = path.join(projectDir, "apps/server"); - const s = spinner(); - s.start("Setting up Prisma PostgreSQL..."); try { await fs.ensureDir(serverDir); - s.stop("Prisma PostgreSQL setup ready"); + const setupOptions = [ + { + label: "Quick setup with create-db", + value: "create-db", + hint: "Fastest, automated database creation", + }, + ]; - const config = await initPrismaDatabase(serverDir, packageManager); + if (orm === "prisma") { + setupOptions.push({ + label: "Custom setup with Prisma Console", + value: "custom", + hint: "More control - use existing Prisma account", + }); + } - if (config) { - await writeEnvFile(projectDir, config); - await addPrismaAccelerateExtension(serverDir); + const setupMethod = await select({ + message: "Choose your Prisma setup method:", + options: setupOptions, + initialValue: "create-db", + }); + + if (isCancel(setupMethod)) { + cancel(pc.red("Operation cancelled")); + process.exit(0); + } + + let prismaConfig: PrismaConfig | null = null; + + if (setupMethod === "create-db") { + prismaConfig = await setupWithCreateDb(serverDir, packageManager, orm); + } else { + prismaConfig = await initPrismaDatabase(serverDir, packageManager); + } + + if (prismaConfig) { + await writeEnvFile(projectDir, prismaConfig); + if (orm === "prisma") { + await addPrismaAccelerateExtension(serverDir); + log.info( + pc.cyan( + 'NOTE: Make sure to uncomment `import "dotenv/config";` in `apps/server/src/prisma.config.ts` to load environment variables.', + ), + ); + } log.success( pc.green("Prisma PostgreSQL database configured successfully!"), ); - log.info( - pc.cyan( - 'NOTE: Make sure to uncomment `import "dotenv/config";` in `apps/server/src/prisma.config.ts` to load environment variables.', - ), - ); } else { const fallbackSpinner = spinner(); fallbackSpinner.start("Setting up fallback configuration..."); @@ -178,7 +265,6 @@ export async function setupPrismaPostgres(config: ProjectConfig) { displayManualSetupInstructions(); } } catch (error) { - s.stop(pc.red("Prisma PostgreSQL setup failed")); consola.error( pc.red( `Error during Prisma PostgreSQL setup: ${ diff --git a/apps/cli/src/helpers/database-providers/supabase-setup.ts b/apps/cli/src/helpers/database-providers/supabase-setup.ts index 8c48a16..20a6ab5 100644 --- a/apps/cli/src/helpers/database-providers/supabase-setup.ts +++ b/apps/cli/src/helpers/database-providers/supabase-setup.ts @@ -11,10 +11,7 @@ import { type EnvVariable, } from "../project-generation/env-setup"; -async function writeSupabaseEnvFile( - projectDir: string, - databaseUrl: string, -): Promise { +async function writeSupabaseEnvFile(projectDir: string, databaseUrl: string) { try { const envPath = path.join(projectDir, "apps/server", ".env"); const dbUrlToUse = @@ -54,7 +51,7 @@ function extractDbUrl(output: string): string | null { async function initializeSupabase( serverDir: string, packageManager: PackageManager, -): Promise { +) { log.info("Initializing Supabase project..."); try { const supabaseInitCommand = getPackageExecutionCommand( @@ -90,7 +87,7 @@ async function initializeSupabase( async function startSupabase( serverDir: string, packageManager: PackageManager, -): Promise { +) { log.info("Starting Supabase services (this may take a moment)..."); const supabaseStartCommand = getPackageExecutionCommand( packageManager, diff --git a/apps/cli/src/helpers/project-generation/detect-project-config.ts b/apps/cli/src/helpers/project-generation/detect-project-config.ts index f4ac423..7e490d8 100644 --- a/apps/cli/src/helpers/project-generation/detect-project-config.ts +++ b/apps/cli/src/helpers/project-generation/detect-project-config.ts @@ -33,9 +33,7 @@ export async function detectProjectConfig( } } -export async function isBetterTStackProject( - projectDir: string, -): Promise { +export async function isBetterTStackProject(projectDir: string) { try { return await fs.pathExists(path.join(projectDir, "bts.jsonc")); } catch (_error) { diff --git a/apps/cli/src/helpers/setup/db-setup.ts b/apps/cli/src/helpers/setup/db-setup.ts index 6ca567f..ee5ac27 100644 --- a/apps/cli/src/helpers/setup/db-setup.ts +++ b/apps/cli/src/helpers/setup/db-setup.ts @@ -84,7 +84,7 @@ export async function setupDatabase(config: ProjectConfig): Promise { } else if (database === "sqlite" && dbSetup === "d1") { await setupCloudflareD1(config); } else if (database === "postgres") { - if (orm === "prisma" && dbSetup === "prisma-postgres") { + if (dbSetup === "prisma-postgres") { await setupPrismaPostgres(config); } else if (dbSetup === "neon") { await setupNeonPostgres(config); diff --git a/apps/cli/src/prompts/auth.ts b/apps/cli/src/prompts/auth.ts index 166c1de..1b7696e 100644 --- a/apps/cli/src/prompts/auth.ts +++ b/apps/cli/src/prompts/auth.ts @@ -7,7 +7,7 @@ export async function getAuthChoice( auth: boolean | undefined, hasDatabase: boolean, backend?: Backend, -): Promise { +) { if (backend === "convex") { return false; } diff --git a/apps/cli/src/prompts/database-setup.ts b/apps/cli/src/prompts/database-setup.ts index 331fe05..164d275 100644 --- a/apps/cli/src/prompts/database-setup.ts +++ b/apps/cli/src/prompts/database-setup.ts @@ -56,15 +56,11 @@ export async function getDBSetupChoice( label: "Supabase", hint: "Local Supabase stack (requires Docker)", }, - ...(orm === "prisma" - ? [ - { - value: "prisma-postgres" as const, - label: "Prisma Postgres", - hint: "Instant Postgres for Global Applications", - }, - ] - : []), + { + value: "prisma-postgres" as const, + label: "Prisma Postgres", + hint: "Instant Postgres for Global Applications", + }, { value: "docker" as const, label: "Docker", diff --git a/apps/cli/src/prompts/git.ts b/apps/cli/src/prompts/git.ts index d7d180d..54eb10c 100644 --- a/apps/cli/src/prompts/git.ts +++ b/apps/cli/src/prompts/git.ts @@ -2,7 +2,7 @@ import { cancel, confirm, isCancel } from "@clack/prompts"; import pc from "picocolors"; import { DEFAULT_CONFIG } from "../constants"; -export async function getGitChoice(git?: boolean): Promise { +export async function getGitChoice(git?: boolean) { if (git !== undefined) return git; const response = await confirm({ diff --git a/apps/cli/src/prompts/install.ts b/apps/cli/src/prompts/install.ts index 2d17d1c..6ad5c5a 100644 --- a/apps/cli/src/prompts/install.ts +++ b/apps/cli/src/prompts/install.ts @@ -2,7 +2,7 @@ import { cancel, confirm, isCancel } from "@clack/prompts"; import pc from "picocolors"; import { DEFAULT_CONFIG } from "../constants"; -export async function getinstallChoice(install?: boolean): Promise { +export async function getinstallChoice(install?: boolean) { if (install !== undefined) return install; const response = await confirm({ diff --git a/apps/cli/src/utils/command-exists.ts b/apps/cli/src/utils/command-exists.ts index c363c4e..440decc 100644 --- a/apps/cli/src/utils/command-exists.ts +++ b/apps/cli/src/utils/command-exists.ts @@ -1,6 +1,6 @@ import { execa } from "execa"; -export async function commandExists(command: string): Promise { +export async function commandExists(command: string) { try { const isWindows = process.platform === "win32"; if (isWindows) { diff --git a/apps/cli/src/utils/docker-utils.ts b/apps/cli/src/utils/docker-utils.ts index 95d3169..8a14db8 100644 --- a/apps/cli/src/utils/docker-utils.ts +++ b/apps/cli/src/utils/docker-utils.ts @@ -3,11 +3,11 @@ import pc from "picocolors"; import type { Database } from "../types"; import { commandExists } from "./command-exists"; -export async function isDockerInstalled(): Promise { +export async function isDockerInstalled() { return commandExists("docker"); } -export async function isDockerRunning(): Promise { +export async function isDockerRunning() { try { const { $ } = await import("execa"); await $`docker info`; diff --git a/apps/cli/templates/backend/server/server-base/package.json.hbs b/apps/cli/templates/backend/server/server-base/package.json.hbs index 1618a78..dc8e0fd 100644 --- a/apps/cli/templates/backend/server/server-base/package.json.hbs +++ b/apps/cli/templates/backend/server/server-base/package.json.hbs @@ -4,7 +4,7 @@ "type": "module", "scripts": { "build": "tsdown", - "check-types": "tsc --noEmit", + "check-types": "tsc -b", "compile": "bun build --compile --minify --sourcemap --bytecode ./src/index.ts --outfile server" }, {{#if (eq orm 'prisma')}} diff --git a/apps/web/src/app/(home)/_components/stack-builder.tsx b/apps/web/src/app/(home)/_components/stack-builder.tsx index 1eb4f27..143ccc4 100644 --- a/apps/web/src/app/(home)/_components/stack-builder.tsx +++ b/apps/web/src/app/(home)/_components/stack-builder.tsx @@ -448,21 +448,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { "Database set to 'PostgreSQL' (required by Prisma PostgreSQL setup)", }); } - if (nextStack.orm !== "prisma") { - notes.dbSetup.notes.push("Requires Prisma ORM. It will be selected."); - notes.orm.notes.push( - "Prisma PostgreSQL setup requires Prisma ORM. It will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.orm.hasIssue = true; - nextStack.orm = "prisma"; - changed = true; - changes.push({ - category: "dbSetup", - message: - "ORM set to 'Prisma' (required by Prisma PostgreSQL setup)", - }); - } } else if (nextStack.dbSetup === "mongodb-atlas") { if (nextStack.database !== "mongodb") { notes.dbSetup.notes.push("Requires MongoDB. It will be selected.");