mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
add automated neon postgres database setup
This commit is contained in:
5
.changeset/tricky-turtles-teach.md
Normal file
5
.changeset/tricky-turtles-teach.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-better-t-stack": minor
|
||||
---
|
||||
|
||||
add neon postgres setup
|
||||
@@ -57,6 +57,7 @@ export async function createProject(options: ProjectConfig): Promise<string> {
|
||||
options.dbSetup === "turso",
|
||||
options.dbSetup === "prisma-postgres",
|
||||
options.dbSetup === "mongodb-atlas",
|
||||
options.dbSetup === "neon",
|
||||
);
|
||||
|
||||
await setupAuthTemplate(
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
} from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
import { setupMongoDBAtlas } from "./mongodb-atlas-setup";
|
||||
import { setupNeonPostgres } from "./neon-setup";
|
||||
import { setupPrismaPostgres } from "./prisma-postgres-setup";
|
||||
import { setupTurso } from "./turso-setup";
|
||||
|
||||
@@ -20,6 +21,7 @@ export async function setupDatabase(
|
||||
setupTursoDb: boolean,
|
||||
setupPrismaPostgresDb: boolean,
|
||||
setupMongoDBAtlasDb: boolean,
|
||||
setupNeonPostgresDb: boolean,
|
||||
): Promise<void> {
|
||||
const s = spinner();
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
@@ -60,12 +62,12 @@ export async function setupDatabase(
|
||||
|
||||
if (databaseType === "sqlite" && setupTursoDb) {
|
||||
await setupTurso(projectDir, orm === "drizzle");
|
||||
} else if (
|
||||
databaseType === "postgres" &&
|
||||
orm === "prisma" &&
|
||||
setupPrismaPostgresDb
|
||||
) {
|
||||
} else if (databaseType === "postgres") {
|
||||
if (orm === "prisma" && setupPrismaPostgresDb) {
|
||||
await setupPrismaPostgres(projectDir, packageManager);
|
||||
} else if (setupNeonPostgresDb) {
|
||||
await setupNeonPostgres(projectDir, packageManager);
|
||||
}
|
||||
} else if (databaseType === "mongodb" && setupMongoDBAtlasDb) {
|
||||
await setupMongoDBAtlas(projectDir);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,8 @@ export async function setupEnvironmentVariables(
|
||||
const specializedSetup =
|
||||
options.dbSetup === "turso" ||
|
||||
options.dbSetup === "prisma-postgres" ||
|
||||
options.dbSetup === "mongodb-atlas";
|
||||
options.dbSetup === "mongodb-atlas" ||
|
||||
options.dbSetup === "neon";
|
||||
|
||||
if (!specializedSetup) {
|
||||
if (options.database === "postgres") {
|
||||
|
||||
194
apps/cli/src/helpers/neon-setup.ts
Normal file
194
apps/cli/src/helpers/neon-setup.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import path from "node:path";
|
||||
import { cancel, isCancel, log, spinner, text } from "@clack/prompts";
|
||||
import { execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectPackageManager } from "../types";
|
||||
|
||||
type NeonConfig = {
|
||||
connectionString: string;
|
||||
projectId: string;
|
||||
dbName: string;
|
||||
roleName: string;
|
||||
};
|
||||
|
||||
function buildNeonCommand(
|
||||
packageManager: string,
|
||||
args: string[],
|
||||
): { cmd: string; cmdArgs: string[] } {
|
||||
let cmd: string;
|
||||
let cmdArgs: string[];
|
||||
|
||||
switch (packageManager) {
|
||||
case "pnpm":
|
||||
cmd = "pnpm";
|
||||
cmdArgs = ["dlx", "neonctl", ...args];
|
||||
break;
|
||||
case "bun":
|
||||
cmd = "bunx";
|
||||
cmdArgs = ["neonctl", ...args];
|
||||
break;
|
||||
default:
|
||||
cmd = "npx";
|
||||
cmdArgs = ["neonctl", ...args];
|
||||
}
|
||||
|
||||
return { cmd, cmdArgs };
|
||||
}
|
||||
|
||||
async function executeNeonCommand(
|
||||
packageManager: string,
|
||||
args: string[],
|
||||
spinnerText?: string,
|
||||
) {
|
||||
const s = spinnerText ? spinner() : null;
|
||||
try {
|
||||
const { cmd, cmdArgs } = buildNeonCommand(packageManager, args);
|
||||
|
||||
if (s) s.start(spinnerText);
|
||||
const result = await execa(cmd, cmdArgs);
|
||||
if (s) s.stop();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (s) s.stop(pc.red(`Failed: ${spinnerText}`));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function isNeonAuthenticated(packageManager: string) {
|
||||
try {
|
||||
const { stdout } = await executeNeonCommand(packageManager, [
|
||||
"projects",
|
||||
"list",
|
||||
]);
|
||||
return !stdout.includes("not authenticated") && !stdout.includes("error");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function authenticateWithNeon(packageManager: string) {
|
||||
try {
|
||||
await executeNeonCommand(
|
||||
packageManager,
|
||||
["auth"],
|
||||
"Authenticating with Neon...",
|
||||
);
|
||||
log.success("Authenticated with Neon successfully!");
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(pc.red("Failed to authenticate with Neon"));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createNeonProject(
|
||||
projectName: string,
|
||||
packageManager: string,
|
||||
): Promise<NeonConfig | null> {
|
||||
try {
|
||||
const { stdout } = await executeNeonCommand(
|
||||
packageManager,
|
||||
["projects", "create", "--name", projectName, "--output", "json"],
|
||||
`Creating Neon project "${projectName}"...`,
|
||||
);
|
||||
|
||||
const response = JSON.parse(stdout);
|
||||
|
||||
if (
|
||||
response.project &&
|
||||
response.connection_uris &&
|
||||
response.connection_uris.length > 0
|
||||
) {
|
||||
const projectId = response.project.id;
|
||||
const connectionUri = response.connection_uris[0].connection_uri;
|
||||
const params = response.connection_uris[0].connection_parameters;
|
||||
|
||||
return {
|
||||
connectionString: connectionUri,
|
||||
projectId: projectId,
|
||||
dbName: params.database,
|
||||
roleName: params.role,
|
||||
};
|
||||
}
|
||||
log.error(pc.red("Failed to extract connection information from response"));
|
||||
return null;
|
||||
} catch (error) {
|
||||
log.error(pc.red("Failed to create Neon project"));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeEnvFile(projectDir: string, config?: NeonConfig) {
|
||||
const envPath = path.join(projectDir, "apps/server", ".env");
|
||||
const envContent = config
|
||||
? `DATABASE_URL="${config.connectionString}"`
|
||||
: `DATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"`;
|
||||
|
||||
await fs.ensureDir(path.dirname(envPath));
|
||||
await fs.writeFile(envPath, envContent);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function displayManualSetupInstructions() {
|
||||
log.info(`Manual Neon PostgreSQL Setup Instructions:
|
||||
|
||||
1. Visit https://neon.tech and create an account
|
||||
2. Create a new project from the dashboard
|
||||
3. Get your connection string
|
||||
4. Add the database URL to the .env file in apps/server/.env
|
||||
|
||||
DATABASE_URL="your_connection_string"`);
|
||||
}
|
||||
|
||||
export async function setupNeonPostgres(
|
||||
projectDir: string,
|
||||
packageManager: ProjectPackageManager,
|
||||
) {
|
||||
const s = spinner();
|
||||
|
||||
try {
|
||||
const isAuthenticated = await isNeonAuthenticated(packageManager);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
log.info("Please authenticate with Neon to continue:");
|
||||
await authenticateWithNeon(packageManager);
|
||||
}
|
||||
|
||||
const suggestedProjectName = path.basename(projectDir);
|
||||
const projectName = await text({
|
||||
message: "Enter a name for your Neon project:",
|
||||
defaultValue: suggestedProjectName,
|
||||
initialValue: suggestedProjectName,
|
||||
});
|
||||
|
||||
if (isCancel(projectName)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const config = await createNeonProject(
|
||||
projectName as string,
|
||||
packageManager,
|
||||
);
|
||||
|
||||
if (!config) {
|
||||
throw new Error(
|
||||
"Failed to create project - couldn't get connection information",
|
||||
);
|
||||
}
|
||||
|
||||
await fs.ensureDir(path.join(projectDir, "apps/server"));
|
||||
await writeEnvFile(projectDir, config);
|
||||
log.success("Neon database configured successfully!");
|
||||
} catch (error) {
|
||||
s.stop(pc.red("Neon PostgreSQL setup failed"));
|
||||
if (error instanceof Error) {
|
||||
log.error(pc.red(error.message));
|
||||
}
|
||||
await writeEnvFile(projectDir);
|
||||
displayManualSetupInstructions();
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ async function main() {
|
||||
.option("--no-install", "Skip installing dependencies")
|
||||
.option(
|
||||
"--db-setup <setup>",
|
||||
"Database setup (turso, prisma-postgres, mongodb-atlas, none)",
|
||||
"Database setup (turso, neon, prisma-postgres, mongodb-atlas, none)",
|
||||
)
|
||||
.option(
|
||||
"--backend <framework>",
|
||||
@@ -169,13 +169,13 @@ function processAndValidateFlags(
|
||||
|
||||
if (options.dbSetup) {
|
||||
if (
|
||||
!["turso", "prisma-postgres", "mongodb-atlas", "none"].includes(
|
||||
!["turso", "prisma-postgres", "mongodb-atlas", "neon", "none"].includes(
|
||||
options.dbSetup,
|
||||
)
|
||||
) {
|
||||
cancel(
|
||||
pc.red(
|
||||
`Invalid database setup: ${options.dbSetup}. Must be turso, prisma-postgres, mongodb-atlas, or none.`,
|
||||
`Invalid database setup: ${options.dbSetup}. Must be turso, prisma-postgres, mongodb-atlas, neon, or none.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
@@ -235,6 +235,16 @@ function processAndValidateFlags(
|
||||
}
|
||||
config.database = "mongodb";
|
||||
config.orm = "prisma";
|
||||
} else if (options.dbSetup === "neon") {
|
||||
if (options.database && options.database !== "postgres") {
|
||||
cancel(
|
||||
pc.red(
|
||||
"Neon PostgreSQL setup requires PostgreSQL database. Cannot use --db-setup neon with a different database type.",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.database = "postgres";
|
||||
}
|
||||
} else {
|
||||
config.dbSetup = "none";
|
||||
|
||||
@@ -25,13 +25,22 @@ export async function getDBSetupChoice(
|
||||
},
|
||||
{ value: "none" as const, label: "None", hint: "Manual setup" },
|
||||
];
|
||||
} else if (databaseType === "postgres" && orm === "prisma") {
|
||||
} else if (databaseType === "postgres") {
|
||||
options = [
|
||||
{
|
||||
value: "neon" as const,
|
||||
label: "Neon Postgres",
|
||||
hint: "Serverless Postgres with branching capability",
|
||||
},
|
||||
...(orm === "prisma"
|
||||
? [
|
||||
{
|
||||
value: "prisma-postgres" as const,
|
||||
label: "Prisma Postgres",
|
||||
hint: "Instant Postgres for Global Applications",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ value: "none" as const, label: "None", hint: "Manual setup" },
|
||||
];
|
||||
} else if (databaseType === "mongodb") {
|
||||
|
||||
@@ -19,6 +19,7 @@ export type ProjectDBSetup =
|
||||
| "turso"
|
||||
| "prisma-postgres"
|
||||
| "mongodb-atlas"
|
||||
| "neon"
|
||||
| "none";
|
||||
|
||||
export interface ProjectConfig {
|
||||
|
||||
@@ -111,6 +111,10 @@ const StackArchitect = () => {
|
||||
if (stack.orm !== "prisma") {
|
||||
setStack((prev) => ({ ...prev, orm: "prisma" }));
|
||||
}
|
||||
} else if (stack.dbSetup === "neon") {
|
||||
if (stack.database !== "postgres") {
|
||||
setStack((prev) => ({ ...prev, database: "postgres" }));
|
||||
}
|
||||
}
|
||||
}, [stack.database, stack.orm, stack.dbSetup, stack.auth]);
|
||||
|
||||
@@ -150,6 +154,10 @@ const StackArchitect = () => {
|
||||
if (stack.database !== "mongodb") {
|
||||
notes.dbSetup.push("MongoDB Atlas setup requires MongoDB database.");
|
||||
}
|
||||
} else if (stack.dbSetup === "neon") {
|
||||
if (stack.database !== "postgres") {
|
||||
notes.dbSetup.push("Neon setup requires PostgreSQL database.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,7 +402,8 @@ const StackArchitect = () => {
|
||||
(updatedState.dbSetup === "prisma-postgres" &&
|
||||
techId !== "postgres") ||
|
||||
(updatedState.dbSetup === "mongodb-atlas" &&
|
||||
techId !== "mongodb")
|
||||
techId !== "mongodb") ||
|
||||
(updatedState.dbSetup === "neon" && techId !== "postgres")
|
||||
) {
|
||||
updatedState.dbSetup = "none";
|
||||
}
|
||||
@@ -446,6 +455,8 @@ const StackArchitect = () => {
|
||||
} else if (techId === "mongodb-atlas") {
|
||||
updatedState.database = "mongodb";
|
||||
updatedState.orm = "prisma";
|
||||
} else if (techId === "neon") {
|
||||
updatedState.database = "postgres";
|
||||
}
|
||||
|
||||
return updatedState;
|
||||
@@ -726,7 +737,9 @@ const StackArchitect = () => {
|
||||
(stack.database !== "postgres" ||
|
||||
stack.orm !== "prisma")) ||
|
||||
(tech.id === "mongodb-atlas" &&
|
||||
stack.database !== "mongodb"))) ||
|
||||
stack.database !== "mongodb") ||
|
||||
(tech.id === "neon" &&
|
||||
stack.database !== "postgres"))) ||
|
||||
(activeTab === "examples" &&
|
||||
(((tech.id === "todo" || tech.id === "ai") &&
|
||||
!hasWebFrontendSelected) ||
|
||||
|
||||
@@ -152,6 +152,13 @@ export const TECH_OPTIONS = {
|
||||
icon: "☁️",
|
||||
color: "from-pink-400 to-pink-600",
|
||||
},
|
||||
{
|
||||
id: "neon",
|
||||
name: "Neon Postgres",
|
||||
description: "Serverless PostgreSQL with Neon",
|
||||
icon: "⚡",
|
||||
color: "from-blue-400 to-blue-600",
|
||||
},
|
||||
{
|
||||
id: "prisma-postgres",
|
||||
name: "Prisma PostgreSQL",
|
||||
|
||||
Reference in New Issue
Block a user