feat(cli): prisma + workers, prisma + turso, planetscale (postgres/mysql) support (#567)

This commit is contained in:
Aman Varshney
2025-09-08 12:15:26 +05:30
committed by GitHub
parent 33344d91be
commit cd5d0f0aeb
66 changed files with 1486 additions and 729 deletions

View File

@@ -50,6 +50,7 @@ export const dependencyVersionMap = {
"drizzle-orm": "^0.44.2",
"drizzle-kit": "^0.31.2",
"@planetscale/database": "^1.19.0",
"@libsql/client": "^0.15.9",
@@ -63,7 +64,11 @@ export const dependencyVersionMap = {
"@prisma/client": "^6.15.0",
prisma: "^6.15.0",
"@prisma/adapter-d1": "^6.15.0",
"@prisma/extension-accelerate": "^2.0.2",
"@prisma/adapter-libsql": "^6.15.0",
"@prisma/adapter-planetscale": "^6.15.0",
mongoose: "^8.14.0",
@@ -141,12 +146,12 @@ export const dependencyVersionMap = {
wrangler: "^4.23.0",
"@cloudflare/vite-plugin": "^1.9.0",
"@opennextjs/cloudflare": "^1.3.0",
"@opennextjs/cloudflare": "^1.6.5",
"nitro-cloudflare-dev": "^0.2.2",
"@sveltejs/adapter-cloudflare": "^7.2.1",
"@cloudflare/workers-types": "^4.20250822.0",
alchemy: "^0.63.0",
alchemy: "^0.65.0",
// temporary workaround for alchemy + tanstack start
nitropack: "^2.12.4",

View File

@@ -3,7 +3,6 @@ import fs from "fs-extra";
import type { ProjectConfig } from "../../types";
import { writeBtsConfig } from "../../utils/bts-config";
import { exitWithError } from "../../utils/errors";
import { formatProjectWithBiome } from "../../utils/format-with-biome";
import { setupAddons } from "../addons/addons-setup";
import { setupExamples } from "../addons/examples-setup";
import { setupApi } from "../core/api-setup";
@@ -86,8 +85,6 @@ export async function createProject(options: ProjectConfig) {
await writeBtsConfig(options);
await formatProjectWithBiome(projectDir);
if (isConvex) {
await runConvexCodegen(projectDir, options.packageManager);
}

View File

@@ -9,6 +9,7 @@ import { setupCloudflareD1 } from "../database-providers/d1-setup";
import { setupDockerCompose } from "../database-providers/docker-compose-setup";
import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup";
import { setupNeonPostgres } from "../database-providers/neon-setup";
import { setupPlanetScale } from "../database-providers/planetscale-setup";
import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup";
import { setupSupabase } from "../database-providers/supabase-setup";
import { setupTurso } from "../database-providers/turso-setup";
@@ -36,11 +37,29 @@ export async function setupDatabase(config: ProjectConfig) {
try {
if (orm === "prisma") {
await addPackageDependency({
dependencies: ["@prisma/client"],
devDependencies: ["prisma"],
projectDir: serverDir,
});
if (database === "mysql" && dbSetup === "planetscale") {
await addPackageDependency({
dependencies: [
"@prisma/client",
"@prisma/adapter-planetscale",
"@planetscale/database",
],
devDependencies: ["prisma"],
projectDir: serverDir,
});
} else if (database === "sqlite" && dbSetup === "turso") {
await addPackageDependency({
dependencies: ["@prisma/client", "@prisma/adapter-libsql"],
devDependencies: ["prisma"],
projectDir: serverDir,
});
} else {
await addPackageDependency({
dependencies: ["@prisma/client"],
devDependencies: ["prisma"],
projectDir: serverDir,
});
}
} else if (orm === "drizzle") {
if (database === "sqlite") {
await addPackageDependency({
@@ -55,6 +74,12 @@ export async function setupDatabase(config: ProjectConfig) {
devDependencies: ["drizzle-kit", "@types/ws"],
projectDir: serverDir,
});
} else if (dbSetup === "planetscale") {
await addPackageDependency({
dependencies: ["drizzle-orm", "pg"],
devDependencies: ["drizzle-kit", "@types/pg"],
projectDir: serverDir,
});
} else {
await addPackageDependency({
dependencies: ["drizzle-orm", "pg"],
@@ -63,11 +88,19 @@ export async function setupDatabase(config: ProjectConfig) {
});
}
} else if (database === "mysql") {
await addPackageDependency({
dependencies: ["drizzle-orm", "mysql2"],
devDependencies: ["drizzle-kit"],
projectDir: serverDir,
});
if (dbSetup === "planetscale") {
await addPackageDependency({
dependencies: ["drizzle-orm", "@planetscale/database"],
devDependencies: ["drizzle-kit"],
projectDir: serverDir,
});
} else {
await addPackageDependency({
dependencies: ["drizzle-orm", "mysql2"],
devDependencies: ["drizzle-kit"],
projectDir: serverDir,
});
}
}
} else if (orm === "mongoose") {
await addPackageDependency({
@@ -88,9 +121,15 @@ export async function setupDatabase(config: ProjectConfig) {
await setupPrismaPostgres(config);
} else if (dbSetup === "neon") {
await setupNeonPostgres(config);
} else if (dbSetup === "planetscale") {
await setupPlanetScale(config);
} else if (dbSetup === "supabase") {
await setupSupabase(config);
}
} else if (database === "mysql") {
if (dbSetup === "planetscale") {
await setupPlanetScale(config);
}
} else if (database === "mongodb" && dbSetup === "mongodb-atlas") {
await setupMongoDBAtlas(config);
}

View File

@@ -232,16 +232,8 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
}
let databaseUrl: string | null = null;
const specializedSetup =
dbSetup === "turso" ||
dbSetup === "prisma-postgres" ||
dbSetup === "mongodb-atlas" ||
dbSetup === "neon" ||
dbSetup === "supabase" ||
dbSetup === "d1" ||
dbSetup === "docker";
if (database !== "none" && !specializedSetup) {
if (database !== "none" && dbSetup === "none") {
switch (database) {
case "postgres":
databaseUrl = "postgresql://postgres:password@localhost:5432/postgres";
@@ -281,7 +273,7 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
{
key: "DATABASE_URL",
value: databaseUrl,
condition: database !== "none" && !specializedSetup,
condition: database !== "none" && dbSetup === "none",
},
{
key: "GOOGLE_GENERATIVE_AI_API_KEY",

View File

@@ -287,22 +287,40 @@ async function getDatabaseInstructions(
}
if (dbSetup === "d1" && serverDeploy === "alchemy") {
instructions.push(
`${pc.yellow(
"NOTE:",
)} D1 migrations are automatically handled by Alchemy`,
);
}
if (orm === "prisma") {
if (dbSetup === "turso") {
if (orm === "drizzle") {
instructions.push(
`${pc.yellow(
"NOTE:",
)} Turso support with Prisma is in Early Access and requires\n additional setup. Learn more at:\n https://www.prisma.io/docs/orm/overview/databases/turso`,
)} D1 migrations are automatically handled by Alchemy`,
);
} else if (orm === "prisma") {
instructions.push(
`${pc.cyan("•")} Generate migrations: ${`${runCmd} db:generate`}`,
);
instructions.push(
`${pc.cyan("•")} Apply migrations: ${`${runCmd} db:migrate`}`,
);
}
}
if (dbSetup === "planetscale") {
if (database === "mysql" && orm === "drizzle") {
instructions.push(
`${pc.yellow(
"NOTE:",
)} Enable foreign key constraints in PlanetScale database settings`,
);
}
if (database === "mysql" && orm === "prisma") {
instructions.push(
`${pc.yellow(
"NOTE:",
)} How to handle Prisma migrations with PlanetScale:\n https://github.com/prisma/prisma/issues/7292`,
);
}
}
if (orm === "prisma") {
if (database === "mongodb" && dbSetup === "docker") {
instructions.push(
`${pc.yellow(

View File

@@ -76,9 +76,7 @@ async function updateRootPackageJson(
}
if (options.orm === "prisma") {
scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {
scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`;
}
scripts["db:migrate"] = `turbo -F ${backendPackageName} db:migrate`;
} else if (options.orm === "drizzle") {
scripts["db:generate"] = `turbo -F ${backendPackageName} db:generate`;
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {
@@ -110,10 +108,8 @@ async function updateRootPackageJson(
if (options.orm === "prisma") {
scripts["db:generate"] =
`pnpm --filter ${backendPackageName} db:generate`;
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {
scripts["db:migrate"] =
`pnpm --filter ${backendPackageName} db:migrate`;
}
scripts["db:migrate"] =
`pnpm --filter ${backendPackageName} db:migrate`;
} else if (options.orm === "drizzle") {
scripts["db:generate"] =
`pnpm --filter ${backendPackageName} db:generate`;
@@ -149,10 +145,8 @@ async function updateRootPackageJson(
if (options.orm === "prisma") {
scripts["db:generate"] =
`npm run db:generate --workspace ${backendPackageName}`;
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {
scripts["db:migrate"] =
`npm run db:migrate --workspace ${backendPackageName}`;
}
scripts["db:migrate"] =
`npm run db:migrate --workspace ${backendPackageName}`;
} else if (options.orm === "drizzle") {
scripts["db:generate"] =
`npm run db:generate --workspace ${backendPackageName}`;
@@ -189,10 +183,8 @@ async function updateRootPackageJson(
if (options.orm === "prisma") {
scripts["db:generate"] =
`bun run --filter ${backendPackageName} db:generate`;
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {
scripts["db:migrate"] =
`bun run --filter ${backendPackageName} db:migrate`;
}
scripts["db:migrate"] =
`bun run --filter ${backendPackageName} db:migrate`;
} else if (options.orm === "drizzle") {
scripts["db:generate"] =
`bun run --filter ${backendPackageName} db:generate`;
@@ -278,9 +270,7 @@ async function updateServerPackageJson(
scripts["db:studio"] = "prisma studio";
}
scripts["db:generate"] = "prisma generate";
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {
scripts["db:migrate"] = "prisma migrate dev";
}
scripts["db:migrate"] = "prisma migrate dev";
} else if (options.orm === "drizzle") {
scripts["db:push"] = "drizzle-kit push";
if (!(options.dbSetup === "d1" && options.serverDeploy === "alchemy")) {

View File

@@ -1,9 +1,10 @@
import path from "node:path";
import type { ProjectConfig } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps";
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
export async function setupCloudflareD1(config: ProjectConfig) {
const { projectDir, serverDeploy } = config;
const { projectDir, serverDeploy, orm } = config;
if (serverDeploy === "wrangler") {
const envPath = path.join(projectDir, "apps/server", ".env");
@@ -30,4 +31,28 @@ export async function setupCloudflareD1(config: ProjectConfig) {
await addEnvVariablesToFile(envPath, variables);
} catch (_err) {}
}
if (
(serverDeploy === "wrangler" || serverDeploy === "alchemy") &&
orm === "prisma"
) {
const envPath = path.join(projectDir, "apps/server", ".env");
const variables: EnvVariable[] = [
{
key: "DATABASE_URL",
value: "file:./local.db",
condition: true,
},
];
try {
await addEnvVariablesToFile(envPath, variables);
} catch (_err) {}
const serverDir = path.join(projectDir, "apps/server");
await addPackageDependency({
dependencies: ["@prisma/adapter-d1"],
projectDir: serverDir,
});
}
}

View File

@@ -1,11 +1,12 @@
import path from "node:path";
import { cancel, isCancel, log, spinner, text } 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 { ProjectConfig } from "../../types";
import { commandExists } from "../../utils/command-exists";
import { exitCancelled } from "../../utils/errors";
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
type MongoDBConfig = {
@@ -130,6 +131,32 @@ export async function setupMongoDBAtlas(config: ProjectConfig) {
try {
await fs.ensureDir(serverDir);
const mode = await select({
message: "MongoDB Atlas setup: choose mode",
options: [
{
label: "Automatic",
value: "auto",
hint: "Automated setup with provider CLI, sets .env",
},
{
label: "Manual",
value: "manual",
hint: "Manual setup, add env vars yourself",
},
],
initialValue: "auto",
});
if (isCancel(mode)) return exitCancelled("Operation cancelled");
if (mode === "manual") {
mainSpinner.stop("MongoDB Atlas manual setup selected");
await writeEnvFile(projectDir);
displayManualSetupInstructions();
return;
}
mainSpinner.stop("MongoDB Atlas setup ready");
const config = await initMongoDBAtlas(serverDir);

View File

@@ -158,6 +158,31 @@ export async function setupNeonPostgres(config: ProjectConfig) {
const { packageManager, projectDir } = config;
try {
const mode = await select({
message: "Neon setup: choose mode",
options: [
{
label: "Automatic",
value: "auto",
hint: "Automated setup with provider CLI, sets .env",
},
{
label: "Manual",
value: "manual",
hint: "Manual setup, add env vars yourself",
},
],
initialValue: "auto",
});
if (isCancel(mode)) return exitCancelled("Operation cancelled");
if (mode === "manual") {
await writeEnvFile(projectDir);
displayManualSetupInstructions();
return;
}
const setupMethod = await select({
message: "Choose your Neon setup method:",
options: [

View File

@@ -0,0 +1,79 @@
import path from "node:path";
import fs from "fs-extra";
import type { ProjectConfig } from "../../types";
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
export async function setupPlanetScale(config: ProjectConfig) {
const { projectDir, database, orm } = config;
const envPath = path.join(projectDir, "apps/server", ".env");
if (database === "mysql" && orm === "drizzle") {
const variables: EnvVariable[] = [
{
key: "DATABASE_URL",
value:
'mysql://username:password@host/database?ssl={"rejectUnauthorized":true}',
condition: true,
},
{
key: "DATABASE_HOST",
value: "",
condition: true,
},
{
key: "DATABASE_USERNAME",
value: "",
condition: true,
},
{
key: "DATABASE_PASSWORD",
value: "",
condition: true,
},
];
await fs.ensureDir(path.join(projectDir, "apps/server"));
await addEnvVariablesToFile(envPath, variables);
}
if (database === "postgres" && orm === "prisma") {
const variables: EnvVariable[] = [
{
key: "DATABASE_URL",
value: "postgresql://username:password@host/database?sslaccept=strict",
condition: true,
},
];
await fs.ensureDir(path.join(projectDir, "apps/server"));
await addEnvVariablesToFile(envPath, variables);
}
if (database === "postgres" && orm === "drizzle") {
const variables: EnvVariable[] = [
{
key: "DATABASE_URL",
value:
"postgresql://username:password@host/database?sslmode=verify-full",
condition: true,
},
];
await fs.ensureDir(path.join(projectDir, "apps/server"));
await addEnvVariablesToFile(envPath, variables);
}
if (database === "mysql" && orm === "prisma") {
const variables: EnvVariable[] = [
{
key: "DATABASE_URL",
value: "mysql://username:password@host/database?sslaccept=strict",
condition: true,
},
];
await fs.ensureDir(path.join(projectDir, "apps/server"));
await addEnvVariablesToFile(envPath, variables);
}
}

View File

@@ -245,6 +245,31 @@ export async function setupPrismaPostgres(config: ProjectConfig) {
try {
await fs.ensureDir(serverDir);
const mode = await select({
message: "Prisma Postgres setup: choose mode",
options: [
{
label: "Automatic",
value: "auto",
hint: "Automated setup with provider CLI, sets .env",
},
{
label: "Manual",
value: "manual",
hint: "Manual setup, add env vars yourself",
},
],
initialValue: "auto",
});
if (isCancel(mode)) return exitCancelled("Operation cancelled");
if (mode === "manual") {
await writeEnvFile(projectDir);
displayManualSetupInstructions();
return;
}
const setupOptions = [
{
label: "Quick setup with create-db",

View File

@@ -1,10 +1,11 @@
import path from "node:path";
import { log } from "@clack/prompts";
import { isCancel, log, select } from "@clack/prompts";
import { consola } from "consola";
import { type ExecaError, execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import type { PackageManager, ProjectConfig } from "../../types";
import { exitCancelled } from "../../utils/errors";
import { getPackageExecutionCommand } from "../../utils/package-runner";
import { addEnvVariablesToFile, type EnvVariable } from "../core/env-setup";
@@ -159,6 +160,31 @@ export async function setupSupabase(config: ProjectConfig) {
try {
await fs.ensureDir(serverDir);
const mode = await select({
message: "Supabase setup: choose mode",
options: [
{
label: "Automatic",
value: "auto",
hint: "Automated setup with provider CLI, sets .env",
},
{
label: "Manual",
value: "manual",
hint: "Manual setup, add env vars yourself",
},
],
initialValue: "auto",
});
if (isCancel(mode)) return exitCancelled("Operation cancelled");
if (mode === "manual") {
displayManualSupabaseInstructions();
await writeSupabaseEnvFile(projectDir, "");
return;
}
const initialized = await initializeSupabase(serverDir, packageManager);
if (!initialized) {
displayManualSupabaseInstructions();

View File

@@ -197,23 +197,49 @@ export async function setupTurso(config: ProjectConfig) {
const { orm, projectDir } = config;
const _isDrizzle = orm === "drizzle";
const setupSpinner = spinner();
setupSpinner.start("Checking Turso CLI availability...");
try {
const mode = await select({
message: "Turso setup: choose mode",
options: [
{
label: "Automatic",
value: "auto",
hint: "Automated setup with provider CLI, sets .env",
},
{
label: "Manual",
value: "manual",
hint: "Manual setup, add env vars yourself",
},
],
initialValue: "auto",
});
if (isCancel(mode)) return exitCancelled("Operation cancelled");
if (mode === "manual") {
await writeEnvFile(projectDir);
displayManualSetupInstructions();
return;
}
setupSpinner.start("Checking Turso CLI availability...");
const platform = os.platform();
const isMac = platform === "darwin";
const _isLinux = platform === "linux";
const isWindows = platform === "win32";
if (isWindows) {
setupSpinner.stop(pc.yellow("Turso setup not supported on Windows"));
if (setupSpinner)
setupSpinner.stop(pc.yellow("Turso setup not supported on Windows"));
log.warn(pc.yellow("Automatic Turso setup is not supported on Windows."));
await writeEnvFile(projectDir);
displayManualSetupInstructions();
return;
}
setupSpinner.stop("Turso CLI availability checked");
if (setupSpinner) setupSpinner.stop("Turso CLI availability checked");
const isCliInstalled = await isTursoInstalled();
@@ -273,7 +299,8 @@ export async function setupTurso(config: ProjectConfig) {
log.success("Turso database setup completed successfully!");
} catch (error) {
setupSpinner.stop(pc.red("Turso CLI availability check failed"));
if (setupSpinner)
setupSpinner.stop(pc.red("Turso CLI availability check failed"));
consola.error(
pc.red(
`Error during Turso setup: ${

View File

@@ -12,7 +12,8 @@ export async function setupNextAlchemyDeploy(
if (!(await fs.pathExists(webAppDir))) return;
await addPackageDependency({
devDependencies: ["alchemy", "dotenv"],
dependencies: ["@opennextjs/cloudflare"],
devDependencies: ["alchemy", "dotenv", "wrangler"],
projectDir: webAppDir,
});
@@ -29,4 +30,22 @@ export async function setupNextAlchemyDeploy(
}
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
}
const openNextConfigPath = path.join(webAppDir, "open-next.config.ts");
const openNextConfigContent = `import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig({});
`;
await fs.writeFile(openNextConfigPath, openNextConfigContent);
const gitignorePath = path.join(webAppDir, ".gitignore");
if (await fs.pathExists(gitignorePath)) {
const gitignoreContent = await fs.readFile(gitignorePath, "utf-8");
if (!gitignoreContent.includes("wrangler.jsonc")) {
await fs.appendFile(gitignorePath, "\nwrangler.jsonc\n");
}
} else {
await fs.writeFile(gitignorePath, "wrangler.jsonc\n");
}
}

View File

@@ -5,6 +5,7 @@ import fs from "fs-extra";
import pc from "picocolors";
import type { PackageManager, ProjectConfig } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps";
import { getPackageExecutionCommand } from "../../utils/package-runner";
export async function setupServerDeploy(config: ProjectConfig) {
const { serverDeploy, webDeploy, projectDir } = config;
@@ -64,8 +65,11 @@ async function generateCloudflareWorkerTypes({
const s = spinner();
try {
s.start("Generating Cloudflare Workers types...");
const runCmd = packageManager === "npm" ? "npm" : packageManager;
await execa(runCmd, ["run", "cf-typegen"], { cwd: serverDir });
const runCmd = getPackageExecutionCommand(
packageManager,
"wrangler types --env-interface CloudflareBindings",
);
await execa(runCmd, { cwd: serverDir, shell: true });
s.stop("Cloudflare Workers types generated successfully!");
} catch {
s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));

View File

@@ -5,7 +5,7 @@ import { exitCancelled } from "../utils/errors";
export async function getDBSetupChoice(
databaseType: string,
dbSetup: DatabaseSetup | undefined,
orm?: ORM,
_orm?: ORM,
backend?: Backend,
runtime?: Runtime,
): Promise<DatabaseSetup> {
@@ -19,10 +19,6 @@ export async function getDBSetupChoice(
return "none";
}
if (databaseType === "sqlite" && orm === "prisma") {
return "none";
}
let options: Array<{ value: DatabaseSetup; label: string; hint: string }> =
[];
@@ -51,6 +47,11 @@ export async function getDBSetupChoice(
label: "Neon Postgres",
hint: "Serverless Postgres with branching capability",
},
{
value: "planetscale" as const,
label: "PlanetScale",
hint: "Serverless MySQL platform with branching (Postgres compatible)",
},
{
value: "supabase" as const,
label: "Supabase",
@@ -70,6 +71,11 @@ export async function getDBSetupChoice(
];
} else if (databaseType === "mysql") {
options = [
{
value: "planetscale" as const,
label: "PlanetScale",
hint: "Serverless MySQL platform with branching",
},
{
value: "docker" as const,
label: "Docker",

View File

@@ -35,10 +35,6 @@ export async function getORMChoice(
if (!hasDatabase) return "none";
if (orm !== undefined) return orm;
if (runtime === "workers") {
return "drizzle";
}
const options = [
...(database === "mongodb"
? [ormOptions.prisma, ormOptions.mongoose]
@@ -48,7 +44,12 @@ export async function getORMChoice(
const response = await select<ORM>({
message: "Select ORM",
options,
initialValue: database === "mongodb" ? "prisma" : DEFAULT_CONFIG.orm,
initialValue:
database === "mongodb"
? "prisma"
: runtime === "workers"
? "drizzle"
: DEFAULT_CONFIG.orm,
});
if (isCancel(response)) return exitCancelled("Operation cancelled");

View File

@@ -47,10 +47,7 @@ export async function getDeploymentChoice(
return "none";
}
const hasIncompatibleFrontend = frontend.some((f) => f === "next");
const availableDeployments = hasIncompatibleFrontend
? ["wrangler", "none"]
: ["wrangler", "alchemy", "none"];
const availableDeployments = ["wrangler", "alchemy", "none"];
const options: DeploymentOption[] = availableDeployments.map((deploy) => {
const { label, hint } = getDeploymentDisplay(deploy as WebDeploy);
@@ -64,9 +61,7 @@ export async function getDeploymentChoice(
const response = await select<WebDeploy>({
message: "Select web deployment",
options,
initialValue: hasIncompatibleFrontend
? "wrangler"
: DEFAULT_CONFIG.webDeploy,
initialValue: DEFAULT_CONFIG.webDeploy,
});
if (isCancel(response)) return exitCancelled("Operation cancelled");
@@ -82,8 +77,6 @@ export async function getDeploymentToAdd(
return "none";
}
const hasIncompatibleFrontend = frontend.some((f) => f === "next");
const options: DeploymentOption[] = [];
if (existingDeployment !== "wrangler") {
@@ -95,7 +88,7 @@ export async function getDeploymentToAdd(
});
}
if (existingDeployment !== "alchemy" && !hasIncompatibleFrontend) {
if (existingDeployment !== "alchemy") {
const { label, hint } = getDeploymentDisplay("alchemy");
options.push({
value: "alchemy",
@@ -123,9 +116,7 @@ export async function getDeploymentToAdd(
const response = await select<WebDeploy>({
message: "Select web deployment",
options,
initialValue: hasIncompatibleFrontend
? "wrangler"
: DEFAULT_CONFIG.webDeploy,
initialValue: DEFAULT_CONFIG.webDeploy,
});
if (isCancel(response)) return exitCancelled("Operation cancelled");

View File

@@ -68,6 +68,7 @@ export const DatabaseSetupSchema = z
"turso",
"neon",
"prisma-postgres",
"planetscale",
"mongodb-atlas",
"supabase",
"d1",

View File

@@ -68,37 +68,13 @@ export function validateWorkersCompatibility(
);
}
if (
providedFlags.has("runtime") &&
options.runtime === "workers" &&
config.orm &&
config.orm !== "drizzle" &&
config.orm !== "none"
) {
exitWithError(
`Cloudflare Workers runtime (--runtime workers) is only supported with Drizzle ORM (--orm drizzle) or no ORM (--orm none). Current ORM: ${config.orm}. Please use '--orm drizzle', '--orm none', or choose a different runtime.`,
);
}
if (
providedFlags.has("orm") &&
config.orm &&
config.orm !== "drizzle" &&
config.orm !== "none" &&
config.runtime === "workers"
) {
exitWithError(
`ORM '${config.orm}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Please use '--orm drizzle', '--orm none', or choose a different runtime.`,
);
}
if (
providedFlags.has("runtime") &&
options.runtime === "workers" &&
config.database === "mongodb"
) {
exitWithError(
"Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.",
"Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.",
);
}
@@ -118,7 +94,7 @@ export function validateWorkersCompatibility(
config.runtime === "workers"
) {
exitWithError(
"MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.",
"MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.",
);
}
@@ -261,29 +237,3 @@ export function validateExamplesCompatibility(
);
}
}
export function validateAlchemyCompatibility(
webDeploy: WebDeploy | undefined,
serverDeploy: ServerDeploy | undefined,
frontends: Frontend[] = [],
) {
const isAlchemyWebDeploy = webDeploy === "alchemy";
const isAlchemyServerDeploy = serverDeploy === "alchemy";
if (isAlchemyWebDeploy || isAlchemyServerDeploy) {
const incompatibleFrontends = frontends.filter((f) => f === "next");
if (incompatibleFrontends.length > 0) {
const deployType =
isAlchemyWebDeploy && isAlchemyServerDeploy
? "web and server deployment"
: isAlchemyWebDeploy
? "web deployment"
: "server deployment";
exitWithError(
`Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")} frontend(s). Please choose a different frontend or deployment option.`,
);
}
}
}

View File

@@ -9,7 +9,6 @@ import {
ensureSingleWebAndNative,
isWebFrontend,
validateAddonsAgainstFrontends,
validateAlchemyCompatibility,
validateApiFrontendCompatibility,
validateExamplesCompatibility,
validateServerDeployRequiresBackend,
@@ -126,6 +125,10 @@ export function validateDatabaseSetup(
errorMessage:
"Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
},
planetscale: {
errorMessage:
"PlanetScale setup requires PostgreSQL or MySQL database. Please use '--database postgres' or '--database mysql' or choose a different setup.",
},
"mongodb-atlas": {
database: "mongodb",
errorMessage:
@@ -152,8 +155,15 @@ export function validateDatabaseSetup(
if (dbSetup && dbSetup !== "none") {
const validation = setupValidations[dbSetup];
if (validation.database && database !== validation.database) {
exitWithError(validation.errorMessage);
// Special handling for PlanetScale - supports both postgres and mysql
if (dbSetup === "planetscale") {
if (database !== "postgres" && database !== "mysql") {
exitWithError(validation.errorMessage);
}
} else {
if (validation.database && database !== validation.database) {
exitWithError(validation.errorMessage);
}
}
if (validation.runtime && runtime !== validation.runtime) {
@@ -416,12 +426,6 @@ export function validateFullConfig(
config.database,
config.frontend ?? [],
);
validateAlchemyCompatibility(
config.webDeploy,
config.serverDeploy,
config.frontend ?? [],
);
}
export function validateConfigForProgrammaticUse(

View File

@@ -1,61 +0,0 @@
import path from "node:path";
import { Biome } from "@biomejs/js-api/nodejs";
import fs from "fs-extra";
import { glob } from "tinyglobby";
export async function formatProjectWithBiome(projectDir: string) {
const biome = new Biome();
const { projectKey } = biome.openProject(projectDir);
biome.applyConfiguration(projectKey, {
formatter: {
enabled: true,
indentStyle: "tab",
},
javascript: {
formatter: {
quoteStyle: "double",
},
},
});
const files = await glob("**/*", {
cwd: projectDir,
dot: true,
absolute: true,
onlyFiles: true,
});
for (const filePath of files) {
try {
const ext = path.extname(filePath).toLowerCase();
const supported = new Set([
".ts",
".tsx",
".js",
".jsx",
".cjs",
".mjs",
".cts",
".mts",
".json",
".jsonc",
".md",
".mdx",
".css",
".scss",
".html",
]);
if (!supported.has(ext)) continue;
const original = await fs.readFile(filePath, "utf8");
const result = biome.formatContent(projectKey, original, { filePath });
const content = result?.content;
if (typeof content !== "string") continue;
if (content.length === 0 && original.length > 0) continue;
if (content !== original) {
await fs.writeFile(filePath, content);
}
} catch {}
}
}