feat(cli): add prisma create-db setup (#419)

This commit is contained in:
Aman Varshney
2025-07-23 23:35:28 +05:30
committed by GitHub
parent 2543c5317b
commit 0c26578e8e
15 changed files with 139 additions and 71 deletions

View File

@@ -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

View File

@@ -35,6 +35,7 @@ export const dependencyVersionMap = {
"drizzle-kit": "^0.31.2", "drizzle-kit": "^0.31.2",
"@libsql/client": "^0.15.9", "@libsql/client": "^0.15.9",
"@neondatabase/serverless": "^1.0.1", "@neondatabase/serverless": "^1.0.1",
pg: "^8.14.1", pg: "^8.14.1",
"@types/pg": "^8.11.11", "@types/pg": "^8.11.11",
@@ -43,8 +44,9 @@ export const dependencyVersionMap = {
mysql2: "^3.14.0", mysql2: "^3.14.0",
"@prisma/client": "^6.9.0", "@prisma/client": "^6.12.0",
prisma: "^6.9.0", prisma: "^6.12.0",
"@prisma/extension-accelerate": "^2.0.2",
mongoose: "^8.14.0", mongoose: "^8.14.0",
@@ -89,8 +91,6 @@ export const dependencyVersionMap = {
"@ai-sdk/svelte": "^2.1.9", "@ai-sdk/svelte": "^2.1.9",
"@ai-sdk/react": "^1.2.12", "@ai-sdk/react": "^1.2.12",
"@prisma/extension-accelerate": "^1.3.0",
"@orpc/server": "^1.5.0", "@orpc/server": "^1.5.0",
"@orpc/client": "^1.5.0", "@orpc/client": "^1.5.0",
"@orpc/tanstack-query": "^1.5.0", "@orpc/tanstack-query": "^1.5.0",

View File

@@ -15,7 +15,7 @@ type MongoDBConfig = {
connectionString: string; connectionString: string;
}; };
async function checkAtlasCLI(): Promise<boolean> { async function checkAtlasCLI() {
const s = spinner(); const s = spinner();
s.start("Checking for MongoDB Atlas CLI..."); s.start("Checking for MongoDB Atlas CLI...");

View File

@@ -1,10 +1,10 @@
import path from "node:path"; 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 { consola } from "consola";
import { execa } from "execa"; import { execa } from "execa";
import fs from "fs-extra"; import fs from "fs-extra";
import pc from "picocolors"; import pc from "picocolors";
import type { PackageManager } from "../../types"; import type { ORM, PackageManager, ProjectConfig } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps"; import { addPackageDependency } from "../../utils/add-package-deps";
import { getPackageExecutionCommand } from "../../utils/package-runner"; import { getPackageExecutionCommand } from "../../utils/package-runner";
import { import {
@@ -16,18 +16,77 @@ type PrismaConfig = {
databaseUrl: string; 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( async function initPrismaDatabase(
serverDir: string, serverDir: string,
packageManager: PackageManager, packageManager: PackageManager,
): Promise<PrismaConfig | null> { ) {
const s = spinner();
try { try {
s.start("Initializing Prisma PostgreSQL...");
const prismaDir = path.join(serverDir, "prisma"); const prismaDir = path.join(serverDir, "prisma");
await fs.ensureDir(prismaDir); 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( const prismaInitCommand = getPackageExecutionCommand(
packageManager, packageManager,
@@ -46,7 +105,7 @@ async function initPrismaDatabase(
), ),
); );
const databaseUrl = await password({ const databaseUrl = await text({
message: "Paste your Prisma Postgres database URL:", message: "Paste your Prisma Postgres database URL:",
validate(value) { validate(value) {
if (!value) return "Please enter a database URL"; if (!value) return "Please enter a database URL";
@@ -65,7 +124,6 @@ async function initPrismaDatabase(
databaseUrl: databaseUrl as string, databaseUrl: databaseUrl as string,
}; };
} catch (error) { } catch (error) {
s.stop(pc.red("Prisma PostgreSQL initialization failed"));
if (error instanceof Error) { if (error instanceof Error) {
consola.error(error.message); consola.error(error.message);
} }
@@ -144,32 +202,61 @@ export default prisma;
} }
} }
import type { ProjectConfig } from "../../types";
export async function setupPrismaPostgres(config: ProjectConfig) { export async function setupPrismaPostgres(config: ProjectConfig) {
const { packageManager, projectDir } = config; const { packageManager, projectDir, orm } = config;
const serverDir = path.join(projectDir, "apps/server"); const serverDir = path.join(projectDir, "apps/server");
const s = spinner();
s.start("Setting up Prisma PostgreSQL...");
try { try {
await fs.ensureDir(serverDir); 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) { const setupMethod = await select({
await writeEnvFile(projectDir, config); message: "Choose your Prisma setup method:",
await addPrismaAccelerateExtension(serverDir); 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( log.success(
pc.green("Prisma PostgreSQL database configured successfully!"), 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 { } else {
const fallbackSpinner = spinner(); const fallbackSpinner = spinner();
fallbackSpinner.start("Setting up fallback configuration..."); fallbackSpinner.start("Setting up fallback configuration...");
@@ -178,7 +265,6 @@ export async function setupPrismaPostgres(config: ProjectConfig) {
displayManualSetupInstructions(); displayManualSetupInstructions();
} }
} catch (error) { } catch (error) {
s.stop(pc.red("Prisma PostgreSQL setup failed"));
consola.error( consola.error(
pc.red( pc.red(
`Error during Prisma PostgreSQL setup: ${ `Error during Prisma PostgreSQL setup: ${

View File

@@ -11,10 +11,7 @@ import {
type EnvVariable, type EnvVariable,
} from "../project-generation/env-setup"; } from "../project-generation/env-setup";
async function writeSupabaseEnvFile( async function writeSupabaseEnvFile(projectDir: string, databaseUrl: string) {
projectDir: string,
databaseUrl: string,
): Promise<boolean> {
try { try {
const envPath = path.join(projectDir, "apps/server", ".env"); const envPath = path.join(projectDir, "apps/server", ".env");
const dbUrlToUse = const dbUrlToUse =
@@ -54,7 +51,7 @@ function extractDbUrl(output: string): string | null {
async function initializeSupabase( async function initializeSupabase(
serverDir: string, serverDir: string,
packageManager: PackageManager, packageManager: PackageManager,
): Promise<boolean> { ) {
log.info("Initializing Supabase project..."); log.info("Initializing Supabase project...");
try { try {
const supabaseInitCommand = getPackageExecutionCommand( const supabaseInitCommand = getPackageExecutionCommand(
@@ -90,7 +87,7 @@ async function initializeSupabase(
async function startSupabase( async function startSupabase(
serverDir: string, serverDir: string,
packageManager: PackageManager, packageManager: PackageManager,
): Promise<string | null> { ) {
log.info("Starting Supabase services (this may take a moment)..."); log.info("Starting Supabase services (this may take a moment)...");
const supabaseStartCommand = getPackageExecutionCommand( const supabaseStartCommand = getPackageExecutionCommand(
packageManager, packageManager,

View File

@@ -33,9 +33,7 @@ export async function detectProjectConfig(
} }
} }
export async function isBetterTStackProject( export async function isBetterTStackProject(projectDir: string) {
projectDir: string,
): Promise<boolean> {
try { try {
return await fs.pathExists(path.join(projectDir, "bts.jsonc")); return await fs.pathExists(path.join(projectDir, "bts.jsonc"));
} catch (_error) { } catch (_error) {

View File

@@ -84,7 +84,7 @@ export async function setupDatabase(config: ProjectConfig): Promise<void> {
} else if (database === "sqlite" && dbSetup === "d1") { } else if (database === "sqlite" && dbSetup === "d1") {
await setupCloudflareD1(config); await setupCloudflareD1(config);
} else if (database === "postgres") { } else if (database === "postgres") {
if (orm === "prisma" && dbSetup === "prisma-postgres") { if (dbSetup === "prisma-postgres") {
await setupPrismaPostgres(config); await setupPrismaPostgres(config);
} else if (dbSetup === "neon") { } else if (dbSetup === "neon") {
await setupNeonPostgres(config); await setupNeonPostgres(config);

View File

@@ -7,7 +7,7 @@ export async function getAuthChoice(
auth: boolean | undefined, auth: boolean | undefined,
hasDatabase: boolean, hasDatabase: boolean,
backend?: Backend, backend?: Backend,
): Promise<boolean> { ) {
if (backend === "convex") { if (backend === "convex") {
return false; return false;
} }

View File

@@ -56,15 +56,11 @@ export async function getDBSetupChoice(
label: "Supabase", label: "Supabase",
hint: "Local Supabase stack (requires Docker)", hint: "Local Supabase stack (requires Docker)",
}, },
...(orm === "prisma" {
? [ value: "prisma-postgres" as const,
{ label: "Prisma Postgres",
value: "prisma-postgres" as const, hint: "Instant Postgres for Global Applications",
label: "Prisma Postgres", },
hint: "Instant Postgres for Global Applications",
},
]
: []),
{ {
value: "docker" as const, value: "docker" as const,
label: "Docker", label: "Docker",

View File

@@ -2,7 +2,7 @@ import { cancel, confirm, isCancel } from "@clack/prompts";
import pc from "picocolors"; import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants"; import { DEFAULT_CONFIG } from "../constants";
export async function getGitChoice(git?: boolean): Promise<boolean> { export async function getGitChoice(git?: boolean) {
if (git !== undefined) return git; if (git !== undefined) return git;
const response = await confirm({ const response = await confirm({

View File

@@ -2,7 +2,7 @@ import { cancel, confirm, isCancel } from "@clack/prompts";
import pc from "picocolors"; import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants"; import { DEFAULT_CONFIG } from "../constants";
export async function getinstallChoice(install?: boolean): Promise<boolean> { export async function getinstallChoice(install?: boolean) {
if (install !== undefined) return install; if (install !== undefined) return install;
const response = await confirm({ const response = await confirm({

View File

@@ -1,6 +1,6 @@
import { execa } from "execa"; import { execa } from "execa";
export async function commandExists(command: string): Promise<boolean> { export async function commandExists(command: string) {
try { try {
const isWindows = process.platform === "win32"; const isWindows = process.platform === "win32";
if (isWindows) { if (isWindows) {

View File

@@ -3,11 +3,11 @@ import pc from "picocolors";
import type { Database } from "../types"; import type { Database } from "../types";
import { commandExists } from "./command-exists"; import { commandExists } from "./command-exists";
export async function isDockerInstalled(): Promise<boolean> { export async function isDockerInstalled() {
return commandExists("docker"); return commandExists("docker");
} }
export async function isDockerRunning(): Promise<boolean> { export async function isDockerRunning() {
try { try {
const { $ } = await import("execa"); const { $ } = await import("execa");
await $`docker info`; await $`docker info`;

View File

@@ -4,7 +4,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "tsdown", "build": "tsdown",
"check-types": "tsc --noEmit", "check-types": "tsc -b",
"compile": "bun build --compile --minify --sourcemap --bytecode ./src/index.ts --outfile server" "compile": "bun build --compile --minify --sourcemap --bytecode ./src/index.ts --outfile server"
}, },
{{#if (eq orm 'prisma')}} {{#if (eq orm 'prisma')}}

View File

@@ -448,21 +448,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
"Database set to 'PostgreSQL' (required by Prisma PostgreSQL setup)", "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") { } else if (nextStack.dbSetup === "mongodb-atlas") {
if (nextStack.database !== "mongodb") { if (nextStack.database !== "mongodb") {
notes.dbSetup.notes.push("Requires MongoDB. It will be selected."); notes.dbSetup.notes.push("Requires MongoDB. It will be selected.");