diff --git a/.cursor/rules/better-t-stack-repo.mdc b/.cursor/rules/better-t-stack-repo.mdc index 5171b32..90a5fa2 100644 --- a/.cursor/rules/better-t-stack-repo.mdc +++ b/.cursor/rules/better-t-stack-repo.mdc @@ -2,7 +2,8 @@ alwaysApply: true --- -- use functional programming -- use normal function syntax for functions do not use arrow functions -- no emojis -- use type syntax, dont use interface syntax for types +- Always use functional programming; avoid object-oriented programming. +- Define functions using the standard function declaration syntax, not arrow functions. +- Do not include emojis. +- Use TypeScript type aliases instead of interface declarations. +- In Handlebars templates, avoid generic if/else blocks. Write explicit conditions, such as: use if (eq orm "prisma") for Prisma, and else if (eq orm "drizzle") for Drizzle. diff --git a/apps/cli/package.json b/apps/cli/package.json index c76c181..f55e388 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,90 +1,88 @@ { - "name": "create-better-t-stack", - "version": "2.40.5", - "description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations", - "type": "module", - "license": "MIT", - "author": "Aman Varshney", - "bin": { - "create-better-t-stack": "dist/cli.js" - }, - "files": [ - "templates", - "dist" - ], - "keywords": [ - "better-t-stack", - "typescript", - "boilerplate", - "starter", - "cli", - "turborepo", - "trpc", - "better-auth", - "monorepo", - "fullstack", - "type-safety", - "react", - "react-native", - "expo", - "hono", - "elysia", - "drizzle", - "prisma", - "tanstack", - "tailwind", - "shadcn", - "pwa", - "tauri", - "biome" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/AmanVarshney01/create-better-t-stack.git", - "directory": "apps/cli" - }, - "publishConfig": { - "access": "public" - }, - "homepage": "https://better-t-stack.dev/", - "scripts": { - "build": "tsdown", - "dev": "tsdown --watch", - "check-types": "tsc --noEmit", - "check": "biome check --write .", - "test": "bun run build && vitest run", - "test:ui": "bun run build && vitest --ui", - "test:with-build": "bun run build && WITH_BUILD=1 vitest --ui", - "prepublishOnly": "npm run build" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "dependencies": { - "@biomejs/js-api": "^3.0.0", - "@biomejs/wasm-nodejs": "^2.2.0", - "@clack/prompts": "^1.0.0-alpha.4", - "consola": "^3.4.2", - "execa": "^9.6.0", - "fs-extra": "^11.3.1", - "gradient-string": "^3.0.0", - "handlebars": "^4.7.8", - "jsonc-parser": "^3.3.1", - "picocolors": "^1.1.1", - "tinyglobby": "^0.2.14", - "trpc-cli": "^0.10.2", - "ts-morph": "^26.0.0", - "zod": "^4.0.17" - }, - "devDependencies": { - "@types/fs-extra": "^11.0.4", - "@types/node": "^24.3.0", - "@vitest/ui": "^3.2.4", - "tsdown": "^0.14.1", - "typescript": "^5.9.2", - "vitest": "^3.2.4" - } + "name": "create-better-t-stack", + "version": "2.40.4", + "description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations", + "type": "module", + "license": "MIT", + "author": "Aman Varshney", + "bin": { + "create-better-t-stack": "dist/cli.js" + }, + "files": [ + "templates", + "dist" + ], + "keywords": [ + "better-t-stack", + "typescript", + "boilerplate", + "starter", + "cli", + "turborepo", + "trpc", + "better-auth", + "monorepo", + "fullstack", + "type-safety", + "react", + "react-native", + "expo", + "hono", + "elysia", + "drizzle", + "prisma", + "tanstack", + "tailwind", + "shadcn", + "pwa", + "tauri", + "biome" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/AmanVarshney01/create-better-t-stack.git", + "directory": "apps/cli" + }, + "publishConfig": { + "access": "public" + }, + "homepage": "https://better-t-stack.dev/", + "scripts": { + "build": "tsdown", + "dev": "tsdown --watch", + "check-types": "tsc --noEmit", + "check": "biome check --write .", + "test": "bun run build && vitest run", + "test:ui": "bun run build && vitest --ui", + "test:with-build": "bun run build && WITH_BUILD=1 vitest --ui", + "prepublishOnly": "npm run build" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "dependencies": { + "@clack/prompts": "^1.0.0-alpha.4", + "consola": "^3.4.2", + "execa": "^9.6.0", + "fs-extra": "^11.3.1", + "gradient-string": "^3.0.0", + "handlebars": "^4.7.8", + "jsonc-parser": "^3.3.1", + "picocolors": "^1.1.1", + "tinyglobby": "^0.2.15", + "trpc-cli": "^0.10.2", + "ts-morph": "^27.0.0", + "zod": "^4.1.5" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.4", + "@types/node": "^24.3.1", + "@vitest/ui": "^3.2.4", + "tsdown": "^0.14.2", + "typescript": "^5.9.2", + "vitest": "^3.2.4" + } } diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index 1189e3c..e679d3e 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -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", diff --git a/apps/cli/src/helpers/core/create-project.ts b/apps/cli/src/helpers/core/create-project.ts index e02a7b6..124219c 100644 --- a/apps/cli/src/helpers/core/create-project.ts +++ b/apps/cli/src/helpers/core/create-project.ts @@ -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); } diff --git a/apps/cli/src/helpers/core/db-setup.ts b/apps/cli/src/helpers/core/db-setup.ts index 48d47e1..fb0c7a7 100644 --- a/apps/cli/src/helpers/core/db-setup.ts +++ b/apps/cli/src/helpers/core/db-setup.ts @@ -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); } diff --git a/apps/cli/src/helpers/core/env-setup.ts b/apps/cli/src/helpers/core/env-setup.ts index 4578de9..f31a5d6 100644 --- a/apps/cli/src/helpers/core/env-setup.ts +++ b/apps/cli/src/helpers/core/env-setup.ts @@ -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", diff --git a/apps/cli/src/helpers/core/post-installation.ts b/apps/cli/src/helpers/core/post-installation.ts index 1d75f56..54b9b41 100644 --- a/apps/cli/src/helpers/core/post-installation.ts +++ b/apps/cli/src/helpers/core/post-installation.ts @@ -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( diff --git a/apps/cli/src/helpers/core/project-config.ts b/apps/cli/src/helpers/core/project-config.ts index 152d203..e680abf 100644 --- a/apps/cli/src/helpers/core/project-config.ts +++ b/apps/cli/src/helpers/core/project-config.ts @@ -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")) { diff --git a/apps/cli/src/helpers/database-providers/d1-setup.ts b/apps/cli/src/helpers/database-providers/d1-setup.ts index 05bfd47..05d8988 100644 --- a/apps/cli/src/helpers/database-providers/d1-setup.ts +++ b/apps/cli/src/helpers/database-providers/d1-setup.ts @@ -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, + }); + } } 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 ac65fae..266f42c 100644 --- a/apps/cli/src/helpers/database-providers/mongodb-atlas-setup.ts +++ b/apps/cli/src/helpers/database-providers/mongodb-atlas-setup.ts @@ -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); diff --git a/apps/cli/src/helpers/database-providers/neon-setup.ts b/apps/cli/src/helpers/database-providers/neon-setup.ts index 5cf25db..acaf0f2 100644 --- a/apps/cli/src/helpers/database-providers/neon-setup.ts +++ b/apps/cli/src/helpers/database-providers/neon-setup.ts @@ -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: [ diff --git a/apps/cli/src/helpers/database-providers/planetscale-setup.ts b/apps/cli/src/helpers/database-providers/planetscale-setup.ts new file mode 100644 index 0000000..f2c9411 --- /dev/null +++ b/apps/cli/src/helpers/database-providers/planetscale-setup.ts @@ -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); + } +} 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 ca4974a..7de2ef7 100644 --- a/apps/cli/src/helpers/database-providers/prisma-postgres-setup.ts +++ b/apps/cli/src/helpers/database-providers/prisma-postgres-setup.ts @@ -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", diff --git a/apps/cli/src/helpers/database-providers/supabase-setup.ts b/apps/cli/src/helpers/database-providers/supabase-setup.ts index c40615d..fa4bdbb 100644 --- a/apps/cli/src/helpers/database-providers/supabase-setup.ts +++ b/apps/cli/src/helpers/database-providers/supabase-setup.ts @@ -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(); diff --git a/apps/cli/src/helpers/database-providers/turso-setup.ts b/apps/cli/src/helpers/database-providers/turso-setup.ts index effe2cc..5f339ce 100644 --- a/apps/cli/src/helpers/database-providers/turso-setup.ts +++ b/apps/cli/src/helpers/database-providers/turso-setup.ts @@ -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: ${ diff --git a/apps/cli/src/helpers/deployment/alchemy/alchemy-next-setup.ts b/apps/cli/src/helpers/deployment/alchemy/alchemy-next-setup.ts index 86b4ecc..ad10355 100644 --- a/apps/cli/src/helpers/deployment/alchemy/alchemy-next-setup.ts +++ b/apps/cli/src/helpers/deployment/alchemy/alchemy-next-setup.ts @@ -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"); + } } diff --git a/apps/cli/src/helpers/deployment/server-deploy-setup.ts b/apps/cli/src/helpers/deployment/server-deploy-setup.ts index 154c931..175f121 100644 --- a/apps/cli/src/helpers/deployment/server-deploy-setup.ts +++ b/apps/cli/src/helpers/deployment/server-deploy-setup.ts @@ -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")); diff --git a/apps/cli/src/prompts/database-setup.ts b/apps/cli/src/prompts/database-setup.ts index 2d27d7c..4cb3c6c 100644 --- a/apps/cli/src/prompts/database-setup.ts +++ b/apps/cli/src/prompts/database-setup.ts @@ -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 { @@ -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", diff --git a/apps/cli/src/prompts/orm.ts b/apps/cli/src/prompts/orm.ts index 5e8f03e..da050cf 100644 --- a/apps/cli/src/prompts/orm.ts +++ b/apps/cli/src/prompts/orm.ts @@ -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({ 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"); diff --git a/apps/cli/src/prompts/web-deploy.ts b/apps/cli/src/prompts/web-deploy.ts index cf08530..7c3933a 100644 --- a/apps/cli/src/prompts/web-deploy.ts +++ b/apps/cli/src/prompts/web-deploy.ts @@ -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({ 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({ message: "Select web deployment", options, - initialValue: hasIncompatibleFrontend - ? "wrangler" - : DEFAULT_CONFIG.webDeploy, + initialValue: DEFAULT_CONFIG.webDeploy, }); if (isCancel(response)) return exitCancelled("Operation cancelled"); diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index fce9a9b..6204368 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -68,6 +68,7 @@ export const DatabaseSetupSchema = z "turso", "neon", "prisma-postgres", + "planetscale", "mongodb-atlas", "supabase", "d1", diff --git a/apps/cli/src/utils/compatibility-rules.ts b/apps/cli/src/utils/compatibility-rules.ts index 07600ec..2387e9a 100644 --- a/apps/cli/src/utils/compatibility-rules.ts +++ b/apps/cli/src/utils/compatibility-rules.ts @@ -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.`, - ); - } - } -} diff --git a/apps/cli/src/utils/config-validation.ts b/apps/cli/src/utils/config-validation.ts index d4e9153..2966f4d 100644 --- a/apps/cli/src/utils/config-validation.ts +++ b/apps/cli/src/utils/config-validation.ts @@ -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( diff --git a/apps/cli/src/utils/format-with-biome.ts b/apps/cli/src/utils/format-with-biome.ts deleted file mode 100644 index 67d0ebf..0000000 --- a/apps/cli/src/utils/format-with-biome.ts +++ /dev/null @@ -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 {} - } -} diff --git a/apps/cli/templates/auth/better-auth/server/base/src/lib/auth.ts.hbs b/apps/cli/templates/auth/better-auth/server/base/src/lib/auth.ts.hbs index 10e53ee..2d876f5 100644 --- a/apps/cli/templates/auth/better-auth/server/base/src/lib/auth.ts.hbs +++ b/apps/cli/templates/auth/better-auth/server/base/src/lib/auth.ts.hbs @@ -4,7 +4,7 @@ import { prismaAdapter } from "better-auth/adapters/prisma"; {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} import { expo } from "@better-auth/expo"; {{/if}} -import prisma from "../../prisma"; +import prisma from "../db"; export const auth = betterAuth({ database: prismaAdapter(prisma, { diff --git a/apps/cli/templates/db/drizzle/mysql/src/db/index.ts.hbs b/apps/cli/templates/db/drizzle/mysql/src/db/index.ts.hbs index b51809a..e6915df 100644 --- a/apps/cli/templates/db/drizzle/mysql/src/db/index.ts.hbs +++ b/apps/cli/templates/db/drizzle/mysql/src/db/index.ts.hbs @@ -1,4 +1,15 @@ {{#if (or (eq runtime "bun") (eq runtime "node"))}} +{{#if (eq dbSetup "planetscale")}} +import { drizzle } from "drizzle-orm/planetscale-serverless"; + +export const db = drizzle({ + connection: { + host: process.env.DATABASE_HOST, + username: process.env.DATABASE_USERNAME, + password: process.env.DATABASE_PASSWORD, + }, +}); +{{else}} import { drizzle } from "drizzle-orm/mysql2"; export const db = drizzle({ @@ -7,8 +18,21 @@ export const db = drizzle({ }, }); {{/if}} +{{/if}} {{#if (eq runtime "workers")}} +{{#if (eq dbSetup "planetscale")}} +import { drizzle } from "drizzle-orm/planetscale-serverless"; +import { env } from "cloudflare:workers"; + +export const db = drizzle({ + connection: { + host: env.DATABASE_HOST, + username: env.DATABASE_USERNAME, + password: env.DATABASE_PASSWORD, + }, +}); +{{else}} import { drizzle } from "drizzle-orm/mysql2"; import { env } from "cloudflare:workers"; @@ -18,3 +42,4 @@ export const db = drizzle({ }, }); {{/if}} +{{/if}} diff --git a/apps/cli/templates/db/prisma/mongodb/prisma/index.ts.hbs b/apps/cli/templates/db/prisma/mongodb/prisma/index.ts.hbs deleted file mode 100644 index 4c7a320..0000000 --- a/apps/cli/templates/db/prisma/mongodb/prisma/index.ts.hbs +++ /dev/null @@ -1,5 +0,0 @@ -import { PrismaClient } from "./generated/client"; - -const prisma = new PrismaClient(); - -export default prisma; diff --git a/apps/cli/templates/db/prisma/mongodb/src/db/index.ts.hbs b/apps/cli/templates/db/prisma/mongodb/src/db/index.ts.hbs new file mode 100644 index 0000000..daa212a --- /dev/null +++ b/apps/cli/templates/db/prisma/mongodb/src/db/index.ts.hbs @@ -0,0 +1,5 @@ +import { PrismaClient } from "../../prisma/generated/client"; + +const prisma = new PrismaClient(); + +export default prisma; diff --git a/apps/cli/templates/db/prisma/mysql/prisma/index.ts b/apps/cli/templates/db/prisma/mysql/prisma/index.ts deleted file mode 100644 index 4c7a320..0000000 --- a/apps/cli/templates/db/prisma/mysql/prisma/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PrismaClient } from "./generated/client"; - -const prisma = new PrismaClient(); - -export default prisma; diff --git a/apps/cli/templates/db/prisma/mysql/prisma/schema/schema.prisma.hbs b/apps/cli/templates/db/prisma/mysql/prisma/schema/schema.prisma.hbs index efad1d7..158d57b 100644 --- a/apps/cli/templates/db/prisma/mysql/prisma/schema/schema.prisma.hbs +++ b/apps/cli/templates/db/prisma/mysql/prisma/schema/schema.prisma.hbs @@ -11,9 +11,15 @@ generator client { {{#if (eq runtime "workers")}} runtime = "workerd" {{/if}} + {{#if (eq dbSetup "planetscale")}} + previewFeatures = ["driverAdapters"] + {{/if}} } datasource db { provider = "mysql" url = env("DATABASE_URL") + {{#if (eq dbSetup "planetscale")}} + relationMode = "prisma" + {{/if}} } diff --git a/apps/cli/templates/db/prisma/mysql/src/db/index.ts.hbs b/apps/cli/templates/db/prisma/mysql/src/db/index.ts.hbs new file mode 100644 index 0000000..81193e8 --- /dev/null +++ b/apps/cli/templates/db/prisma/mysql/src/db/index.ts.hbs @@ -0,0 +1,12 @@ +import { PrismaClient } from "../../prisma/generated/client"; +{{#if (eq dbSetup "planetscale")}} +import { PrismaPlanetScale } from '@prisma/adapter-planetscale' + +const adapter = new PrismaPlanetScale({ url: process.env.DATABASE_URL }) + +const prisma = new PrismaClient({adapter}); +{{else}} +const prisma = new PrismaClient(); +{{/if}} + +export default prisma; diff --git a/apps/cli/templates/db/prisma/postgres/prisma/index.ts b/apps/cli/templates/db/prisma/postgres/prisma/index.ts deleted file mode 100644 index 4c7a320..0000000 --- a/apps/cli/templates/db/prisma/postgres/prisma/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PrismaClient } from "./generated/client"; - -const prisma = new PrismaClient(); - -export default prisma; diff --git a/apps/cli/templates/db/prisma/postgres/prisma/schema/schema.prisma.hbs b/apps/cli/templates/db/prisma/postgres/prisma/schema/schema.prisma.hbs index 9f8aa8c..b61dc42 100644 --- a/apps/cli/templates/db/prisma/postgres/prisma/schema/schema.prisma.hbs +++ b/apps/cli/templates/db/prisma/postgres/prisma/schema/schema.prisma.hbs @@ -19,4 +19,7 @@ datasource db { {{#if (eq dbSetup "supabase")}} directUrl = env("DIRECT_URL") {{/if}} + {{#if (eq dbSetup "planetscale")}} + relationMode = "prisma" + {{/if}} } diff --git a/apps/cli/templates/db/prisma/postgres/src/db/index.ts.hbs b/apps/cli/templates/db/prisma/postgres/src/db/index.ts.hbs new file mode 100644 index 0000000..daa212a --- /dev/null +++ b/apps/cli/templates/db/prisma/postgres/src/db/index.ts.hbs @@ -0,0 +1,5 @@ +import { PrismaClient } from "../../prisma/generated/client"; + +const prisma = new PrismaClient(); + +export default prisma; diff --git a/apps/cli/templates/db/prisma/sqlite/prisma.config.ts.hbs b/apps/cli/templates/db/prisma/sqlite/prisma.config.ts.hbs index b6cf18b..594c94a 100644 --- a/apps/cli/templates/db/prisma/sqlite/prisma.config.ts.hbs +++ b/apps/cli/templates/db/prisma/sqlite/prisma.config.ts.hbs @@ -1,10 +1,38 @@ import "dotenv/config"; import path from "node:path"; import type { PrismaConfig } from "prisma"; +{{#if (eq dbSetup "d1")}} +import { PrismaD1 } from "@prisma/adapter-d1"; +{{/if}} +{{#if (eq dbSetup "turso")}} +import { PrismaLibSQL } from "@prisma/adapter-libsql"; +{{/if}} export default { + {{#if (or (eq dbSetup "d1") (eq dbSetup "turso"))}} + experimental: { + adapter: true + }, + {{/if}} schema: path.join("prisma", "schema"), migrations: { path: path.join("prisma", "migrations"), - } + }, + {{#if (eq dbSetup "d1")}} + async adapter() { + return new PrismaD1({ + CLOUDFLARE_D1_TOKEN: process.env.CLOUDFLARE_D1_TOKEN, + CLOUDFLARE_ACCOUNT_ID: process.env.CLOUDFLARE_ACCOUNT_ID, + CLOUDFLARE_DATABASE_ID: process.env.CLOUDFLARE_DATABASE_ID, + }); + }, + {{/if}} + {{#if (eq dbSetup "turso")}} + async adapter() { + return new PrismaLibSQL({ + url: process.env.DATABASE_URL || "", + authToken: process.env.DATABASE_AUTH_TOKEN, + }); + }, + {{/if}} } satisfies PrismaConfig; diff --git a/apps/cli/templates/db/prisma/sqlite/prisma/index.ts b/apps/cli/templates/db/prisma/sqlite/prisma/index.ts deleted file mode 100644 index 4c7a320..0000000 --- a/apps/cli/templates/db/prisma/sqlite/prisma/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PrismaClient } from "./generated/client"; - -const prisma = new PrismaClient(); - -export default prisma; diff --git a/apps/cli/templates/db/prisma/sqlite/prisma/schema/schema.prisma.hbs b/apps/cli/templates/db/prisma/sqlite/prisma/schema/schema.prisma.hbs index 4e4d1e3..a858365 100644 --- a/apps/cli/templates/db/prisma/sqlite/prisma/schema/schema.prisma.hbs +++ b/apps/cli/templates/db/prisma/sqlite/prisma/schema/schema.prisma.hbs @@ -10,10 +10,20 @@ generator client { {{/if}} {{#if (eq runtime "workers")}} runtime = "workerd" + {{#if (eq dbSetup "d1")}} + previewFeatures = ["driverAdapters"] + {{/if}} + {{/if}} + {{#if (eq dbSetup "turso")}} + previewFeatures = ["driverAdapters"] {{/if}} } datasource db { provider = "sqlite" + {{#if (eq dbSetup "turso")}} + url = "file:./local.db" + {{else}} url = env("DATABASE_URL") + {{/if}} } diff --git a/apps/cli/templates/db/prisma/sqlite/src/db/index.ts.hbs b/apps/cli/templates/db/prisma/sqlite/src/db/index.ts.hbs new file mode 100644 index 0000000..926a78b --- /dev/null +++ b/apps/cli/templates/db/prisma/sqlite/src/db/index.ts.hbs @@ -0,0 +1,28 @@ +{{#if (eq dbSetup "d1")}} +import { env } from "cloudflare:workers"; +import { PrismaD1 } from "@prisma/adapter-d1"; +import { PrismaClient } from "../../prisma/generated/client"; + +const adapter = new PrismaD1(env.DB); +const prisma = new PrismaClient({ adapter }); + +export default prisma; +{{else if (eq dbSetup "turso")}} +import { PrismaLibSQL } from "@prisma/adapter-libsql"; +import { PrismaClient } from "../../prisma/generated/client"; + +const adapter = new PrismaLibSQL({ + url: process.env.DATABASE_URL || "", + authToken: process.env.DATABASE_AUTH_TOKEN, +}); + +const prisma = new PrismaClient({ adapter }); + +export default prisma; +{{else}} +import { PrismaClient } from "../../prisma/generated/client"; + +const prisma = new PrismaClient(); + +export default prisma; +{{/if}} diff --git a/apps/cli/templates/deploy/alchemy/alchemy.run.ts.hbs b/apps/cli/templates/deploy/alchemy/alchemy.run.ts.hbs index 027eb77..5f04a58 100644 --- a/apps/cli/templates/deploy/alchemy/alchemy.run.ts.hbs +++ b/apps/cli/templates/deploy/alchemy/alchemy.run.ts.hbs @@ -1,7 +1,7 @@ import alchemy from "alchemy"; {{#if (eq webDeploy "alchemy")}} {{#if (includes frontend "next")}} -import { Next } from "alchemy/cloudflare"; +import { Nextjs } from "alchemy/cloudflare"; {{else if (includes frontend "nuxt")}} import { Nuxt } from "alchemy/cloudflare"; {{else if (includes frontend "svelte")}} @@ -44,13 +44,17 @@ await Exec("db-generate", { }); const db = await D1Database("database", { + {{#if (eq orm "prisma")}} + migrationsDir: "apps/server/prisma/migrations", + {{else if (eq orm "drizzle")}} migrationsDir: "apps/server/src/db/migrations", + {{/if}} }); {{/if}} {{#if (eq webDeploy "alchemy")}} {{#if (includes frontend "next")}} -export const web = await Next("web", { +export const web = await Nextjs("web", { {{#if (eq serverDeploy "alchemy")}}cwd: "apps/web",{{/if}} bindings: { {{#if (eq backend "convex")}} diff --git a/apps/cli/templates/deploy/wrangler/server/wrangler.jsonc.hbs b/apps/cli/templates/deploy/wrangler/server/wrangler.jsonc.hbs index 1aabfb3..9c739b9 100644 --- a/apps/cli/templates/deploy/wrangler/server/wrangler.jsonc.hbs +++ b/apps/cli/templates/deploy/wrangler/server/wrangler.jsonc.hbs @@ -27,7 +27,12 @@ "database_name": "YOUR_DB_NAME", "database_id": "YOUR_DB_ID", "preview_database_id": "local-test-db", + {{#if (eq orm "drizzle")}} "migrations_dir": "./src/db/migrations" + {{/if}} + {{#if (eq orm "prisma")}} + "migrations_dir": "./prisma/migrations" + {{/if}} } ] {{/if}} diff --git a/apps/cli/templates/examples/todo/server/prisma/base/src/routers/todo.ts.hbs b/apps/cli/templates/examples/todo/server/prisma/base/src/routers/todo.ts.hbs index ad7fe12..57303b6 100644 --- a/apps/cli/templates/examples/todo/server/prisma/base/src/routers/todo.ts.hbs +++ b/apps/cli/templates/examples/todo/server/prisma/base/src/routers/todo.ts.hbs @@ -1,6 +1,6 @@ {{#if (eq api "orpc")}} import z from "zod"; -import prisma from "../../prisma"; +import prisma from "../db"; import { publicProcedure } from "../lib/orpc"; export const todoRouter = { @@ -52,7 +52,7 @@ export const todoRouter = { {{#if (eq api "trpc")}} import { TRPCError } from "@trpc/server"; import z from "zod"; -import prisma from "../../prisma"; +import prisma from "../db"; import { publicProcedure, router } from "../lib/trpc"; export const todoRouter = router({ diff --git a/apps/cli/templates/extras/bunfig.toml.hbs b/apps/cli/templates/extras/bunfig.toml.hbs index 3c52875..79ea608 100644 --- a/apps/cli/templates/extras/bunfig.toml.hbs +++ b/apps/cli/templates/extras/bunfig.toml.hbs @@ -1,7 +1,2 @@ [install] -{{#if (or (or (includes frontend "nuxt") (includes frontend "native-nativewind")) (includes frontend -"native-unistyles"))}} # linker = "isolated" -{{else}} -linker = "isolated" -{{/if}} \ No newline at end of file diff --git a/apps/cli/templates/frontend/react/next/next.config.ts.hbs b/apps/cli/templates/frontend/react/next/next.config.ts.hbs index b99d587..69e118e 100644 --- a/apps/cli/templates/frontend/react/next/next.config.ts.hbs +++ b/apps/cli/templates/frontend/react/next/next.config.ts.hbs @@ -1,3 +1,6 @@ +{{#if (or (eq webDeploy "alchemy") (eq webDeploy "wrangler"))}} +import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"; +{{/if}} import type { NextConfig } from "next"; const nextConfig: NextConfig = { @@ -5,3 +8,7 @@ const nextConfig: NextConfig = { }; export default nextConfig; + +{{#if (or (eq webDeploy "alchemy") (eq webDeploy "wrangler"))}} +initOpenNextCloudflareForDev(); +{{/if}} \ No newline at end of file diff --git a/apps/cli/test/cli.smoke.test.ts b/apps/cli/test/cli.smoke.test.ts index 30903f3..565e39a 100644 --- a/apps/cli/test/cli.smoke.test.ts +++ b/apps/cli/test/cli.smoke.test.ts @@ -2936,6 +2936,190 @@ describe("create-better-t-stack smoke", () => { }); }); + it("scaffolds with MySQL + Drizzle + PlanetScale", async () => { + const projectName = "app-mysql-drizzle-planetscale"; + await runCli( + [ + projectName, + + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "bun", + "--database", + "mysql", + "--orm", + "drizzle", + "--api", + "trpc", + "--auth", + "none", + "--addons", + "none", + "--examples", + "none", + "--db-setup", + "planetscale", + "--web-deploy", + "none", + "--server-deploy", + "none", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + + const projectDir = join(workdir, projectName); + assertScaffoldedProject(projectDir); + assertBtsConfig(projectDir, { + database: "mysql", + orm: "drizzle", + }); + }); + + it("scaffolds with MySQL + Prisma + PlanetScale", async () => { + const projectName = "app-mysql-prisma-planetscale"; + await runCli( + [ + projectName, + + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "bun", + "--database", + "mysql", + "--orm", + "prisma", + "--api", + "trpc", + "--auth", + "none", + "--addons", + "none", + "--examples", + "none", + "--db-setup", + "planetscale", + "--web-deploy", + "none", + "--server-deploy", + "none", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + + const projectDir = join(workdir, projectName); + assertScaffoldedProject(projectDir); + assertBtsConfig(projectDir, { + database: "mysql", + orm: "prisma", + }); + }); + + it("scaffolds with PostgreSQL + Drizzle + PlanetScale", async () => { + const projectName = "app-postgres-drizzle-planetscale"; + await runCli( + [ + projectName, + + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "bun", + "--database", + "postgres", + "--orm", + "drizzle", + "--api", + "trpc", + "--auth", + "none", + "--addons", + "none", + "--examples", + "none", + "--db-setup", + "planetscale", + "--web-deploy", + "none", + "--server-deploy", + "none", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + + const projectDir = join(workdir, projectName); + await assertScaffoldedProject(projectDir); + await assertBtsConfig(projectDir, { + database: "postgres", + orm: "drizzle", + }); + }); + + it("scaffolds with PostgreSQL + Prisma + PlanetScale", async () => { + const projectName = "app-postgres-prisma-planetscale"; + await runCli( + [ + projectName, + + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "bun", + "--database", + "postgres", + "--orm", + "prisma", + "--api", + "trpc", + "--auth", + "none", + "--addons", + "none", + "--examples", + "none", + "--db-setup", + "planetscale", + "--web-deploy", + "none", + "--server-deploy", + "none", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + + const projectDir = join(workdir, projectName); + assertScaffoldedProject(projectDir); + assertBtsConfig(projectDir, { + database: "postgres", + orm: "prisma", + }); + }); + it("scaffolds oRPC with Next.js", async () => { const projectName = "app-orpc-next"; await runCli( @@ -3210,6 +3394,100 @@ describe("create-better-t-stack smoke", () => { runtime: "node", }); }); + + it("scaffolds with MySQL + Drizzle + PlanetScale + Node runtime", async () => { + const projectName = "app-mysql-drizzle-planetscale-node"; + await runCli( + [ + projectName, + + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "node", + "--database", + "mysql", + "--orm", + "drizzle", + "--api", + "trpc", + "--auth", + "none", + "--addons", + "none", + "--examples", + "none", + "--db-setup", + "planetscale", + "--web-deploy", + "none", + "--server-deploy", + "none", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + + const projectDir = join(workdir, projectName); + await assertScaffoldedProject(projectDir); + await assertBtsConfig(projectDir, { + database: "mysql", + orm: "drizzle", + runtime: "node", + }); + }); + + it("scaffolds with MySQL + Prisma + PlanetScale + Workers runtime", async () => { + const projectName = "app-mysql-prisma-planetscale-workers"; + await runCli( + [ + projectName, + + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "workers", + "--database", + "mysql", + "--orm", + "prisma", + "--api", + "trpc", + "--auth", + "none", + "--addons", + "none", + "--examples", + "none", + "--db-setup", + "planetscale", + "--web-deploy", + "none", + "--server-deploy", + "wrangler", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + + const projectDir = join(workdir, projectName); + assertScaffoldedProject(projectDir); + assertBtsConfig(projectDir, { + database: "mysql", + orm: "prisma", + runtime: "workers", + }); + }); }); (process.env.WITH_BUILD === "1" ? describe : describe.skip)( @@ -3296,12 +3574,18 @@ describe("create-better-t-stack smoke", () => { "app-with-auth", "app-mysql-prisma", "app-mysql-drizzle", + "app-mysql-drizzle-planetscale", + "app-mysql-prisma-planetscale", + "app-postgres-drizzle-planetscale", + "app-postgres-prisma-planetscale", "app-orpc-next", "app-orpc-nuxt", "app-orpc-svelte", "app-orpc-solid", "app-backend-next", "app-node-runtime", + "app-mysql-drizzle-planetscale-node", + "app-mysql-prisma-planetscale-workers", ].forEach((n) => { projectNames.add(n); }); diff --git a/apps/web/content/docs/cli/compatibility.mdx b/apps/web/content/docs/cli/compatibility.mdx index 35d11fe..dd19d74 100644 --- a/apps/web/content/docs/cli/compatibility.mdx +++ b/apps/web/content/docs/cli/compatibility.mdx @@ -42,7 +42,7 @@ Cloudflare Workers has specific compatibility requirements: | Component | Requirement | Reason | |-----------|-------------|--------| | Backend | Must be `hono` | Only Hono supports Workers runtime | -| ORM | Must be `drizzle` or `none` | Workers doesn't support Prisma/Mongoose | +| ORM | Must be `drizzle` or `prisma` | Workers supports Drizzle and Prisma; Mongoose is not supported | | Database | Cannot be `mongodb` | MongoDB requires Prisma/Mongoose | | Database Setup | Cannot be `docker` | Workers is serverless, no Docker support | @@ -52,6 +52,9 @@ create-better-t-stack --runtime workers --backend express # ✅ Valid - Workers with Hono create-better-t-stack --runtime workers --backend hono --database sqlite --orm drizzle --db-setup d1 + +# ✅ Also valid - Workers with Prisma (D1) +create-better-t-stack --runtime workers --backend hono --database sqlite --orm prisma --db-setup d1 ``` ### Backend Presets @@ -123,8 +126,8 @@ create-better-t-stack --frontend next native-nativewind | Setup Provider | Required Database | Notes | |---------------|------------------|-------| -| `turso` | `sqlite` | Distributed SQLite | -| `d1` | `sqlite` | Cloudflare D1 (requires Workers runtime) | +| `turso` | `sqlite` | Distributed SQLite; works with Drizzle and Prisma | +| `d1` | `sqlite` | Cloudflare D1 (requires Workers runtime); works with Drizzle and Prisma | | `neon` | `postgres` | Serverless PostgreSQL | | `supabase` | `postgres` | PostgreSQL with additional features | | `prisma-postgres` | `postgres` | Managed PostgreSQL via Prisma | diff --git a/apps/web/content/docs/compatibility.mdx b/apps/web/content/docs/compatibility.mdx deleted file mode 100644 index 5591fff..0000000 --- a/apps/web/content/docs/compatibility.mdx +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: Compatibility -description: Valid and invalid combinations across frontend, backend, runtime, database, and addons ---- - -## Rules - -- **Convex backend**: Sets database, ORM, and API to `none`; auth to `clerk` (if compatible frontends) or `none` -- **Backend `none`**: Forces API, ORM, database, authentication, and runtime to `none`; disables examples -- **Frontend `none`**: Backend-only project; PWA/Tauri/examples may be disabled -- **API `none`**: No tRPC/oRPC setup; use framework-native APIs -- **Database `none`**: Disables ORM and Better-Auth (but allows Clerk with Convex) -- **ORM `none`**: No ORM setup; manage DB manually -- **Runtime `none`**: Only with Convex backend or when backend is `none` -- **Auth `clerk`**: Only available with Convex backend and compatible frontends - -## Cloudflare Workers - -- Backend: `hono` only -- Database: `sqlite` with Cloudflare D1 -- ORM: `drizzle` (or none) -- Not compatible with MongoDB - -## Framework Notes - -- SvelteKit, Nuxt, and SolidJS frontends are only compatible with `orpc` API layer -- PWA addon requires a web frontend: TanStack Router, React Router, Next.js, or SolidJS -- Tauri addon requires React (TanStack Router/React Router), Nuxt, SvelteKit, SolidJS, or Next.js -- AI example is not compatible with Elysia backend or SolidJS frontend - diff --git a/apps/web/content/docs/meta.json b/apps/web/content/docs/meta.json index 656119f..8ea0cff 100644 --- a/apps/web/content/docs/meta.json +++ b/apps/web/content/docs/meta.json @@ -7,7 +7,6 @@ "bts-config", "analytics", "contributing", - "compatibility", "faq" ] } diff --git a/apps/web/package.json b/apps/web/package.json index 7776414..3876aa3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,72 +1,74 @@ { - "name": "web", - "version": "0.0.0", - "private": true, - "scripts": { - "build": "next build", - "dev": "next dev --turbopack", - "start": "next start", - "check": "biome check --write .", - "postinstall": "fumadocs-mdx", - "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview", - "deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy", - "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts", - "generate-analytics": "bun scripts/generate-analytics.ts", - "generate-schema": "bun scripts/generate-schema.ts" - }, - "dependencies": { - "@better-t-stack/backend": "workspace:*", - "@erquhart/convex-oss-stats": "^0.8.1", - "@number-flow/react": "^0.5.10", - "@opennextjs/cloudflare": "^1.6.3", - "@orama/orama": "^3.1.11", - "@radix-ui/react-dropdown-menu": "^2.1.15", - "@radix-ui/react-hover-card": "^1.1.14", - "babel-plugin-react-compiler": "^19.1.0-rc.2", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "convex": "^1.25.4", - "convex-helpers": "^0.1.104", - "culori": "^4.0.2", - "date-fns": "^4.1.0", - "fumadocs-core": "15.6.7", - "fumadocs-mdx": "11.7.3", - "fumadocs-ui": "15.6.7", - "lucide-react": "^0.536.0", - "motion": "^12.23.12", - "next": "15.3.5", - "next-themes": "^0.4.6", - "nuqs": "^2.5.2", - "papaparse": "^5.5.3", - "posthog-js": "^1.258.5", - "qrcode": "^1.5.4", - "radix-ui": "^1.4.2", - "react": "^19.1.1", - "react-dom": "^19.1.1", - "react-tweet": "^3.2.2", - "recharts": "2.15.4", - "remark": "^15.0.1", - "remark-gfm": "^4.0.1", - "remark-mdx": "^3.1.0", - "shiki": "^3.9.1", - "sonner": "^2.0.6", - "tailwind-merge": "^3.3.1" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4.1.11", - "@types/culori": "^4.0.0", - "@types/mdx": "^2.0.13", - "@types/node": "24.1.0", - "@types/papaparse": "^5.3.16", - "@types/qrcode": "^1.5.5", - "@types/react": "^19.1.9", - "@types/react-dom": "^19.1.7", - "eslint": "^9.32.0", - "eslint-config-next": "15.4.5", - "postcss": "^8.5.6", - "tailwindcss": "^4.1.11", - "tw-animate-css": "^1.3.6", - "typescript": "^5.9.2", - "wrangler": "^4.27.0" - } + "name": "web", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "next build", + "dev": "next dev --turbopack", + "start": "next start", + "check": "biome check --write .", + "postinstall": "fumadocs-mdx", + "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview", + "deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy", + "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts", + "generate-analytics": "bun scripts/generate-analytics.ts", + "generate-schema": "bun scripts/generate-schema.ts" + }, + "dependencies": { + "@better-t-stack/backend": "workspace:*", + "@erquhart/convex-oss-stats": "^0.8.1", + "@number-flow/react": "^0.5.10", + "@opennextjs/cloudflare": "^1.6.3", + "@orama/orama": "^3.1.11", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-toggle": "^1.1.10", + "babel-plugin-react-compiler": "^19.1.0-rc.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "convex": "^1.25.4", + "convex-helpers": "^0.1.104", + "culori": "^4.0.2", + "date-fns": "^4.1.0", + "fumadocs-core": "15.6.7", + "fumadocs-mdx": "11.7.3", + "fumadocs-ui": "15.6.7", + "lucide-react": "^0.536.0", + "motion": "^12.23.12", + "next": "15.3.5", + "next-themes": "^0.4.6", + "nuqs": "^2.5.2", + "papaparse": "^5.5.3", + "posthog-js": "^1.258.5", + "qrcode": "^1.5.4", + "radix-ui": "^1.4.2", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-tweet": "^3.2.2", + "recharts": "2.15.4", + "remark": "^15.0.1", + "remark-gfm": "^4.0.1", + "remark-mdx": "^3.1.0", + "shiki": "^3.9.1", + "sonner": "^2.0.6", + "tailwind-merge": "^3.3.1" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.11", + "@types/culori": "^4.0.0", + "@types/mdx": "^2.0.13", + "@types/node": "24.1.0", + "@types/papaparse": "^5.3.16", + "@types/qrcode": "^1.5.5", + "@types/react": "^19.1.9", + "@types/react-dom": "^19.1.7", + "eslint": "^9.32.0", + "eslint-config-next": "15.4.5", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.11", + "tw-animate-css": "^1.3.6", + "typescript": "^5.9.2", + "wrangler": "^4.27.0" + } } diff --git a/apps/web/src/app/(home)/new/_components/action-buttons.tsx b/apps/web/src/app/(home)/new/_components/action-buttons.tsx new file mode 100644 index 0000000..ff66cb6 --- /dev/null +++ b/apps/web/src/app/(home)/new/_components/action-buttons.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { RefreshCw, Settings, Shuffle, Star } from "lucide-react"; + +interface ActionButtonsProps { + onReset: () => void; + onRandom: () => void; + onSave: () => void; + onLoad: () => void; + hasSavedStack: boolean; +} + +export function ActionButtons({ + onReset, + onRandom, + onSave, + onLoad, + hasSavedStack, +}: ActionButtonsProps) { + return ( +
+ + + + {hasSavedStack && ( + + )} +
+ ); +} diff --git a/apps/web/src/app/(home)/new/_components/preset-dropdown.tsx b/apps/web/src/app/(home)/new/_components/preset-dropdown.tsx new file mode 100644 index 0000000..1361d08 --- /dev/null +++ b/apps/web/src/app/(home)/new/_components/preset-dropdown.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { ChevronDown, Zap } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { PRESET_TEMPLATES } from "@/lib/constant"; + +interface PresetDropdownProps { + onApplyPreset: (presetId: string) => void; +} + +export function PresetDropdown({ onApplyPreset }: PresetDropdownProps) { + return ( + + + + + + {PRESET_TEMPLATES.map((preset) => ( + onApplyPreset(preset.id)} + className="flex flex-col items-start gap-1 p-3" + > +
{preset.name}
+
{preset.description}
+
+ ))} +
+
+ ); +} diff --git a/apps/web/src/app/(home)/new/_components/share-button.tsx b/apps/web/src/app/(home)/new/_components/share-button.tsx new file mode 100644 index 0000000..5a53d8f --- /dev/null +++ b/apps/web/src/app/(home)/new/_components/share-button.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { Share2 } from "lucide-react"; +import { ShareDialog } from "@/components/ui/share-dialog"; +import type { StackState } from "@/lib/constant"; + +interface ShareButtonProps { + stackUrl: string; + stackState: StackState; +} + +export function ShareButton({ stackUrl, stackState }: ShareButtonProps) { + return ( + + + + ); +} diff --git a/apps/web/src/app/(home)/new/_components/stack-builder.tsx b/apps/web/src/app/(home)/new/_components/stack-builder.tsx index 5c1d839..6710927 100644 --- a/apps/web/src/app/(home)/new/_components/stack-builder.tsx +++ b/apps/web/src/app/(home)/new/_components/stack-builder.tsx @@ -5,13 +5,8 @@ import { ChevronDown, ClipboardCopy, InfoIcon, - RefreshCw, Settings, - Share2, - Shuffle, - Star, Terminal, - Zap, } from "lucide-react"; import { motion } from "motion/react"; import type React from "react"; @@ -27,11 +22,9 @@ import { toast } from "sonner"; import { DropdownMenu, DropdownMenuContent, - DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { ShareDialog } from "@/components/ui/share-dialog"; import { Tooltip, TooltipContent, @@ -51,7 +44,10 @@ import { generateStackSharingUrl, } from "@/lib/stack-utils"; import { cn } from "@/lib/utils"; +import { ActionButtons } from "./action-buttons"; import { getBadgeColors } from "./get-badge-color"; +import { PresetDropdown } from "./preset-dropdown"; +import { ShareButton } from "./share-button"; import { TechIcon } from "./tech-icon"; import { analyzeStackCompatibility, @@ -60,6 +56,7 @@ import { isOptionCompatible, validateProjectName, } from "./utils"; +import { YoloToggle } from "./yolo-toggle"; const StackBuilder = () => { const [stack, setStack] = useStackState(); @@ -407,7 +404,12 @@ const StackBuilder = () => { const applyPreset = (presetId: string) => { const preset = PRESET_TEMPLATES.find( - (template) => template.id === presetId, + (template: { + id: string; + name: string; + description: string; + stack: StackState; + }) => template.id === presetId, ); if (preset) { startTransition(() => { @@ -505,92 +507,41 @@ const StackBuilder = () => {
-
- - -
+ -
- - {lastSavedStack ? ( - - ) : ( -
- )} -
+
+ - - - + - - - - - - {PRESET_TEMPLATES.map((preset) => ( - applyPreset(preset.id)} - className="flex flex-col items-start gap-1 p-3" + + + + + + setStack({ yolo })} + /> + + +
diff --git a/apps/web/src/app/(home)/new/_components/utils.ts b/apps/web/src/app/(home)/new/_components/utils.ts index df910d7..df0db3a 100644 --- a/apps/web/src/app/(home)/new/_components/utils.ts +++ b/apps/web/src/app/(home)/new/_components/utils.ts @@ -61,6 +61,15 @@ interface CompatibilityResult { export const analyzeStackCompatibility = ( stack: StackState, ): CompatibilityResult => { + // Skip all validation if YOLO mode is enabled + if (stack.yolo === "true") { + return { + adjustedStack: null, + notes: {}, + changes: [], + }; + } + const nextStack = { ...stack }; let changed = false; const notes: CompatibilityResult["notes"] = {}; @@ -352,12 +361,12 @@ export const analyzeStackCompatibility = ( "Database set to 'SQLite' (Turso hosting requires SQLite database)", }); } - if (nextStack.orm !== "drizzle") { + if (nextStack.orm !== "drizzle" && nextStack.orm !== "prisma") { notes.dbSetup.notes.push( - "Turso requires Drizzle ORM. It will be selected.", + "Turso requires Drizzle or Prisma ORM. Drizzle will be selected.", ); notes.orm.notes.push( - "Turso DB setup requires Drizzle ORM. It will be selected.", + "Turso DB setup requires Drizzle or Prisma ORM. Drizzle will be selected.", ); notes.dbSetup.hasIssue = true; notes.orm.hasIssue = true; @@ -366,7 +375,7 @@ export const analyzeStackCompatibility = ( changes.push({ category: "dbSetup", message: - "ORM set to 'Drizzle' (Turso hosting requires Drizzle ORM)", + "ORM set to 'Drizzle' (Turso hosting requires Drizzle or Prisma ORM)", }); } } else if (nextStack.dbSetup === "prisma-postgres") { @@ -454,6 +463,27 @@ export const analyzeStackCompatibility = ( "Database set to 'PostgreSQL' (Supabase hosting requires PostgreSQL database)", }); } + } else if (nextStack.dbSetup === "planetscale") { + if ( + nextStack.database !== "postgres" && + nextStack.database !== "mysql" + ) { + notes.dbSetup.notes.push( + "PlanetScale requires PostgreSQL or MySQL. PostgreSQL will be selected.", + ); + notes.database.notes.push( + "PlanetScale DB setup requires PostgreSQL or MySQL. PostgreSQL will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.database.hasIssue = true; + nextStack.database = "postgres"; + changed = true; + changes.push({ + category: "dbSetup", + message: + "Database set to 'PostgreSQL' (PlanetScale supports PostgreSQL and MySQL)", + }); + } } else if (nextStack.dbSetup === "d1") { if (nextStack.database !== "sqlite") { notes.dbSetup.notes.push( @@ -471,6 +501,23 @@ export const analyzeStackCompatibility = ( message: "Database set to 'SQLite' (required by Cloudflare D1)", }); } + if (nextStack.orm !== "drizzle" && nextStack.orm !== "prisma") { + notes.dbSetup.notes.push( + "Cloudflare D1 requires Drizzle or Prisma ORM. Drizzle will be selected.", + ); + notes.orm.notes.push( + "Cloudflare D1 DB setup requires Drizzle or Prisma ORM. Drizzle will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "drizzle"; + changed = true; + changes.push({ + category: "dbSetup", + message: + "ORM set to 'Drizzle' (Cloudflare D1 requires Drizzle or Prisma ORM)", + }); + } if (nextStack.runtime !== "workers") { notes.dbSetup.notes.push( "Cloudflare D1 requires Cloudflare Workers runtime. It will be selected.", @@ -487,22 +534,6 @@ export const analyzeStackCompatibility = ( message: "Runtime set to 'Cloudflare Workers' (required by D1)", }); } - if (nextStack.orm !== "drizzle") { - notes.dbSetup.notes.push( - "Cloudflare D1 requires Drizzle ORM. It will be selected.", - ); - notes.orm.notes.push( - "Cloudflare D1 DB setup requires Drizzle ORM. It will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.orm.hasIssue = true; - nextStack.orm = "drizzle"; - changed = true; - changes.push({ - category: "dbSetup", - message: "ORM set to 'Drizzle' (required by Cloudflare D1)", - }); - } if (nextStack.backend !== "hono") { notes.dbSetup.notes.push( "Cloudflare D1 requires Hono backend. It will be selected.", @@ -581,6 +612,9 @@ export const analyzeStackCompatibility = ( } else if (nextStack.dbSetup === "mongodb-atlas") { selectedDatabase = "mongodb"; databaseName = "MongoDB"; + } else if (nextStack.dbSetup === "planetscale") { + selectedDatabase = "postgres"; + databaseName = "PostgreSQL"; } notes.dbSetup.notes.push( @@ -618,24 +652,6 @@ export const analyzeStackCompatibility = ( }); } - if (nextStack.orm !== "drizzle" && nextStack.orm !== "none") { - notes.runtime.notes.push( - "Cloudflare Workers runtime requires Drizzle ORM or no ORM. Drizzle will be selected.", - ); - notes.orm.notes.push( - "Cloudflare Workers runtime requires Drizzle ORM or no ORM. Drizzle will be selected.", - ); - notes.runtime.hasIssue = true; - notes.orm.hasIssue = true; - nextStack.orm = "drizzle"; - changed = true; - changes.push({ - category: "runtime", - message: - "ORM set to 'Drizzle' (Cloudflare Workers runtime only supports Drizzle or no ORM)", - }); - } - if (nextStack.database === "mongodb") { notes.runtime.notes.push( "Cloudflare Workers runtime is not compatible with MongoDB. SQLite will be selected.", @@ -1050,49 +1066,6 @@ export const analyzeStackCompatibility = ( }); } - const isAlchemyWebDeploy = nextStack.webDeploy === "alchemy"; - const isAlchemyServerDeploy = nextStack.serverDeploy === "alchemy"; - - if (isAlchemyWebDeploy || isAlchemyServerDeploy) { - const incompatibleFrontends = nextStack.webFrontend.filter( - (f) => f === "next", - ); - - if (incompatibleFrontends.length > 0) { - const deployType = - isAlchemyWebDeploy && isAlchemyServerDeploy - ? "web and server deployment" - : isAlchemyWebDeploy - ? "web deployment" - : "server deployment"; - - notes.webFrontend.notes.push( - `Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")}. These frontends will be removed.`, - ); - notes.webDeploy.notes.push( - `Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")}.`, - ); - notes.serverDeploy.notes.push( - `Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")}.`, - ); - notes.webFrontend.hasIssue = true; - notes.webDeploy.hasIssue = true; - notes.serverDeploy.hasIssue = true; - - nextStack.webFrontend = nextStack.webFrontend.filter((f) => f !== "next"); - - if (nextStack.webFrontend.length === 0) { - nextStack.webFrontend = ["tanstack-router"]; - } - - changed = true; - changes.push({ - category: "alchemy", - message: `Removed ${incompatibleFrontends.join(" and ")} frontend (temporarily not compatible with Alchemy ${deployType} - support coming soon)`, - }); - } - } - if ( nextStack.serverDeploy === "alchemy" && (nextStack.runtime !== "workers" || nextStack.backend !== "hono") @@ -1223,15 +1196,6 @@ export const getDisabledReason = ( const { adjustedStack } = analyzeStackCompatibility(simulatedStack); const finalStack = adjustedStack ?? simulatedStack; - if (category === "webFrontend" && optionId === "next") { - const isAlchemyWebDeploy = finalStack.webDeploy === "alchemy"; - const isAlchemyServerDeploy = finalStack.serverDeploy === "alchemy"; - - if (isAlchemyWebDeploy || isAlchemyServerDeploy) { - return "Next.js is temporarily not compatible with Alchemy deployment. Support coming soon!"; - } - } - if (category === "webFrontend" && optionId === "solid") { if (finalStack.backend === "convex") { return "Solid is not compatible with Convex backend. Try TanStack Router, React Router, or Next.js instead."; @@ -1383,11 +1347,17 @@ export const getDisabledReason = ( if (finalStack.database === "none") { return "Prisma ORM requires a database. Select a database first (SQLite, PostgreSQL, MySQL, or MongoDB)."; } + if (finalStack.dbSetup === "turso" && finalStack.database !== "sqlite") { + return "Turso setup requires SQLite database. Select SQLite first."; + } + if (finalStack.dbSetup === "d1" && finalStack.database !== "sqlite") { + return "Cloudflare D1 setup requires SQLite database. Select SQLite first."; + } } if (category === "dbSetup" && optionId === "turso") { - if (finalStack.orm !== "drizzle") { - return "Turso requires Drizzle ORM. Select Drizzle first."; + if (finalStack.orm !== "drizzle" && finalStack.orm !== "prisma") { + return "Turso requires Drizzle or Prisma ORM. Select Drizzle or Prisma first."; } } @@ -1398,8 +1368,8 @@ export const getDisabledReason = ( } if (category === "dbSetup" && optionId === "d1") { - if (finalStack.orm !== "drizzle") { - return "Cloudflare D1 requires Drizzle ORM. Select Drizzle first."; + if (finalStack.orm !== "drizzle" && finalStack.orm !== "prisma") { + return "Cloudflare D1 requires Drizzle or Prisma ORM. Select Drizzle or Prisma first."; } if (finalStack.runtime !== "workers") { return "Cloudflare D1 requires Cloudflare Workers runtime. Select Workers runtime first."; @@ -1461,15 +1431,20 @@ export const getDisabledReason = ( finalStack.dbSetup !== "docker" && finalStack.dbSetup !== "prisma-postgres" && finalStack.dbSetup !== "neon" && - finalStack.dbSetup !== "supabase" + finalStack.dbSetup !== "supabase" && + finalStack.dbSetup !== "planetscale" ) { - return "PostgreSQL database only works with Docker, Prisma PostgreSQL, Neon, Supabase, or Basic Setup. Select one of these options or change database."; + return "PostgreSQL database only works with Docker, Prisma PostgreSQL, Neon, Supabase, PlanetScale, or Basic Setup. Select one of these options or change database."; } } if (category === "database" && optionId === "mysql") { - if (finalStack.dbSetup !== "none" && finalStack.dbSetup !== "docker") { - return "MySQL database only works with Docker or Basic Setup. Select one of these options or change database."; + if ( + finalStack.dbSetup !== "none" && + finalStack.dbSetup !== "docker" && + finalStack.dbSetup !== "planetscale" + ) { + return "MySQL database only works with Docker, PlanetScale, or Basic Setup. Select one of these options or change database."; } } @@ -1584,12 +1559,27 @@ export const getDisabledReason = ( } } + if (category === "dbSetup" && optionId === "planetscale") { + if (finalStack.database !== "postgres" && finalStack.database !== "mysql") { + return "PlanetScale requires PostgreSQL or MySQL database. Select PostgreSQL or MySQL first."; + } + } + if (category === "dbSetup" && optionId === "supabase") { if ((finalStack.database as string) !== "postgres") { return "Supabase requires PostgreSQL database. Select PostgreSQL first."; } } + if ( + category === "database" && + (finalStack.dbSetup as string) === "planetscale" + ) { + if (optionId !== "postgres" && optionId !== "mysql") { + return "Selected DB Setup 'PlanetScale' requires PostgreSQL or MySQL. Select PostgreSQL or MySQL, or change DB Setup."; + } + } + if ( category === "database" && (finalStack.dbSetup as string) === "supabase" @@ -1607,5 +1597,8 @@ export const isOptionCompatible = ( category: keyof typeof TECH_OPTIONS, optionId: string, ): boolean => { + if (currentStack.yolo === "true") { + return true; + } return getDisabledReason(currentStack, category, optionId) === null; }; diff --git a/apps/web/src/app/(home)/new/_components/yolo-toggle.tsx b/apps/web/src/app/(home)/new/_components/yolo-toggle.tsx new file mode 100644 index 0000000..845fe7a --- /dev/null +++ b/apps/web/src/app/(home)/new/_components/yolo-toggle.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { AlertTriangle } from "lucide-react"; +import { Switch } from "@/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import type { StackState } from "@/lib/constant"; +import { cn } from "@/lib/utils"; + +interface YoloToggleProps { + stack: StackState; + onToggle: (yolo: string) => void; +} + +export function YoloToggle({ stack, onToggle }: YoloToggleProps) { + const isYoloEnabled = stack.yolo === "true"; + + return ( + + +
+ +
+
YOLO Mode
+
+ {isYoloEnabled ? "Enabled" : "Disabled"} +
+
+ onToggle(checked ? "true" : "false")} + className={cn( + isYoloEnabled && "data-[state=checked]:bg-destructive", + )} + /> +
+
+ +

+ Disables all validation and adds --yolo flag to the command. Use at + your own risk! +

+
+
+ ); +} diff --git a/apps/web/src/components/ui/share-dialog.tsx b/apps/web/src/components/ui/share-dialog.tsx index 856b37f..9c14c82 100644 --- a/apps/web/src/components/ui/share-dialog.tsx +++ b/apps/web/src/components/ui/share-dialog.tsx @@ -114,7 +114,6 @@ export function ShareDialog({ ); }; - // Generate QR code using local qrcode library useEffect(() => { const generateQRCode = async () => { try { @@ -264,25 +263,6 @@ export function ShareDialog({ - -
-
-
- - - OUTPUT.URL - -
-
-
-
- $ - - {stackUrl} - -
-
-
diff --git a/apps/web/src/components/ui/switch.tsx b/apps/web/src/components/ui/switch.tsx index b85b7bf..bc3afa8 100644 --- a/apps/web/src/components/ui/switch.tsx +++ b/apps/web/src/components/ui/switch.tsx @@ -1,6 +1,6 @@ "use client"; -import { Switch as SwitchPrimitive } from "radix-ui"; +import * as SwitchPrimitive from "@radix-ui/react-switch"; import type * as React from "react"; import { cn } from "@/lib/utils"; diff --git a/apps/web/src/components/ui/toggle.tsx b/apps/web/src/components/ui/toggle.tsx new file mode 100644 index 0000000..e3aad6d --- /dev/null +++ b/apps/web/src/components/ui/toggle.tsx @@ -0,0 +1,47 @@ +"use client"; + +import * as TogglePrimitive from "@radix-ui/react-toggle"; +import { cva, type VariantProps } from "class-variance-authority"; +import type * as React from "react"; + +import { cn } from "@/lib/utils"; + +const toggleVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-[color,box-shadow] hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-transparent", + outline: + "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground", + }, + size: { + default: "h-9 min-w-9 px-2", + sm: "h-8 min-w-8 px-1.5", + lg: "h-10 min-w-10 px-2.5", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +function Toggle({ + className, + variant, + size, + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + ); +} + +export { Toggle, toggleVariants }; diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index c19ae6d..43f0893 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -328,6 +328,13 @@ export const TECH_OPTIONS: Record< icon: `${ICON_BASE_URL}/supabase.svg`, color: "from-emerald-400 to-emerald-600", }, + { + id: "planetscale", + name: "PlanetScale", + description: "Serverless MySQL platform with branching", + icon: `${ICON_BASE_URL}/planetscale.svg`, + color: "from-orange-400 to-orange-600", + }, { id: "docker", name: "Docker", @@ -604,6 +611,7 @@ export const PRESET_TEMPLATES = [ api: "trpc", webDeploy: "none", serverDeploy: "none", + yolo: "false", }, }, { @@ -628,6 +636,7 @@ export const PRESET_TEMPLATES = [ api: "none", webDeploy: "none", serverDeploy: "none", + yolo: "false", }, }, { @@ -652,6 +661,7 @@ export const PRESET_TEMPLATES = [ api: "trpc", webDeploy: "none", serverDeploy: "none", + yolo: "false", }, }, { @@ -676,6 +686,7 @@ export const PRESET_TEMPLATES = [ api: "trpc", webDeploy: "none", serverDeploy: "none", + yolo: "false", }, }, { @@ -700,6 +711,7 @@ export const PRESET_TEMPLATES = [ api: "trpc", webDeploy: "alchemy", serverDeploy: "alchemy", + yolo: "false", }, }, ]; @@ -722,6 +734,7 @@ export type StackState = { api: string; webDeploy: string; serverDeploy: string; + yolo: string; }; export const DEFAULT_STACK: StackState = { @@ -742,6 +755,7 @@ export const DEFAULT_STACK: StackState = { api: "trpc", webDeploy: "none", serverDeploy: "none", + yolo: "false", }; export const isStackDefault = ( diff --git a/apps/web/src/lib/stack-url-keys.ts b/apps/web/src/lib/stack-url-keys.ts index 92c8cc8..7f41b65 100644 --- a/apps/web/src/lib/stack-url-keys.ts +++ b/apps/web/src/lib/stack-url-keys.ts @@ -19,4 +19,5 @@ export const stackUrlKeys: UrlKeys> = { install: "i", webDeploy: "wd", serverDeploy: "sd", + yolo: "yolo", }; diff --git a/apps/web/src/lib/stack-url-state.client.ts b/apps/web/src/lib/stack-url-state.client.ts index d72829d..c111694 100644 --- a/apps/web/src/lib/stack-url-state.client.ts +++ b/apps/web/src/lib/stack-url-state.client.ts @@ -61,6 +61,9 @@ export const stackParsers = { serverDeploy: parseAsStringEnum( getValidIds("serverDeploy"), ).withDefault(DEFAULT_STACK.serverDeploy), + yolo: parseAsStringEnum(["true", "false"]).withDefault( + DEFAULT_STACK.yolo, + ), }; export const stackQueryStatesOptions = { diff --git a/apps/web/src/lib/stack-url-state.ts b/apps/web/src/lib/stack-url-state.ts index 11b8f07..fc69bd2 100644 --- a/apps/web/src/lib/stack-url-state.ts +++ b/apps/web/src/lib/stack-url-state.ts @@ -67,6 +67,10 @@ const serverStackParsers = { serverDeploy: parseAsStringEnumServer( getValidIds("serverDeploy"), ).withDefault(DEFAULT_STACK.serverDeploy), + yolo: parseAsStringEnumServer([ + "true", + "false", + ]).withDefault(DEFAULT_STACK.yolo), }; export const loadStackParams = createLoader(serverStackParsers, { diff --git a/apps/web/src/lib/stack-utils.ts b/apps/web/src/lib/stack-utils.ts index 581d5c6..a5d5b8e 100644 --- a/apps/web/src/lib/stack-utils.ts +++ b/apps/web/src/lib/stack-utils.ts @@ -119,6 +119,10 @@ export function generateStackCommand(stack: StackState): string { `--examples ${stack.examples.join(" ") || "none"}`, ]; + if (stack.yolo === "true") { + flags.push("--yolo"); + } + return `${base} ${projectName} ${flags.join(" ")}`; } diff --git a/biome.json b/biome.json index 455a27b..cd0a118 100644 --- a/biome.json +++ b/biome.json @@ -16,7 +16,6 @@ "!**/out", "!**/templates", "!**/.turbo", - "!**/package.json", "!**/analytics-minimal.json", "!**/schema.json", "!**/_generated", diff --git a/bun.lock b/bun.lock index a21f65b..ea9bbb4 100644 --- a/bun.lock +++ b/bun.lock @@ -20,8 +20,6 @@ "create-better-t-stack": "dist/cli.js", }, "dependencies": { - "@biomejs/js-api": "^3.0.0", - "@biomejs/wasm-nodejs": "^2.2.0", "@clack/prompts": "^1.0.0-alpha.4", "consola": "^3.4.2", "execa": "^9.6.0", @@ -30,16 +28,16 @@ "handlebars": "^4.7.8", "jsonc-parser": "^3.3.1", "picocolors": "^1.1.1", - "tinyglobby": "^0.2.14", + "tinyglobby": "^0.2.15", "trpc-cli": "^0.10.2", - "ts-morph": "^26.0.0", - "zod": "^4.0.17", + "ts-morph": "^27.0.0", + "zod": "^4.1.5", }, "devDependencies": { "@types/fs-extra": "^11.0.4", - "@types/node": "^24.3.0", + "@types/node": "^24.3.1", "@vitest/ui": "^3.2.4", - "tsdown": "^0.14.1", + "tsdown": "^0.14.2", "typescript": "^5.9.2", "vitest": "^3.2.4", }, @@ -55,6 +53,8 @@ "@orama/orama": "^3.1.11", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-toggle": "^1.1.10", "babel-plugin-react-compiler": "^19.1.0-rc.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -271,10 +271,6 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww=="], - "@biomejs/js-api": ["@biomejs/js-api@3.0.0", "", { "peerDependencies": { "@biomejs/wasm-bundler": "^2.2.0", "@biomejs/wasm-nodejs": "^2.2.0", "@biomejs/wasm-web": "^2.2.0" }, "optionalPeers": ["@biomejs/wasm-bundler", "@biomejs/wasm-nodejs", "@biomejs/wasm-web"] }, "sha512-5QcGJFj9IO+yXl76ICjvkdE38uxRcTDsBzcCZHEZ+ma+Te/nbvJg4A3KtAds9HCrEF0JKLWiyjMhAbqazuJvYA=="], - - "@biomejs/wasm-nodejs": ["@biomejs/wasm-nodejs@2.2.2", "", {}, "sha512-GxI0ejyXaCjIq6SxBr9e4jyf3zmI5Eyyq5fF0dlb4nECVXwAgUAnT6hQ5502ZACvBxoWmreXw08im/l3APpvIw=="], - "@clack/core": ["@clack/core@1.0.0-alpha.4", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-VCtU+vjyKPMSakVrB9q1bOnXN7QW/w4+YQDQCOF59GrzydW+169i0fVx/qzRRXJgt8KGj/pZZ/JxXroFZIDByg=="], "@clack/prompts": ["@clack/prompts@1.0.0-alpha.4", "", { "dependencies": { "@clack/core": "1.0.0-alpha.4", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-KnmtDF2xQGoI5AlBme9akHtvCRV0RKAARUXHBQO2tMwnY8B08/4zPWigT7uLK25UPrMCEqnyQPkKRjNdhPbf8g=="], @@ -939,7 +935,7 @@ "@trpc/server": ["@trpc/server@11.5.0", "", { "peerDependencies": { "typescript": ">=5.7.2" } }, "sha512-0IBtkmUCeO2ycn4K45/cqsujnlCQrSvkCo7lFDpg3kGMIPiLyLRciID5IiS7prEjRjeITa+od2aaHTIwONApVw=="], - "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], + "@ts-morph/common": ["@ts-morph/common@0.28.0", "", { "dependencies": { "minimatch": "^10.0.1", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.14" } }, "sha512-4w6X/oFmvXcwux6y6ExfM/xSqMHw20cYwFJH+BlYrtGa6nwY9qGq8GXnUs1sVYeF2o/KT3S8hAH6sKBI3VOkBg=="], "@tsconfig/node18": ["@tsconfig/node18@1.0.3", "", {}, "sha512-RbwvSJQsuN9TB04AQbGULYfOGE/RnSFk/FLQ5b0NmDf5Kx2q/lABZbHQPKCO1vZ6Fiwkplu+yb9pGdLy1iGseQ=="], @@ -995,7 +991,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + "@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], @@ -1545,7 +1541,7 @@ "fast-equals": ["fast-equals@5.2.2", "", {}, "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw=="], - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -2481,7 +2477,7 @@ "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tinygradient": ["tinygradient@1.1.5", "", { "dependencies": { "@types/tinycolor2": "^1.4.0", "tinycolor2": "^1.0.0" } }, "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw=="], @@ -2513,7 +2509,7 @@ "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], - "ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="], + "ts-morph": ["ts-morph@27.0.0", "", { "dependencies": { "@ts-morph/common": "~0.28.0", "code-block-writer": "^13.0.3" } }, "sha512-xcqelpTR5PCuZMs54qp9DE3t7tPgA2v/P1/qdW4ke5b3Y5liTGTYj6a/twT35EQW/H5okRqp1UOqwNlgg0K0eQ=="], "ts-tqdm": ["ts-tqdm@0.8.6", "", {}, "sha512-3X3M1PZcHtgQbnwizL+xU8CAgbYbeLHrrDwL9xxcZZrV5J+e7loJm1XrXozHjSkl44J0Zg0SgA8rXbh83kCkcQ=="], @@ -3213,8 +3209,6 @@ "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "@next/eslint-plugin-next/fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], - "@node-minify/core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], "@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], @@ -3301,22 +3295,40 @@ "@ts-morph/common/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], + "@types/fs-extra/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@types/jsonfile/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@types/node-fetch/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@types/papaparse/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@types/qrcode/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "@typescript-eslint/typescript-estree/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + "@vitest/ui/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "body-parser/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + "bun-types/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + "changelogen/c12": ["c12@1.11.2", "", { "dependencies": { "chokidar": "^3.6.0", "confbox": "^0.1.7", "defu": "^6.1.4", "dotenv": "^16.4.5", "giget": "^1.2.3", "jiti": "^1.21.6", "mlly": "^1.7.1", "ohash": "^1.1.3", "pathe": "^1.1.2", "perfect-debounce": "^1.0.0", "pkg-types": "^1.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.4" }, "optionalPeers": ["magicast"] }, "sha512-oBs8a4uvSDO9dm8b7OCFW7+dgtVrwmwnrVXYzLm43ta7ep2jCn/0MhoUFygIWtxhyy6+/MG7/agvpY0U1Iemew=="], "changelogen/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "changelogen/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "changelogithub/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "cloudflare/@types/node": ["@types/node@18.19.123", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg=="], @@ -3325,6 +3337,8 @@ "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-import-resolver-typescript/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -3349,6 +3363,8 @@ "fumadocs-mdx/esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="], + "fumadocs-mdx/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "glob/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], "htmlparser2/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], @@ -3413,10 +3429,16 @@ "trpc-cli/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "tsdown/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "vite/esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="], + "vite/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + "vitest/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "web/@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], @@ -3843,8 +3865,6 @@ "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], - "@next/eslint-plugin-next/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "@node-minify/core/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="], "@node-minify/core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], @@ -3875,6 +3895,8 @@ "@smithy/util-endpoints/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.0.5", "", { "dependencies": { "@smithy/types": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ=="], + "@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "changelogen/c12/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], diff --git a/package.json b/package.json index cf96841..23607d1 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,47 @@ { - "name": "better-t-stack", - "version": "0.0.0", - "private": true, - "scripts": { - "build": "turbo build", - "dev": "turbo dev", - "dev:cli": "turbo run dev --filter=create-better-t-stack", - "cli": "cd apps/cli && node dist/cli.js", - "dev:web": "turbo run dev --filter=web", - "build:web": "turbo run build --filter=web", - "build:cli": "turbo run build --filter=create-better-t-stack", - "lint": "turbo lint", - "check": "turbo check", - "format": "biome check --write .", - "release": "bun run scripts/release.ts", - "bump": "bun run scripts/bump-version.ts", - "canary": "bun run scripts/canary-release.ts", - "deploy:convex": "turbo run --filter=@better-t-stack/backend deploy", - "deploy:web": "bun run --filter=web deploy", - "generate": "bun run --filter=web generate-analytics && bun run --filter=web generate-schema", - "deploy": "bun run deploy:web" - }, - "devDependencies": { - "@biomejs/biome": "2.2.0", - "changelogithub": "^13.16.0", - "husky": "^9.1.7", - "lint-staged": "^16.1.5", - "turbo": "^2.5.6", - "typescript": "5.9.2", - "@types/bun": "latest" - }, - "lint-staged": { - "*": [ - "bun biome check --write ." - ] - }, - "engines": { - "node": ">=20" - }, - "packageManager": "bun@1.2.20", - "workspaces": [ - "apps/*", - "packages/*" - ], - "dependencies": {} + "name": "better-t-stack", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "turbo build", + "dev": "turbo dev", + "dev:cli": "turbo run dev --filter=create-better-t-stack", + "cli": "cd apps/cli && node dist/cli.js", + "dev:web": "turbo run dev --filter=web", + "build:web": "turbo run build --filter=web", + "build:cli": "turbo run build --filter=create-better-t-stack", + "lint": "turbo lint", + "check": "turbo check", + "format": "biome check --write .", + "release": "bun run scripts/release.ts", + "bump": "bun run scripts/bump-version.ts", + "canary": "bun run scripts/canary-release.ts", + "deploy:convex": "turbo run --filter=@better-t-stack/backend deploy", + "deploy:web": "bun run --filter=web deploy", + "generate": "bun run --filter=web generate-analytics && bun run --filter=web generate-schema", + "deploy": "bun run deploy:web" + }, + "devDependencies": { + "@biomejs/biome": "2.2.0", + "changelogithub": "^13.16.0", + "husky": "^9.1.7", + "lint-staged": "^16.1.5", + "turbo": "^2.5.6", + "typescript": "5.9.2", + "@types/bun": "latest" + }, + "lint-staged": { + "*": [ + "bun biome check --write ." + ] + }, + "engines": { + "node": ">=20" + }, + "packageManager": "bun@1.2.20", + "workspaces": [ + "apps/*", + "packages/*" + ], + "dependencies": {} } diff --git a/packages/backend/package.json b/packages/backend/package.json index 00190dc..1c8f794 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,21 +1,21 @@ { - "name": "@better-t-stack/backend", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "convex dev", - "dev:setup": "convex dev --configure --until-success", - "deploy": "convex deploy" - }, - "author": "", - "license": "ISC", - "description": "", - "devDependencies": { - "typescript": "^5.9.2" - }, - "dependencies": { - "@erquhart/convex-oss-stats": "^0.8.1", - "convex": "^1.25.4", - "convex-helpers": "^0.1.104" - } + "name": "@better-t-stack/backend", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "convex dev", + "dev:setup": "convex dev --configure --until-success", + "deploy": "convex deploy" + }, + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "typescript": "^5.9.2" + }, + "dependencies": { + "@erquhart/convex-oss-stats": "^0.8.1", + "convex": "^1.25.4", + "convex-helpers": "^0.1.104" + } }