From 98a0ab9ee6f586425f69b37b24385ef744ad7648 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Sun, 14 Sep 2025 11:29:01 +0530 Subject: [PATCH] chore(cli): add tests (#576) --- apps/cli/package.json | 5 +- apps/cli/src/helpers/core/command-handlers.ts | 2 +- apps/cli/src/helpers/core/create-project.ts | 7 +- apps/cli/src/helpers/core/db-setup.ts | 15 +- .../database-providers/mongodb-atlas-setup.ts | 13 +- .../helpers/database-providers/neon-setup.ts | 12 +- .../prisma-postgres-setup.ts | 12 +- .../database-providers/supabase-setup.ts | 12 +- .../helpers/database-providers/turso-setup.ts | 12 +- apps/cli/src/index.ts | 7 + apps/cli/src/types.ts | 1 + apps/cli/src/utils/biome-formatter.ts | 19 +- apps/cli/src/utils/config-validation.ts | 3 +- apps/cli/test/addons.test.ts | 338 ++ apps/cli/test/api.test.ts | 639 +++ apps/cli/test/auth.test.ts | 490 ++ apps/cli/test/backend-runtime.test.ts | 393 ++ apps/cli/test/basic-configurations.test.ts | 176 + apps/cli/test/benchmark.test.ts | 524 +++ apps/cli/test/cli.smoke.test.ts | 4020 ----------------- apps/cli/test/database-orm.test.ts | 237 + apps/cli/test/database-setup.test.ts | 506 +++ apps/cli/test/deployment.test.ts | 560 +++ apps/cli/test/examples.test.ts | 444 ++ apps/cli/test/frontend.test.ts | 454 ++ apps/cli/test/index.test.ts | 83 + apps/cli/test/integration.test.ts | 541 +++ apps/cli/test/programmatic-api.test.ts | 561 --- apps/cli/test/test-utils.ts | 245 + 29 files changed, 5720 insertions(+), 4611 deletions(-) create mode 100644 apps/cli/test/addons.test.ts create mode 100644 apps/cli/test/api.test.ts create mode 100644 apps/cli/test/auth.test.ts create mode 100644 apps/cli/test/backend-runtime.test.ts create mode 100644 apps/cli/test/basic-configurations.test.ts create mode 100644 apps/cli/test/benchmark.test.ts delete mode 100644 apps/cli/test/cli.smoke.test.ts create mode 100644 apps/cli/test/database-orm.test.ts create mode 100644 apps/cli/test/database-setup.test.ts create mode 100644 apps/cli/test/deployment.test.ts create mode 100644 apps/cli/test/examples.test.ts create mode 100644 apps/cli/test/frontend.test.ts create mode 100644 apps/cli/test/index.test.ts create mode 100644 apps/cli/test/integration.test.ts delete mode 100644 apps/cli/test/programmatic-api.test.ts create mode 100644 apps/cli/test/test-utils.ts diff --git a/apps/cli/package.json b/apps/cli/package.json index 5bfef9e..da83822 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -52,9 +52,8 @@ "dev": "tsdown --watch", "check-types": "tsc --noEmit", "check": "biome check --write .", - "test": "bun run build && vitest run", + "test": "bun run build && vitest run; rm -rf .smoke || true", "test:ui": "bun run build && vitest --ui", - "test:with-build": "bun run build && WITH_BUILD=1 vitest --ui", "prepublishOnly": "npm run build" }, "exports": { @@ -87,4 +86,4 @@ "typescript": "^5.9.2", "vitest": "^3.2.4" } -} +} \ No newline at end of file diff --git a/apps/cli/src/helpers/core/command-handlers.ts b/apps/cli/src/helpers/core/command-handlers.ts index 095058e..8af9146 100644 --- a/apps/cli/src/helpers/core/command-handlers.ts +++ b/apps/cli/src/helpers/core/command-handlers.ts @@ -174,7 +174,7 @@ export async function createProjectHandler( ); } - await createProject(config); + await createProject(config, { manualDb: input.manualDb }); const reproducibleCommand = generateReproducibleCommand(config); log.success( diff --git a/apps/cli/src/helpers/core/create-project.ts b/apps/cli/src/helpers/core/create-project.ts index 124219c..03d1d42 100644 --- a/apps/cli/src/helpers/core/create-project.ts +++ b/apps/cli/src/helpers/core/create-project.ts @@ -32,7 +32,10 @@ import { setupFrontendTemplates, } from "./template-manager"; -export async function createProject(options: ProjectConfig) { +export async function createProject( + options: ProjectConfig, + cliInput?: { manualDb?: boolean }, +) { const projectDir = options.projectDir; const isConvex = options.backend === "convex"; @@ -58,7 +61,7 @@ export async function createProject(options: ProjectConfig) { if (!isConvex) { await setupBackendDependencies(options); - await setupDatabase(options); + await setupDatabase(options, cliInput); await setupRuntime(options); if (options.examples.length > 0 && options.examples[0] !== "none") { await setupExamples(options); diff --git a/apps/cli/src/helpers/core/db-setup.ts b/apps/cli/src/helpers/core/db-setup.ts index fb0c7a7..adf32cd 100644 --- a/apps/cli/src/helpers/core/db-setup.ts +++ b/apps/cli/src/helpers/core/db-setup.ts @@ -14,7 +14,10 @@ import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup import { setupSupabase } from "../database-providers/supabase-setup"; import { setupTurso } from "../database-providers/turso-setup"; -export async function setupDatabase(config: ProjectConfig) { +export async function setupDatabase( + config: ProjectConfig, + cliInput?: { manualDb?: boolean }, +) { const { database, orm, dbSetup, backend, projectDir } = config; if (backend === "convex" || database === "none") { @@ -113,25 +116,25 @@ export async function setupDatabase(config: ProjectConfig) { if (dbSetup === "docker") { await setupDockerCompose(config); } else if (database === "sqlite" && dbSetup === "turso") { - await setupTurso(config); + await setupTurso(config, cliInput); } else if (database === "sqlite" && dbSetup === "d1") { await setupCloudflareD1(config); } else if (database === "postgres") { if (dbSetup === "prisma-postgres") { - await setupPrismaPostgres(config); + await setupPrismaPostgres(config, cliInput); } else if (dbSetup === "neon") { - await setupNeonPostgres(config); + await setupNeonPostgres(config, cliInput); } else if (dbSetup === "planetscale") { await setupPlanetScale(config); } else if (dbSetup === "supabase") { - await setupSupabase(config); + await setupSupabase(config, cliInput); } } else if (database === "mysql") { if (dbSetup === "planetscale") { await setupPlanetScale(config); } } else if (database === "mongodb" && dbSetup === "mongodb-atlas") { - await setupMongoDBAtlas(config); + await setupMongoDBAtlas(config, cliInput); } } catch (error) { s.stop(pc.red("Failed to set up database")); 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 09693f4..34a5f1f 100644 --- a/apps/cli/src/helpers/database-providers/mongodb-atlas-setup.ts +++ b/apps/cli/src/helpers/database-providers/mongodb-atlas-setup.ts @@ -120,8 +120,12 @@ ${pc.green("MongoDB Atlas Manual Setup Instructions:")} `); } -export async function setupMongoDBAtlas(config: ProjectConfig) { +export async function setupMongoDBAtlas( + config: ProjectConfig, + cliInput?: { manualDb?: boolean }, +) { const { projectDir } = config; + const manualDb = cliInput?.manualDb ?? false; const mainSpinner = spinner(); mainSpinner.start("Setting up MongoDB Atlas..."); @@ -129,6 +133,13 @@ export async function setupMongoDBAtlas(config: ProjectConfig) { try { await fs.ensureDir(serverDir); + if (manualDb) { + mainSpinner.stop("MongoDB Atlas manual setup selected"); + await writeEnvFile(projectDir); + displayManualSetupInstructions(); + return; + } + const mode = await select({ message: "MongoDB Atlas setup: choose mode", options: [ diff --git a/apps/cli/src/helpers/database-providers/neon-setup.ts b/apps/cli/src/helpers/database-providers/neon-setup.ts index acaf0f2..f01c842 100644 --- a/apps/cli/src/helpers/database-providers/neon-setup.ts +++ b/apps/cli/src/helpers/database-providers/neon-setup.ts @@ -154,10 +154,20 @@ function displayManualSetupInstructions() { DATABASE_URL="your_connection_string"`); } -export async function setupNeonPostgres(config: ProjectConfig) { +export async function setupNeonPostgres( + config: ProjectConfig, + cliInput?: { manualDb?: boolean }, +) { const { packageManager, projectDir } = config; + const manualDb = cliInput?.manualDb ?? false; try { + if (manualDb) { + await writeEnvFile(projectDir); + displayManualSetupInstructions(); + return; + } + const mode = await select({ message: "Neon setup: choose mode", options: [ 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 bdba1aa..165d8d2 100644 --- a/apps/cli/src/helpers/database-providers/prisma-postgres-setup.ts +++ b/apps/cli/src/helpers/database-providers/prisma-postgres-setup.ts @@ -212,13 +212,23 @@ async function addPrismaAccelerateExtension(serverDir: string) { } } -export async function setupPrismaPostgres(config: ProjectConfig) { +export async function setupPrismaPostgres( + config: ProjectConfig, + cliInput?: { manualDb?: boolean }, +) { const { packageManager, projectDir, orm } = config; + const manualDb = cliInput?.manualDb ?? false; const serverDir = path.join(projectDir, "apps/server"); try { await fs.ensureDir(serverDir); + if (manualDb) { + await writeEnvFile(projectDir); + displayManualSetupInstructions(); + return; + } + const mode = await select({ message: "Prisma Postgres setup: choose mode", options: [ diff --git a/apps/cli/src/helpers/database-providers/supabase-setup.ts b/apps/cli/src/helpers/database-providers/supabase-setup.ts index 9bcdad0..ce6ee15 100644 --- a/apps/cli/src/helpers/database-providers/supabase-setup.ts +++ b/apps/cli/src/helpers/database-providers/supabase-setup.ts @@ -152,14 +152,24 @@ ${pc.dim(output)}` ); } -export async function setupSupabase(config: ProjectConfig) { +export async function setupSupabase( + config: ProjectConfig, + cliInput?: { manualDb?: boolean }, +) { const { projectDir, packageManager } = config; + const manualDb = cliInput?.manualDb ?? false; const serverDir = path.join(projectDir, "apps", "server"); try { await fs.ensureDir(serverDir); + if (manualDb) { + displayManualSupabaseInstructions(); + await writeSupabaseEnvFile(projectDir, ""); + return; + } + const mode = await select({ message: "Supabase setup: choose mode", options: [ diff --git a/apps/cli/src/helpers/database-providers/turso-setup.ts b/apps/cli/src/helpers/database-providers/turso-setup.ts index c8ce849..99afb85 100644 --- a/apps/cli/src/helpers/database-providers/turso-setup.ts +++ b/apps/cli/src/helpers/database-providers/turso-setup.ts @@ -186,12 +186,22 @@ DATABASE_URL=your_database_url DATABASE_AUTH_TOKEN=your_auth_token`); } -export async function setupTurso(config: ProjectConfig) { +export async function setupTurso( + config: ProjectConfig, + cliInput?: { manualDb?: boolean }, +) { const { orm, projectDir } = config; + const manualDb = cliInput?.manualDb ?? false; const _isDrizzle = orm === "drizzle"; const setupSpinner = spinner(); try { + if (manualDb) { + await writeEnvFile(projectDir); + displayManualSetupInstructions(); + return; + } + const mode = await select({ message: "Turso setup: choose mode", options: [ diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 95ade3e..7149a42 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -99,6 +99,13 @@ export const router = t.router({ .optional() .default(false) .describe("Disable analytics"), + manualDb: z + .boolean() + .optional() + .default(false) + .describe( + "Skip automatic/manual database setup prompt and use manual setup", + ), }), ]), ) diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 6204368..ce3bce8 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -147,6 +147,7 @@ export type CreateInput = { directoryConflict?: DirectoryConflict; renderTitle?: boolean; disableAnalytics?: boolean; + manualDb?: boolean; }; export type AddInput = { diff --git a/apps/cli/src/utils/biome-formatter.ts b/apps/cli/src/utils/biome-formatter.ts index 5866743..30eebee 100644 --- a/apps/cli/src/utils/biome-formatter.ts +++ b/apps/cli/src/utils/biome-formatter.ts @@ -2,16 +2,11 @@ import path from "node:path"; import { Biome } from "@biomejs/js-api/nodejs"; import consola from "consola"; -let biome: Biome | null = null; -let projectKey: number | null = null; - -async function initializeBiome() { - if (biome && projectKey !== null) return { biome, projectKey }; - +function initializeBiome() { try { - biome = new Biome(); + const biome = new Biome(); const result = biome.openProject("./"); - projectKey = result.projectKey; + const projectKey = result.projectKey; biome.applyConfiguration(projectKey, { formatter: { @@ -61,18 +56,18 @@ function shouldSkipFile(filePath: string) { return skipPatterns.some((pattern) => basename.includes(pattern)); } -export async function formatFileWithBiome(filePath: string, content: string) { +export function formatFileWithBiome(filePath: string, content: string) { if (!isSupportedFile(filePath) || shouldSkipFile(filePath)) { return null; } try { - const biomeResult = await initializeBiome(); + const biomeResult = initializeBiome(); if (!biomeResult) return null; - const { biome: biomeInstance, projectKey: key } = biomeResult; + const { biome, projectKey } = biomeResult; - const result = biomeInstance.formatContent(key, content, { + const result = biome.formatContent(projectKey, content, { filePath: path.basename(filePath), }); diff --git a/apps/cli/src/utils/config-validation.ts b/apps/cli/src/utils/config-validation.ts index ad5121a..9dd3da4 100644 --- a/apps/cli/src/utils/config-validation.ts +++ b/apps/cli/src/utils/config-validation.ts @@ -380,7 +380,8 @@ export function validateApiConstraints( if ( options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && - options.backend !== "convex" + options.backend !== "convex" && + options.backend !== "none" ) { exitWithError( "Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.", diff --git a/apps/cli/test/addons.test.ts b/apps/cli/test/addons.test.ts new file mode 100644 index 0000000..faa61de --- /dev/null +++ b/apps/cli/test/addons.test.ts @@ -0,0 +1,338 @@ +import { describe, it } from "vitest"; +import type { Addons, Frontend } from "../src"; +import { + expectError, + expectSuccess, + runTRPCTest, + type TestConfig, +} from "./test-utils"; + +describe("Addon Configurations", () => { + describe("Universal Addons (no frontend restrictions)", () => { + const universalAddons = ["biome", "husky", "turborepo", "oxlint"]; + + for (const addon of universalAddons) { + it(`should work with ${addon} addon on any frontend`, async () => { + const result = await runTRPCTest({ + projectName: `${addon}-universal`, + addons: [addon as Addons], + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + } + }); + + describe("Frontend-Specific Addons", () => { + describe("PWA Addon", () => { + const pwaCompatibleFrontends = [ + "tanstack-router", + "react-router", + "solid", + "next", + ]; + + for (const frontend of pwaCompatibleFrontends) { + it(`should work with PWA + ${frontend}`, async () => { + const config: TestConfig = { + projectName: `pwa-${frontend}`, + addons: ["pwa"], + frontend: [frontend as Frontend], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }; + + // Handle special frontend requirements + if (frontend === "solid") { + config.api = "orpc"; // tRPC not supported with solid + } else { + config.api = "trpc"; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + }); + } + + const pwaIncompatibleFrontends = [ + "nuxt", + "svelte", + "native-nativewind", + "native-unistyles", + ]; + + for (const frontend of pwaIncompatibleFrontends) { + it(`should fail with PWA + ${frontend}`, async () => { + const config: TestConfig = { + projectName: `pwa-${frontend}-fail`, + addons: ["pwa"], + frontend: [frontend as Frontend], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }; + + if (["nuxt", "svelte"].includes(frontend)) { + config.api = "orpc"; + } else { + config.api = "trpc"; + } + + const result = await runTRPCTest(config); + expectError( + result, + "pwa addon requires one of these frontends: tanstack-router, react-router, solid, next", + ); + }); + } + }); + + describe("Tauri Addon", () => { + const tauriCompatibleFrontends = [ + "tanstack-router", + "react-router", + "nuxt", + "svelte", + "solid", + "next", + ]; + + for (const frontend of tauriCompatibleFrontends) { + it(`should work with Tauri + ${frontend}`, async () => { + const config: TestConfig = { + projectName: `tauri-${frontend}`, + addons: ["tauri"], + frontend: [frontend as Frontend], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }; + + if (["nuxt", "svelte", "solid"].includes(frontend)) { + config.api = "orpc"; + } else { + config.api = "trpc"; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + }); + } + + const tauriIncompatibleFrontends = [ + "native-nativewind", + "native-unistyles", + ]; + + for (const frontend of tauriIncompatibleFrontends) { + it(`should fail with Tauri + ${frontend}`, async () => { + const result = await runTRPCTest({ + projectName: `tauri-${frontend}-fail`, + addons: ["tauri"], + frontend: [frontend as Frontend], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "tauri addon requires one of these frontends"); + }); + } + }); + }); + + describe("Multiple Addons", () => { + it("should work with multiple compatible addons", async () => { + const result = await runTRPCTest({ + projectName: "multiple-addons", + addons: ["biome", "husky", "turborepo", "pwa"], + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with incompatible addon combination", async () => { + const result = await runTRPCTest({ + projectName: "incompatible-addons-fail", + addons: ["pwa"], // PWA not compatible with nuxt + frontend: ["nuxt"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "orpc", + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "pwa addon requires one of these frontends"); + }); + + it("should deduplicate addons", async () => { + const result = await runTRPCTest({ + projectName: "duplicate-addons", + addons: ["biome", "biome", "turborepo"], // Duplicate biome + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + }); + + describe("Addons with None Option", () => { + it("should work with addons none", async () => { + const result = await runTRPCTest({ + projectName: "no-addons", + addons: ["none"], + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with none + other addons", async () => { + const result = await runTRPCTest({ + projectName: "none-with-other-addons-fail", + addons: ["none", "biome"], + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "Cannot combine 'none' with other addons"); + }); + }); + + describe("All Available Addons", () => { + const testableAddons = [ + "pwa", + "tauri", + "biome", + "husky", + "turborepo", + "oxlint", + // Note: starlight, ultracite, ruler, fumadocs are prompt-controlled only + ]; + + for (const addon of testableAddons) { + it(`should work with ${addon} addon in appropriate setup`, async () => { + const config: TestConfig = { + projectName: `test-${addon}`, + addons: [addon as Addons], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }; + + // Choose compatible frontend for each addon + if (["pwa"].includes(addon)) { + config.frontend = ["tanstack-router"]; // PWA compatible + } else if (["tauri"].includes(addon)) { + config.frontend = ["tanstack-router"]; // Tauri compatible + } else { + config.frontend = ["tanstack-router"]; // Universal addons + } + + const result = await runTRPCTest(config); + expectSuccess(result); + }); + } + }); +}); diff --git a/apps/cli/test/api.test.ts b/apps/cli/test/api.test.ts new file mode 100644 index 0000000..d6a4313 --- /dev/null +++ b/apps/cli/test/api.test.ts @@ -0,0 +1,639 @@ +import { describe, it } from "vitest"; +import type { + API, + Backend, + Database, + Examples, + Frontend, + ORM, + Runtime, +} from "../src/types"; +import { + API_TYPES, + expectError, + expectSuccess, + runTRPCTest, + type TestConfig, +} from "./test-utils"; + +describe("API Configurations", () => { + describe("tRPC API", () => { + it("should work with tRPC + React frontends", async () => { + const reactFrontends = [ + "tanstack-router", + "react-router", + "tanstack-start", + "next", + ]; + + for (const frontend of reactFrontends) { + const result = await runTRPCTest({ + projectName: `trpc-${frontend}`, + api: "trpc", + frontend: [frontend as Frontend], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + } + }); + + it("should work with tRPC + native frontends", async () => { + const nativeFrontends = ["native-nativewind", "native-unistyles"]; + + for (const frontend of nativeFrontends) { + const result = await runTRPCTest({ + projectName: `trpc-${frontend}`, + api: "trpc", + frontend: [frontend as Frontend], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + } + }); + + it("should fail with tRPC + Nuxt", async () => { + const result = await runTRPCTest({ + projectName: "trpc-nuxt-fail", + api: "trpc", + frontend: ["nuxt"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "tRPC API is not supported with 'nuxt' frontend"); + }); + + it("should fail with tRPC + Svelte", async () => { + const result = await runTRPCTest({ + projectName: "trpc-svelte-fail", + api: "trpc", + frontend: ["svelte"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "tRPC API is not supported with 'svelte' frontend"); + }); + + it("should fail with tRPC + Solid", async () => { + const result = await runTRPCTest({ + projectName: "trpc-solid-fail", + api: "trpc", + frontend: ["solid"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "tRPC API is not supported with 'solid' frontend"); + }); + + it("should work with tRPC + all compatible backends", async () => { + const backends = ["hono", "express", "fastify", "next", "elysia"]; + + for (const backend of backends) { + const config: TestConfig = { + projectName: `trpc-${backend}`, + api: "trpc", + backend: backend as Backend, + frontend: ["tanstack-router"], + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }; + + // Set appropriate runtime + if (backend === "elysia") { + config.runtime = "bun"; + } else { + config.runtime = "bun"; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + } + }); + }); + + describe("oRPC API", () => { + it("should work with oRPC + all frontends", async () => { + const frontends = [ + "tanstack-router", + "react-router", + "tanstack-start", + "next", + "nuxt", + "svelte", + "solid", + "native-nativewind", + "native-unistyles", + ]; + + for (const frontend of frontends) { + const result = await runTRPCTest({ + projectName: `orpc-${frontend}`, + api: "orpc", + frontend: [frontend as Frontend], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + } + }); + + it("should work with oRPC + all compatible backends", async () => { + const backends = ["hono", "express", "fastify", "next", "elysia"]; + + for (const backend of backends) { + const config: TestConfig = { + projectName: `orpc-${backend}`, + api: "orpc", + backend: backend as Backend, + frontend: ["tanstack-router"], + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }; + + // Set appropriate runtime + if (backend === "elysia") { + config.runtime = "bun"; + } else { + config.runtime = "bun"; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + } + }); + }); + + describe("No API", () => { + it("should work with API none + basic setup", async () => { + const result = await runTRPCTest({ + projectName: "no-api", + api: "none", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with API none + frontend only", async () => { + const result = await runTRPCTest({ + projectName: "no-api-frontend-only", + api: "none", + frontend: ["tanstack-router"], + backend: "none", + runtime: "none", + database: "none", + orm: "none", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with API none + convex", async () => { + const result = await runTRPCTest({ + projectName: "no-api-convex", + api: "none", + frontend: ["tanstack-router"], + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + auth: "clerk", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with API none + examples (non-convex backend)", async () => { + const result = await runTRPCTest({ + projectName: "no-api-examples-fail", + api: "none", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError( + result, + "Cannot use '--examples' when '--api' is set to 'none'", + ); + }); + + it("should work with API none + examples + convex backend", async () => { + const result = await runTRPCTest({ + projectName: "no-api-examples-convex", + api: "none", + frontend: ["tanstack-router"], + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + auth: "clerk", + addons: ["none"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + }); + + describe("API with Different Database Combinations", () => { + const apiDatabaseCombinations = [ + { api: "trpc", database: "sqlite", orm: "drizzle" }, + { api: "trpc", database: "postgres", orm: "drizzle" }, + { api: "trpc", database: "mysql", orm: "prisma" }, + { api: "trpc", database: "mongodb", orm: "mongoose" }, + { api: "orpc", database: "sqlite", orm: "drizzle" }, + { api: "orpc", database: "postgres", orm: "prisma" }, + { api: "orpc", database: "mysql", orm: "drizzle" }, + { api: "orpc", database: "mongodb", orm: "prisma" }, + ]; + + for (const { api, database, orm } of apiDatabaseCombinations) { + it(`should work with ${api} + ${database} + ${orm}`, async () => { + const result = await runTRPCTest({ + projectName: `${api}-${database}-${orm}`, + api: api as API, + database: database as Database, + orm: orm as ORM, + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + } + }); + + describe("API with Authentication", () => { + it("should work with tRPC + better-auth", async () => { + const result = await runTRPCTest({ + projectName: "trpc-better-auth", + api: "trpc", + auth: "better-auth", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with oRPC + better-auth", async () => { + const result = await runTRPCTest({ + projectName: "orpc-better-auth", + api: "orpc", + auth: "better-auth", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with API none + convex + clerk", async () => { + const result = await runTRPCTest({ + projectName: "no-api-convex-clerk", + api: "none", + auth: "clerk", + frontend: ["tanstack-router"], + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + }); + + describe("API with Examples", () => { + it("should work with tRPC + todo example", async () => { + const result = await runTRPCTest({ + projectName: "trpc-todo", + api: "trpc", + examples: ["todo"], + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with oRPC + AI example", async () => { + const result = await runTRPCTest({ + projectName: "orpc-ai", + api: "orpc", + examples: ["ai"], + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with both APIs + both examples", async () => { + const apiExampleCombinations = [ + { api: "trpc", examples: ["todo", "ai"] }, + { api: "orpc", examples: ["todo", "ai"] }, + ]; + + for (const { api, examples } of apiExampleCombinations) { + const result = await runTRPCTest({ + projectName: `${api}-both-examples`, + api: api as API, + examples: examples as Examples[], + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + } + }); + }); + + describe("All API Types", () => { + for (const api of API_TYPES) { + it(`should work with ${api} in appropriate setup`, async () => { + const config: TestConfig = { + projectName: `test-${api}`, + api, + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }; + + // Set appropriate setup for each API type + if (api === "none") { + config.frontend = ["tanstack-router"]; + config.backend = "none"; + config.runtime = "none"; + config.database = "none"; + config.orm = "none"; + config.auth = "none"; + } else { + config.frontend = ["tanstack-router"]; + config.backend = "hono"; + config.runtime = "bun"; + config.database = "sqlite"; + config.orm = "drizzle"; + config.auth = "none"; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + }); + } + }); + + describe("API Edge Cases", () => { + it("should handle API with complex frontend combinations", async () => { + const result = await runTRPCTest({ + projectName: "api-complex-frontend", + api: "trpc", + frontend: ["tanstack-router", "native-nativewind"], // Web + Native + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should handle API with workers runtime", async () => { + const result = await runTRPCTest({ + projectName: "api-workers", + api: "trpc", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "workers", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "wrangler", // Required for workers + install: false, + }); + + expectSuccess(result); + }); + + it("should handle API constraints with different runtimes", async () => { + const runtimeApiCombinations = [ + { runtime: "bun", api: "trpc" }, + { runtime: "node", api: "orpc" }, + { runtime: "workers", api: "trpc" }, + ]; + + for (const { runtime, api } of runtimeApiCombinations) { + const config: TestConfig = { + projectName: `${runtime}-${api}`, + api: api as API, + runtime: runtime as Runtime, + frontend: ["tanstack-router"], + backend: "hono", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }; + + // Handle workers runtime requirements + if (runtime === "workers") { + config.serverDeploy = "wrangler"; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + } + }); + }); +}); diff --git a/apps/cli/test/auth.test.ts b/apps/cli/test/auth.test.ts new file mode 100644 index 0000000..64e2648 --- /dev/null +++ b/apps/cli/test/auth.test.ts @@ -0,0 +1,490 @@ +import { describe, it } from "vitest"; +import type { Backend, Database, Frontend, ORM } from "../src/types"; +import { + AUTH_PROVIDERS, + expectError, + expectSuccess, + runTRPCTest, + type TestConfig, +} from "./test-utils"; + +describe("Authentication Configurations", () => { + describe("Better-Auth Provider", () => { + it("should work with better-auth + database", async () => { + const result = await runTRPCTest({ + projectName: "better-auth-db", + auth: "better-auth", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with better-auth + different databases", async () => { + const databases = ["sqlite", "postgres", "mysql"]; + + for (const database of databases) { + const result = await runTRPCTest({ + projectName: `better-auth-${database}`, + auth: "better-auth", + backend: "hono", + runtime: "bun", + database: database as Database, + orm: "drizzle", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + } + }); + + it("should work with better-auth + mongodb + mongoose", async () => { + const result = await runTRPCTest({ + projectName: "better-auth-mongodb", + auth: "better-auth", + backend: "hono", + runtime: "bun", + database: "mongodb", + orm: "mongoose", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with better-auth + no database (non-convex)", async () => { + const result = await runTRPCTest({ + projectName: "better-auth-no-db-fail", + auth: "better-auth", + backend: "hono", + runtime: "bun", + database: "none", + orm: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "Authentication requires a database"); + }); + + it("should fail with better-auth + convex backend", async () => { + const result = await runTRPCTest({ + projectName: "better-auth-convex-fail", + auth: "better-auth", + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + api: "none", + frontend: ["tanstack-router"], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "Better-Auth is not compatible with Convex backend"); + }); + + it("should work with better-auth + all compatible frontends", async () => { + const compatibleFrontends = [ + "tanstack-router", + "react-router", + "tanstack-start", + "next", + "nuxt", + "svelte", + "solid", + "native-nativewind", + "native-unistyles", + ]; + + for (const frontend of compatibleFrontends) { + const config: TestConfig = { + projectName: `better-auth-${frontend}`, + auth: "better-auth", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + frontend: [frontend as Frontend], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }; + + // Handle API compatibility + if (["nuxt", "svelte", "solid"].includes(frontend)) { + config.api = "orpc"; + } else { + config.api = "trpc"; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + } + }); + }); + + describe("Clerk Provider", () => { + it("should work with clerk + convex", async () => { + const result = await runTRPCTest({ + projectName: "clerk-convex", + auth: "clerk", + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + api: "none", + frontend: ["tanstack-router"], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with clerk + non-convex backend", async () => { + const result = await runTRPCTest({ + projectName: "clerk-non-convex-fail", + auth: "clerk", + backend: "hono", + runtime: "bun", + database: "sqlite", + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + addons: ["turborepo"], + orm: "drizzle", + api: "trpc", + frontend: ["tanstack-router"], + expectError: true, + }); + + expectError( + result, + "Clerk authentication is only supported with the Convex backend", + ); + }); + + it("should work with clerk + compatible frontends", async () => { + const compatibleFrontends = [ + "tanstack-router", + "react-router", + "tanstack-start", + "next", + "native-nativewind", + "native-unistyles", + ]; + + for (const frontend of compatibleFrontends) { + const result = await runTRPCTest({ + projectName: `clerk-${frontend}`, + auth: "clerk", + backend: "convex", + runtime: "none", + database: "none", + webDeploy: "none", + serverDeploy: "none", + addons: ["turborepo"], + dbSetup: "none", + examples: ["todo"], + orm: "none", + api: "none", + frontend: [frontend as Frontend], + install: false, + }); + + expectSuccess(result); + } + }); + + it("should fail with clerk + incompatible frontends", async () => { + const incompatibleFrontends = ["nuxt", "svelte", "solid"]; + + for (const frontend of incompatibleFrontends) { + const result = await runTRPCTest({ + projectName: `clerk-${frontend}-fail`, + auth: "clerk", + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + api: "none", + frontend: [frontend as Frontend], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "Clerk authentication is not compatible"); + } + }); + }); + + describe("No Authentication", () => { + it("should work with auth none", async () => { + const result = await runTRPCTest({ + projectName: "no-auth", + auth: "none", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with auth none + no database", async () => { + const result = await runTRPCTest({ + projectName: "no-auth-no-db", + auth: "none", + backend: "none", + runtime: "none", + database: "none", + orm: "none", + api: "none", + frontend: ["tanstack-router"], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with auth none + convex", async () => { + const result = await runTRPCTest({ + projectName: "no-auth-convex", + auth: "none", + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + api: "none", + frontend: ["tanstack-router"], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + }); + + describe("Authentication with Different Backends", () => { + const backends = ["hono", "express", "fastify", "next", "elysia"]; + + for (const backend of backends) { + it(`should work with better-auth + ${backend}`, async () => { + const config: TestConfig = { + projectName: `better-auth-${backend}`, + auth: "better-auth", + backend: backend as Backend, + database: "sqlite", + orm: "drizzle", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }; + + // Set appropriate runtime + if (backend === "elysia") { + config.runtime = "bun"; + } else { + config.runtime = "bun"; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + }); + } + }); + + describe("Authentication with Different ORMs", () => { + const ormCombinations = [ + { database: "sqlite", orm: "drizzle" }, + { database: "sqlite", orm: "prisma" }, + { database: "postgres", orm: "drizzle" }, + { database: "postgres", orm: "prisma" }, + { database: "mysql", orm: "drizzle" }, + { database: "mysql", orm: "prisma" }, + { database: "mongodb", orm: "mongoose" }, + { database: "mongodb", orm: "prisma" }, + ]; + + for (const { database, orm } of ormCombinations) { + it(`should work with better-auth + ${database} + ${orm}`, async () => { + const result = await runTRPCTest({ + projectName: `better-auth-${database}-${orm}`, + auth: "better-auth", + backend: "hono", + runtime: "bun", + database: database as Database, + orm: orm as ORM, + api: "trpc", + frontend: ["tanstack-router"], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + } + }); + + describe("All Auth Providers", () => { + for (const auth of AUTH_PROVIDERS) { + it(`should work with ${auth} in appropriate setup`, async () => { + const config: TestConfig = { + projectName: `test-${auth}`, + auth, + frontend: ["tanstack-router"], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }; + + // Set appropriate setup for each auth provider + if (auth === "clerk") { + config.backend = "convex"; + config.runtime = "none"; + config.database = "none"; + config.orm = "none"; + config.api = "none"; + } else if (auth === "better-auth") { + config.backend = "hono"; + config.runtime = "bun"; + config.database = "sqlite"; + config.orm = "drizzle"; + config.api = "trpc"; + } else { + // none + config.backend = "hono"; + config.runtime = "bun"; + config.database = "sqlite"; + config.orm = "drizzle"; + config.api = "trpc"; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + }); + } + }); + + describe("Auth Edge Cases", () => { + it("should handle auth with complex frontend combinations", async () => { + const result = await runTRPCTest({ + projectName: "auth-web-native-combo", + auth: "better-auth", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + api: "trpc", + frontend: ["tanstack-router", "native-nativewind"], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should handle auth constraints with workers runtime", async () => { + const result = await runTRPCTest({ + projectName: "auth-workers", + auth: "better-auth", + backend: "hono", + runtime: "workers", + database: "sqlite", + orm: "drizzle", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "wrangler", + install: false, + }); + + expectSuccess(result); + }); + }); +}); diff --git a/apps/cli/test/backend-runtime.test.ts b/apps/cli/test/backend-runtime.test.ts new file mode 100644 index 0000000..2889fd4 --- /dev/null +++ b/apps/cli/test/backend-runtime.test.ts @@ -0,0 +1,393 @@ +import { describe, it } from "vitest"; +import type { Backend, Runtime } from "../src/types"; +import { + expectError, + expectSuccess, + runTRPCTest, + type TestConfig, +} from "./test-utils"; + +describe("Backend and Runtime Combinations", () => { + describe("Valid Backend-Runtime Combinations", () => { + const validCombinations = [ + // Standard backend-runtime combinations + { backend: "hono" as const, runtime: "bun" as const }, + { backend: "hono" as const, runtime: "node" as const }, + { backend: "hono" as const, runtime: "workers" as const }, + + { backend: "express" as const, runtime: "bun" as const }, + { backend: "express" as const, runtime: "node" as const }, + + { backend: "fastify" as const, runtime: "bun" as const }, + { backend: "fastify" as const, runtime: "node" as const }, + + { backend: "elysia" as const, runtime: "bun" as const }, + + { backend: "next" as const, runtime: "bun" as const }, + { backend: "next" as const, runtime: "node" as const }, + + // Special cases + { backend: "convex" as const, runtime: "none" as const }, + { backend: "none" as const, runtime: "none" as const }, + ]; + + for (const { backend, runtime } of validCombinations) { + it(`should work with ${backend} + ${runtime}`, async () => { + const config: TestConfig = { + projectName: `${backend}-${runtime}`, + backend, + runtime, + frontend: ["tanstack-router"], + webDeploy: "none", + serverDeploy: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + install: false, + }; + + // Set appropriate defaults based on backend + if (backend === "convex") { + config.database = "none"; + config.orm = "none"; + config.auth = "clerk"; + config.api = "none"; + } else if (backend === "none") { + config.database = "none"; + config.orm = "none"; + config.auth = "none"; + config.api = "none"; + } else { + config.database = "sqlite"; + config.orm = "drizzle"; + config.auth = "none"; + config.api = "trpc"; + } + + // Set server deployment for workers runtime + if (runtime === "workers") { + config.serverDeploy = "wrangler"; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + }); + } + }); + + describe("Invalid Backend-Runtime Combinations", () => { + const invalidCombinations = [ + // Workers runtime only works with Hono + { + backend: "express" as const, + runtime: "workers" as const, + error: + "Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend", + }, + { + backend: "fastify", + runtime: "workers", + error: + "Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend", + }, + { + backend: "next", + runtime: "workers", + error: + "Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend", + }, + { + backend: "elysia", + runtime: "workers", + error: + "Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend", + }, + + // Convex backend requires runtime none + { + backend: "convex", + runtime: "bun", + error: "Convex backend requires '--runtime none'", + }, + { + backend: "convex", + runtime: "node", + error: "Convex backend requires '--runtime none'", + }, + { + backend: "convex", + runtime: "workers", + error: "Convex backend requires '--runtime none'", + }, + + // Backend none requires runtime none + { + backend: "none", + runtime: "bun", + error: "Backend 'none' requires '--runtime none'", + }, + { + backend: "none", + runtime: "node", + error: "Backend 'none' requires '--runtime none'", + }, + { + backend: "none", + runtime: "workers", + error: "Backend 'none' requires '--runtime none'", + }, + + // Runtime none only works with convex or none backend + { + backend: "hono", + runtime: "none", + error: + "'--runtime none' is only supported with '--backend convex' or '--backend none'", + }, + { + backend: "express", + runtime: "none", + error: + "'--runtime none' is only supported with '--backend convex' or '--backend none'", + }, + ]; + + for (const { backend, runtime, error } of invalidCombinations) { + it(`should fail with ${backend} + ${runtime}`, async () => { + const config: TestConfig = { + projectName: `invalid-${backend}-${runtime}`, + backend: backend as Backend, + runtime: runtime as Runtime, + frontend: ["tanstack-router"], + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }; + + // Set appropriate defaults based on backend + if (backend === "convex") { + config.database = "none"; + config.orm = "none"; + config.auth = "clerk"; + config.api = "none"; + } else if (backend === "none") { + config.database = "none"; + config.orm = "none"; + config.auth = "none"; + config.api = "none"; + } else { + config.database = "sqlite"; + config.orm = "drizzle"; + config.auth = "none"; + config.api = "trpc"; + } + + const result = await runTRPCTest(config); + expectError(result, error); + }); + } + }); + + describe("Convex Backend Constraints", () => { + it("should enforce all convex constraints", async () => { + const result = await runTRPCTest({ + projectName: "convex-app", + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + auth: "clerk", + api: "none", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail convex with better-auth", async () => { + const result = await runTRPCTest({ + projectName: "convex-better-auth", + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + auth: "better-auth", + api: "none", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "Better-Auth is not compatible with Convex backend"); + }); + + it("should fail convex with database", async () => { + const result = await runTRPCTest({ + projectName: "convex-with-db", + backend: "convex", + runtime: "none", + database: "postgres", + orm: "drizzle", + auth: "clerk", + api: "none", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "Convex backend requires '--database none'"); + }); + }); + + describe("Workers Runtime Constraints", () => { + it("should work with workers + hono + compatible database", async () => { + const result = await runTRPCTest({ + projectName: "workers-compatible", + backend: "hono", + runtime: "workers", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "wrangler", // Workers requires server deployment + install: false, + }); + + expectSuccess(result); + }); + + it("should fail workers with mongodb", async () => { + const result = await runTRPCTest({ + projectName: "workers-mongodb", + backend: "hono", + runtime: "workers", + database: "mongodb", + orm: "prisma", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError( + result, + "Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database", + ); + }); + + it("should fail workers without server deployment", async () => { + const result = await runTRPCTest({ + projectName: "workers-no-deploy", + backend: "hono", + runtime: "workers", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError( + result, + "Cloudflare Workers runtime requires a server deployment", + ); + }); + }); + + describe("All Backend Types", () => { + const backends = [ + "hono", + "express", + "fastify", + "next", + "elysia", + "convex", + "none", + ] as const; + + for (const backend of backends) { + it(`should work with appropriate defaults for ${backend}`, async () => { + const config: TestConfig = { + projectName: `test-${backend}`, + backend: backend as Backend, + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }; + + // Set appropriate defaults for each backend + switch (backend) { + case "convex": + config.runtime = "none"; + config.database = "none"; + config.orm = "none"; + config.auth = "clerk"; + config.api = "none"; + break; + case "none": + config.runtime = "none"; + config.database = "none"; + config.orm = "none"; + config.auth = "none"; + config.api = "none"; + break; + case "elysia": + config.runtime = "bun"; + config.database = "sqlite"; + config.orm = "drizzle"; + config.auth = "none"; + config.api = "trpc"; + break; + default: + config.runtime = "bun"; + config.database = "sqlite"; + config.orm = "drizzle"; + config.auth = "none"; + config.api = "trpc"; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + }); + } + }); +}); diff --git a/apps/cli/test/basic-configurations.test.ts b/apps/cli/test/basic-configurations.test.ts new file mode 100644 index 0000000..b4b4ace --- /dev/null +++ b/apps/cli/test/basic-configurations.test.ts @@ -0,0 +1,176 @@ +import { describe, expect, it } from "vitest"; +import { + expectError, + expectSuccess, + PACKAGE_MANAGERS, + runTRPCTest, +} from "./test-utils"; + +describe("Basic Configurations", () => { + describe("Default Configuration", () => { + it("should create project with --yes flag (default config)", async () => { + const result = await runTRPCTest({ + projectName: "default-app", + yes: true, + install: false, + }); + + expectSuccess(result); + expect(result.result?.projectConfig.projectName).toBe("default-app"); + }); + + it("should create project with explicit default values", async () => { + const result = await runTRPCTest({ + projectName: "explicit-defaults", + database: "sqlite", + orm: "drizzle", + backend: "hono", + runtime: "bun", + frontend: ["tanstack-router"], + auth: "better-auth", + api: "trpc", + addons: ["turborepo"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, // Skip installation for faster tests + }); + + expectSuccess(result); + expect(result.result?.projectConfig.projectName).toBe( + "explicit-defaults", + ); + }); + }); + + describe("Package Managers", () => { + for (const packageManager of PACKAGE_MANAGERS) { + it(`should work with ${packageManager}`, async () => { + const result = await runTRPCTest({ + projectName: `${packageManager}-app`, + packageManager, + yes: true, + install: false, + }); + + expectSuccess(result); + expect(result.result?.projectConfig.packageManager).toBe( + packageManager, + ); + }); + } + }); + + describe("Git Options", () => { + it("should work with git enabled", async () => { + const result = await runTRPCTest({ + projectName: "git-enabled", + yes: true, + git: true, + install: false, + }); + + expectSuccess(result); + expect(result.result?.projectConfig.git).toBe(true); + }); + + it("should work with git disabled", async () => { + const result = await runTRPCTest({ + projectName: "git-disabled", + yes: true, + git: false, + install: false, + }); + + expectSuccess(result); + expect(result.result?.projectConfig.git).toBe(false); + }); + }); + + describe("Installation Options", () => { + it("should work with install enabled", async () => { + const result = await runTRPCTest({ + projectName: "install-enabled", + yes: true, + install: true, + }); + + expectSuccess(result); + expect(result.result?.projectConfig.install).toBe(true); + }); + + it("should work with install disabled", async () => { + const result = await runTRPCTest({ + projectName: "install-disabled", + yes: true, + install: false, + }); + + expectSuccess(result); + expect(result.result?.projectConfig.install).toBe(false); + }); + }); + + describe("YOLO Mode", () => { + it("should bypass validations with --yolo flag", async () => { + // This would normally fail validation but should pass with yolo + const result = await runTRPCTest({ + projectName: "yolo-app", + yolo: true, + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + api: "trpc", + database: "mongodb", + orm: "drizzle", // Incompatible combination + auth: "better-auth", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + expect(result.result?.projectConfig.projectName).toBe("yolo-app"); + }); + }); + + describe("Error Handling", () => { + it("should fail with invalid project name", async () => { + const result = await runTRPCTest({ + projectName: "", + expectError: true, + }); + + expectError(result, "invalid characters"); + }); + + it("should fail when combining --yes with configuration flags", async () => { + const result = await runTRPCTest({ + projectName: "yes-with-flags", + yes: true, // Explicitly set yes flag + database: "postgres", + orm: "drizzle", + backend: "hono", + runtime: "bun", + frontend: ["tanstack-router"], + auth: "better-auth", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError( + result, + "Cannot combine --yes with core stack configuration flags", + ); + }); + }); +}); diff --git a/apps/cli/test/benchmark.test.ts b/apps/cli/test/benchmark.test.ts new file mode 100644 index 0000000..c10166c --- /dev/null +++ b/apps/cli/test/benchmark.test.ts @@ -0,0 +1,524 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { + cleanupSmokeDirectory, + expectSuccess, + runTRPCTest, + type TestConfig, +} from "./test-utils"; + +describe("CLI Performance Benchmarks", () => { + beforeAll(async () => { + await cleanupSmokeDirectory(); + }); + + afterAll(async () => { + await cleanupSmokeDirectory(); + }); + + describe("Basic Project Creation Benchmarks", () => { + it("should benchmark default configuration creation", async () => { + const startTime = performance.now(); + + const result = await runTRPCTest({ + projectName: "benchmark-default", + yes: true, + install: false, // Skip install for faster benchmarking + }); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(10000); // Should complete within 10 seconds + + console.log(`✅ Default configuration: ${duration.toFixed(2)}ms`); + }); + + it("should benchmark minimal configuration creation", async () => { + const startTime = performance.now(); + + const result = await runTRPCTest({ + projectName: "benchmark-minimal", + frontend: ["none"], + backend: "none", + runtime: "none", + database: "none", + orm: "none", + auth: "none", + api: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + serverDeploy: "none", + webDeploy: "none", + install: false, + }); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(8000); + + console.log(`✅ Minimal configuration: ${duration.toFixed(2)}ms`); + }); + + it("should benchmark full-stack configuration creation", async () => { + const startTime = performance.now(); + + const result = await runTRPCTest({ + projectName: "benchmark-fullstack", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "postgres", + orm: "drizzle", + auth: "better-auth", + api: "trpc", + addons: ["turborepo", "biome"], + examples: ["todo"], + dbSetup: "neon", + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + install: false, + }); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(15000); // Should complete within 15 seconds + + console.log(`✅ Full-stack configuration: ${duration.toFixed(2)}ms`); + }); + }); + + describe("Database Setup Benchmarks", () => { + it("should benchmark SQLite with Drizzle setup", async () => { + const startTime = performance.now(); + + const result = await runTRPCTest({ + projectName: "benchmark-sqlite-drizzle", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(12000); + + console.log(`✅ SQLite + Drizzle: ${duration.toFixed(2)}ms`); + }); + + it("should benchmark PostgreSQL with Prisma setup", async () => { + const startTime = performance.now(); + + const result = await runTRPCTest({ + projectName: "benchmark-postgres-prisma", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "postgres", + orm: "prisma", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(12000); + + console.log(`✅ PostgreSQL + Prisma: ${duration.toFixed(2)}ms`); + }); + + it("should benchmark MongoDB with Mongoose setup", async () => { + const startTime = performance.now(); + + const result = await runTRPCTest({ + projectName: "benchmark-mongodb-mongoose", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "mongodb", + orm: "mongoose", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(12000); + + console.log(`✅ MongoDB + Mongoose: ${duration.toFixed(2)}ms`); + }); + }); + + describe("Frontend Framework Benchmarks", () => { + it("should benchmark TanStack Router setup", async () => { + const startTime = performance.now(); + + const result = await runTRPCTest({ + projectName: "benchmark-tanstack-router", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(12000); + + console.log(`✅ TanStack Router: ${duration.toFixed(2)}ms`); + }); + + it("should benchmark Next.js setup", async () => { + const startTime = performance.now(); + + const result = await runTRPCTest({ + projectName: "benchmark-nextjs", + frontend: ["next"], + backend: "next", + runtime: "node", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(12000); + + console.log(`✅ Next.js: ${duration.toFixed(2)}ms`); + }); + + it("should benchmark Nuxt setup", async () => { + const startTime = performance.now(); + + const result = await runTRPCTest({ + projectName: "benchmark-nuxt", + frontend: ["nuxt"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "orpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(12000); + + console.log(`✅ Nuxt: ${duration.toFixed(2)}ms`); + }); + }); + + describe("Backend Framework Benchmarks", () => { + it("should benchmark Hono setup", async () => { + const startTime = performance.now(); + + const result = await runTRPCTest({ + projectName: "benchmark-hono", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(12000); + + console.log(`✅ Hono: ${duration.toFixed(2)}ms`); + }); + + it("should benchmark Express setup", async () => { + const startTime = performance.now(); + + const result = await runTRPCTest({ + projectName: "benchmark-express", + frontend: ["tanstack-router"], + backend: "express", + runtime: "node", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(12000); + + console.log(`✅ Express: ${duration.toFixed(2)}ms`); + }); + + it("should benchmark Convex setup", async () => { + const startTime = performance.now(); + + const result = await runTRPCTest({ + projectName: "benchmark-convex", + frontend: ["tanstack-router"], + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + auth: "none", + api: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(12000); + + console.log(`✅ Convex: ${duration.toFixed(2)}ms`); + }); + }); + + describe("Addon Benchmarks", () => { + it("should benchmark Turborepo addon", async () => { + const startTime = performance.now(); + + const result = await runTRPCTest({ + projectName: "benchmark-turborepo", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["turborepo"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(12000); + + console.log(`✅ Turborepo addon: ${duration.toFixed(2)}ms`); + }); + + it("should benchmark Biome addon", async () => { + const startTime = performance.now(); + + const result = await runTRPCTest({ + projectName: "benchmark-biome", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["biome"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(12000); + + console.log(`✅ Biome addon: ${duration.toFixed(2)}ms`); + }); + + it("should benchmark multiple addons", async () => { + const startTime = performance.now(); + + const result = await runTRPCTest({ + projectName: "benchmark-multiple-addons", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["turborepo", "biome", "husky"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(15000); + + console.log(`✅ Multiple addons: ${duration.toFixed(2)}ms`); + }); + }); + + describe("Performance Regression Tests", () => { + it("should not exceed performance thresholds", async () => { + const configurations = [ + { + name: "Minimal", + config: { + projectName: "perf-minimal", + frontend: ["none"], + backend: "none", + runtime: "none", + database: "none", + orm: "none", + auth: "none", + api: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }, + threshold: 5000, // 5 seconds + }, + { + name: "Default", + config: { + projectName: "perf-default", + yes: true, + install: false, + }, + threshold: 8000, // 8 seconds + }, + { + name: "Complex", + config: { + projectName: "perf-complex", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "postgres", + orm: "prisma", + auth: "better-auth", + api: "trpc", + addons: ["turborepo", "biome"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }, + threshold: 12000, // 12 seconds + }, + ]; + + for (const { name, config, threshold } of configurations) { + const startTime = performance.now(); + + const result = await runTRPCTest(config as TestConfig); + + const endTime = performance.now(); + const duration = endTime - startTime; + + expectSuccess(result); + expect(duration).toBeLessThan(threshold); + + console.log( + `✅ ${name} performance: ${duration.toFixed(2)}ms (threshold: ${threshold}ms)`, + ); + } + }); + }); +}); diff --git a/apps/cli/test/cli.smoke.test.ts b/apps/cli/test/cli.smoke.test.ts deleted file mode 100644 index 565e39a..0000000 --- a/apps/cli/test/cli.smoke.test.ts +++ /dev/null @@ -1,4020 +0,0 @@ -import { join } from "node:path"; -import consola from "consola"; -import { execa } from "execa"; -import { ensureDir, existsSync, readFile, readJson, remove } from "fs-extra"; -import * as JSONC from "jsonc-parser"; -import { FailedToExitError } from "trpc-cli"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { createBtsCli } from "../src/index"; - -async function runCli(argv: string[], cwd: string) { - const previous = process.cwd(); - process.chdir(cwd); - try { - consola.info(`Running CLI command: bts ${argv.join(" ")}`); - - const cli = createBtsCli(); - await cli - .run({ - argv, - logger: { info: () => {}, error: () => {} }, - process: { exit: () => void 0 as never }, - }) - .catch((err) => { - let e: unknown = err; - while (e instanceof FailedToExitError) { - if (e.exitCode === 0) return e.cause; - e = e.cause; - } - throw e; - }); - } finally { - process.chdir(previous); - } -} - -async function createTmpDir(_prefix: string) { - const dir = join(__dirname, "..", ".smoke"); - if (existsSync(dir)) { - await remove(dir); - } - await ensureDir(dir); - return dir; -} - -async function runCliExpectingError(args: string[], cwd: string) { - const previous = process.cwd(); - process.chdir(cwd); - try { - consola.info( - `Running CLI command (expecting error): bts ${args.join(" ")}`, - ); - - const cli = createBtsCli(); - let threw = false; - await cli - .run({ - argv: args, - logger: { info: () => {}, error: () => {} }, - process: { exit: () => void 0 as never }, - }) - .catch((err) => { - threw = true; - let e: unknown = err; - while (e instanceof FailedToExitError) { - if (e.exitCode === 0) throw new Error("Expected failure"); - e = e.cause; - } - }); - expect(threw).toBe(true); - } finally { - process.chdir(previous); - } -} - -async function assertScaffoldedProject(dir: string) { - const pkgJsonPath = join(dir, "package.json"); - expect(existsSync(pkgJsonPath)).toBe(true); - const pkg = await readJson(pkgJsonPath); - expect(typeof pkg.name).toBe("string"); - expect(Array.isArray(pkg.workspaces)).toBe(true); -} - -async function assertProjectStructure( - dir: string, - options: { - hasWeb?: boolean; - hasNative?: boolean; - hasServer?: boolean; - hasConvexBackend?: boolean; - hasTurborepo?: boolean; - hasBiome?: boolean; - hasAuth?: boolean; - hasDatabase?: boolean; - }, -) { - const { - hasWeb = false, - hasNative = false, - hasServer = false, - hasConvexBackend = false, - hasTurborepo = false, - hasBiome = false, - hasAuth = false, - hasDatabase = false, - } = options; - - expect(existsSync(join(dir, "package.json"))).toBe(true); - expect(existsSync(join(dir, ".gitignore"))).toBe(true); - - try { - const pmConfig = (await readBtsConfig(dir)) as { packageManager?: string }; - if (pmConfig && pmConfig.packageManager === "bun") { - expect(existsSync(join(dir, "bunfig.toml"))).toBe(true); - } - } catch {} - - if (hasWeb) { - expect(existsSync(join(dir, "apps", "web", "package.json"))).toBe(true); - const webDir = join(dir, "apps", "web"); - const hasViteConfig = existsSync(join(webDir, "vite.config.ts")); - const hasNextConfig = - existsSync(join(webDir, "next.config.mjs")) || - existsSync(join(webDir, "next.config.js")); - const hasNuxtConfig = existsSync(join(webDir, "nuxt.config.ts")); - const hasSvelteConfig = existsSync(join(webDir, "svelte.config.js")); - const hasTsConfig = existsSync(join(webDir, "tsconfig.json")); - - const hasSrcDir = existsSync(join(webDir, "src")); - const hasAppDir = existsSync(join(webDir, "app")); - const hasPublicDir = existsSync(join(webDir, "public")); - - expect( - hasViteConfig || - hasNextConfig || - hasNuxtConfig || - hasSvelteConfig || - hasTsConfig || - hasSrcDir || - hasAppDir || - hasPublicDir, - ).toBe(true); - - const bts = (await readBtsConfig(dir)) as { - webDeploy?: string; - serverDeploy?: string; - frontend?: string[]; - }; - if (bts.webDeploy === "wrangler") { - expect(existsSync(join(dir, "apps", "web", "wrangler.jsonc"))).toBe(true); - } - - if ( - bts.webDeploy === "alchemy" && - bts.serverDeploy !== "alchemy" && - bts.frontend && - bts.frontend.length > 0 - ) { - const webRunner = join(dir, "apps", "web", "alchemy.run.ts"); - consola.info(`Checking Alchemy web runner at: ${webRunner}`); - expect(existsSync(webRunner)).toBe(true); - } - } - - if (hasNative) { - const nativeDir = join(dir, "apps", "native"); - expect(existsSync(join(nativeDir, "package.json"))).toBe(true); - const hasAppConfig = existsSync(join(nativeDir, "app.json")); - const hasExpoConfig = existsSync(join(nativeDir, "expo")); - const hasSrcDir = existsSync(join(nativeDir, "src")); - const hasMainFile = - existsSync(join(nativeDir, "App.tsx")) || - existsSync(join(nativeDir, "index.tsx")) || - existsSync(join(nativeDir, "index.js")); - expect(hasAppConfig || hasExpoConfig || hasSrcDir || hasMainFile).toBe( - true, - ); - } - - if (hasServer) { - expect(existsSync(join(dir, "apps", "server", "package.json"))).toBe(true); - expect(existsSync(join(dir, "apps", "server", "src", "index.ts"))).toBe( - true, - ); - expect(existsSync(join(dir, "apps", "server", "tsconfig.json"))).toBe(true); - - const bts = (await readBtsConfig(dir)) as { - serverDeploy?: string; - webDeploy?: string; - }; - if (bts.serverDeploy === "wrangler") { - expect(existsSync(join(dir, "apps", "server", "wrangler.jsonc"))).toBe( - true, - ); - } - if (bts.serverDeploy === "alchemy") { - const serverRunner = join(dir, "apps", "server", "alchemy.run.ts"); - const serverEnv = join(dir, "apps", "server", "env.d.ts"); - consola.info(`Checking Alchemy server runner at: ${serverRunner}`); - consola.info(`Checking Alchemy env types at: ${serverEnv}`); - expect(existsSync(serverRunner)).toBe(true); - expect(existsSync(serverEnv)).toBe(true); - } - } - - try { - const btsAll = (await readBtsConfig(dir)) as { - serverDeploy?: string; - webDeploy?: string; - }; - if (btsAll.serverDeploy === "alchemy" && btsAll.webDeploy === "alchemy") { - const rootRunner = join(dir, "alchemy.run.ts"); - const serverEnv = join(dir, "apps", "server", "env.d.ts"); - consola.info(`Checking Alchemy root runner at: ${rootRunner}`); - consola.info(`Checking Alchemy env types at: ${serverEnv}`); - expect(existsSync(rootRunner)).toBe(true); - expect(existsSync(serverEnv)).toBe(true); - } - } catch {} - - if (hasConvexBackend) { - const hasPackagesDir = existsSync(join(dir, "packages")); - const hasConvexRelated = - existsSync(join(dir, "packages", "backend")) || - existsSync(join(dir, "convex")) || - existsSync(join(dir, "convex.config.ts")); - expect(hasPackagesDir || hasConvexRelated).toBe(true); - } - - if (hasTurborepo) { - expect(existsSync(join(dir, "turbo.json"))).toBe(true); - } - - if (hasBiome) { - expect(existsSync(join(dir, "biome.json"))).toBe(true); - } - - if (hasAuth && hasServer) { - expect( - existsSync(join(dir, "apps", "server", "src", "lib", "auth.ts")), - ).toBe(true); - } - - if (hasDatabase && hasServer) { - const serverDir = join(dir, "apps", "server"); - if (existsSync(serverDir)) { - const hasDrizzleConfig = existsSync(join(serverDir, "drizzle.config.ts")); - const hasPrismaSchema = existsSync( - join(serverDir, "prisma", "schema.prisma"), - ); - const hasDbFolder = existsSync(join(serverDir, "src", "db")); - const hasSchemaFile = existsSync(join(serverDir, "src", "schema.ts")); - const hasLibFolder = existsSync(join(serverDir, "src", "lib")); - - const hasRootPrismaDir = existsSync(join(dir, "prisma")); - const hasRootPrismaSchema = existsSync( - join(dir, "prisma", "schema.prisma"), - ); - - expect( - hasDrizzleConfig || - hasPrismaSchema || - hasDbFolder || - hasSchemaFile || - hasLibFolder || - hasRootPrismaDir || - hasRootPrismaSchema, - ).toBe(true); - } - } - - expect(existsSync(join(dir, "bts.jsonc"))).toBe(true); - const btsConfig = await readFile(join(dir, "bts.jsonc"), "utf8"); - expect(btsConfig).toContain("Better-T-Stack configuration"); -} - -async function assertBtsConfig( - dir: string, - expectedConfig: Partial<{ - frontend: string[]; - backend: string; - database: string; - orm: string; - auth: string; - addons: string[]; - examples: string[]; - api: string; - runtime: string; - packageManager: string; - webDeploy: string; - serverDeploy: string; - }>, -) { - const btsConfigPath = join(dir, "bts.jsonc"); - expect(existsSync(btsConfigPath)).toBe(true); - const content = await readFile(btsConfigPath, "utf8"); - - type BtsConfig = { - frontend?: string[]; - backend?: string; - database?: string; - orm?: string; - auth?: string; - addons?: string[]; - examples?: string[]; - api?: string; - runtime?: string; - packageManager?: string; - webDeploy?: string; - serverDeploy?: string; - }; - - const errors: JSONC.ParseError[] = []; - const parsed = JSONC.parse(content, errors, { - allowTrailingComma: true, - disallowComments: false, - }) as BtsConfig | null; - - if (errors.length > 0 || !parsed) { - consola.error("Failed to parse bts.jsonc", errors); - throw new Error("Failed to parse bts.jsonc"); - } - const config = parsed; - - if (expectedConfig.frontend) { - expect(config.frontend).toEqual(expectedConfig.frontend); - } - if (expectedConfig.backend) { - expect(config.backend).toBe(expectedConfig.backend); - } - if (expectedConfig.database) { - expect(config.database).toBe(expectedConfig.database); - } - if (expectedConfig.orm) { - expect(config.orm).toBe(expectedConfig.orm); - } - if (expectedConfig.auth !== undefined) { - expect(config.auth).toBe(expectedConfig.auth); - } - if (expectedConfig.addons) { - expect(config.addons).toEqual(expectedConfig.addons); - } - if (expectedConfig.examples) { - expect(config.examples).toEqual(expectedConfig.examples); - } - if (expectedConfig.api) { - expect(config.api).toBe(expectedConfig.api); - } - if (expectedConfig.runtime) { - expect(config.runtime).toBe(expectedConfig.runtime); - } - if (expectedConfig.packageManager) { - expect(config.packageManager).toBe(expectedConfig.packageManager); - } - if (expectedConfig.webDeploy) { - expect(config.webDeploy).toBe(expectedConfig.webDeploy); - } - if (expectedConfig.serverDeploy) { - expect(config.serverDeploy).toBe(expectedConfig.serverDeploy); - } -} - -async function readBtsConfig(dir: string) { - const btsConfigPath = join(dir, "bts.jsonc"); - if (!existsSync(btsConfigPath)) return {} as Record; - - const content = await readFile(btsConfigPath, "utf8"); - const errors: JSONC.ParseError[] = []; - const parsed = JSONC.parse(content, errors, { - allowTrailingComma: true, - disallowComments: false, - }) as Record | null; - - if (errors.length > 0 || !parsed) { - return {} as Record; - } - return parsed; -} - -describe("create-better-t-stack smoke", () => { - let workdir: string; - - beforeAll(async () => { - workdir = await createTmpDir("cli"); - consola.start("Building CLI..."); - const buildProc = execa("bun", ["run", "build"], { - cwd: join(__dirname, ".."), - env: { - ...process.env, - CI: "true", - NODE_ENV: "production", - }, - }); - buildProc.stdout?.pipe(process.stdout); - buildProc.stderr?.pipe(process.stderr); - const { exitCode } = await buildProc; - expect(exitCode).toBe(0); - consola.success("CLI build completed"); - - process.env.BTS_TELEMETRY_DISABLED = "1"; - consola.info("Programmatic CLI mode"); - }); - - describe("frontend x backend matrix (no db, no api)", () => { - const FRONTENDS = [ - "tanstack-router", - "react-router", - "tanstack-start", - "next", - "nuxt", - "svelte", - "solid", - "native-nativewind", - "native-unistyles", - ] as const; - const BACKENDS = ["hono", "express", "fastify", "elysia"] as const; - - const WEB_FRONTENDS = new Set([ - "tanstack-router", - "react-router", - "tanstack-start", - "next", - "nuxt", - "svelte", - "solid", - ]); - - for (const backend of BACKENDS) { - describe(`backend=${backend}`, () => { - for (const frontend of FRONTENDS) { - it(`scaffolds ${frontend} + ${backend}`, async () => { - const projectName = `app-${backend}-${frontend.replace(/[^a-z-]/g, "").slice(0, 30)}`; - await runCli( - [ - projectName, - "--frontend", - frontend, - "--backend", - backend, - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--addons", - "none", - "--examples", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - await assertScaffoldedProject(projectDir); - await assertProjectStructure(projectDir, { - hasWeb: WEB_FRONTENDS.has(frontend), - hasNative: - frontend === "native-nativewind" || - frontend === "native-unistyles", - hasServer: true, - }); - await assertBtsConfig(projectDir, { - frontend: [frontend], - backend, - database: "none", - orm: "none", - auth: "none", - }); - }); - } - }); - } - }); - - describe("convex backend with all compatible frontends", () => { - const FRONTENDS = [ - "tanstack-router", - "react-router", - "tanstack-start", - "next", - "nuxt", - "svelte", - "native-nativewind", - "native-unistyles", - ] as const; - const WEB_FRONTENDS = new Set([ - "tanstack-router", - "react-router", - "tanstack-start", - "next", - "nuxt", - "svelte", - ]); - - for (const frontend of FRONTENDS) { - it(`scaffolds ${frontend} + convex`, async () => { - const projectName = `app-convex-${frontend.replace(/[^a-z-]/g, "").slice(0, 30)}`; - await runCli( - [ - projectName, - "--frontend", - frontend, - "--backend", - "convex", - "--runtime", - "none", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--addons", - "none", - "--examples", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: WEB_FRONTENDS.has(frontend), - hasNative: - frontend === "native-nativewind" || frontend === "native-unistyles", - hasConvexBackend: true, - hasServer: false, - }); - assertBtsConfig(projectDir, { - frontend: [frontend], - backend: "convex", - database: "none", - orm: "none", - auth: "none", - }); - }); - } - }); - - describe("convex + clerk auth combinations", () => { - const WEB_FRONTENDS = [ - "tanstack-router", - "react-router", - "tanstack-start", - "next", - ] as const; - const NATIVE_FRONTENDS = ["native-nativewind", "native-unistyles"] as const; - - for (const frontend of WEB_FRONTENDS) { - it(`scaffolds ${frontend} + convex + clerk`, async () => { - const projectName = `app-convex-clerk-${frontend.replace(/[^a-z-]/g, "").slice(0, 20)}`; - await runCli( - [ - projectName, - "--frontend", - frontend, - "--backend", - "convex", - "--runtime", - "none", - "--api", - "none", - "--database", - "none", - "--orm", - "none", - "--auth", - "clerk", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--addons", - "none", - "--examples", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasNative: false, - hasConvexBackend: true, - hasServer: false, - }); - assertBtsConfig(projectDir, { - frontend: [frontend], - backend: "convex", - database: "none", - orm: "none", - auth: "clerk", - }); - }); - } - - for (const frontend of NATIVE_FRONTENDS) { - it(`scaffolds ${frontend} + convex + clerk`, async () => { - const projectName = `app-convex-clerk-${frontend.replace(/[^a-z-]/g, "").slice(0, 20)}`; - await runCli( - [ - projectName, - "--frontend", - frontend, - "--backend", - "convex", - "--runtime", - "none", - "--api", - "none", - "--database", - "none", - "--orm", - "none", - "--auth", - "clerk", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--addons", - "none", - "--examples", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: false, - hasNative: true, - hasConvexBackend: true, - hasServer: false, - }); - assertBtsConfig(projectDir, { - frontend: [frontend], - backend: "convex", - database: "none", - orm: "none", - auth: "clerk", - }); - }); - } - - it("scaffolds tanstack-router + native-nativewind + convex + clerk", async () => { - const projectName = "app-convex-clerk-web-native"; - await runCli( - [ - projectName, - "--frontend", - "tanstack-router", - "native-nativewind", - "--backend", - "convex", - "--runtime", - "none", - "--api", - "none", - "--database", - "none", - "--orm", - "none", - "--auth", - "clerk", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--addons", - "none", - "--examples", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasNative: true, - hasConvexBackend: true, - hasServer: false, - }); - assertBtsConfig(projectDir, { - frontend: ["tanstack-router", "native-nativewind"], - backend: "convex", - database: "none", - orm: "none", - auth: "clerk", - }); - }); - - it("scaffolds next + native-unistyles + convex + clerk", async () => { - const projectName = "app-convex-clerk-next-native"; - await runCli( - [ - projectName, - - "--frontend", - "next", - "native-unistyles", - "--backend", - "convex", - "--runtime", - "none", - "--api", - "none", - "--database", - "none", - "--orm", - "none", - "--auth", - "clerk", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--addons", - "none", - "--examples", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasNative: true, - hasConvexBackend: true, - hasServer: false, - }); - assertBtsConfig(projectDir, { - frontend: ["next", "native-unistyles"], - backend: "convex", - database: "none", - orm: "none", - auth: "clerk", - }); - }); - - it("scaffolds tanstack-start + native-nativewind + convex + clerk", async () => { - const projectName = "app-convex-clerk-start-native"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-start", - "native-nativewind", - "--backend", - "convex", - "--runtime", - "none", - "--api", - "none", - "--database", - "none", - "--orm", - "none", - "--auth", - "clerk", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--addons", - "none", - "--examples", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasNative: true, - hasConvexBackend: true, - hasServer: false, - }); - assertBtsConfig(projectDir, { - frontend: ["tanstack-start", "native-nativewind"], - backend: "convex", - database: "none", - orm: "none", - auth: "clerk", - }); - }); - }); - afterAll(async () => { - try { - await remove(workdir); - } catch {} - }); - - it("scaffolds minimal default project with yes flag", async () => { - const projectName = "app-default"; - await runCli([projectName, "--yes", "--no-install", "--no-git"], workdir); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: true, - hasAuth: true, - hasDatabase: true, - hasTurborepo: true, - }); - assertBtsConfig(projectDir, { - frontend: ["tanstack-router"], - backend: "hono", - database: "sqlite", - orm: "drizzle", - auth: "better-auth", - addons: ["turborepo"], - }); - }); - - it("scaffolds with turborepo addon", async () => { - const projectName = "app-turbo"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "turborepo", - "--db-setup", - "none", - "--examples", - "none", - "--package-manager", - "bun", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: true, - hasTurborepo: true, - hasAuth: false, - hasDatabase: false, - }); - assertBtsConfig(projectDir, { - frontend: ["tanstack-router"], - backend: "hono", - addons: ["turborepo"], - }); - }); - - it("scaffolds convex preset", async () => { - const projectName = "app-convex"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "convex", - "--runtime", - "none", - "--api", - "none", - "--database", - "none", - "--orm", - "none", - "--auth", - "none", - "--db-setup", - "none", - "--server-deploy", - "none", - "--addons", - "none", - "--examples", - "todo", - "--package-manager", - "bun", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--no-install", - "--no-git", - ], - workdir, - ); - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasConvexBackend: true, - hasServer: false, - hasAuth: false, - hasDatabase: false, - }); - assertBtsConfig(projectDir, { - frontend: ["tanstack-router"], - backend: "convex", - database: "none", - orm: "none", - auth: "none", - examples: ["todo"], - }); - }); - - describe("frontend combinations", () => { - it("scaffolds with Next.js frontend", async () => { - const projectName = "app-next"; - await runCli( - [ - projectName, - "--frontend", - "next", - "--backend", - "none", - "--runtime", - "none", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--addons", - "none", - "--examples", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: false, - }); - assertBtsConfig(projectDir, { - frontend: ["next"], - backend: "none", - }); - }); - - it("scaffolds with Nuxt frontend", async () => { - const projectName = "app-nuxt"; - await runCli( - [ - projectName, - "--frontend", - "nuxt", - "--backend", - "none", - "--runtime", - "none", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--addons", - "none", - "--examples", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: false, - }); - assertBtsConfig(projectDir, { - frontend: ["nuxt"], - backend: "none", - }); - }); - - it("scaffolds with Svelte frontend", async () => { - const projectName = "app-svelte"; - await runCli( - [ - projectName, - "--frontend", - "svelte", - "--backend", - "none", - "--runtime", - "none", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--addons", - "none", - "--examples", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: false, - }); - assertBtsConfig(projectDir, { - frontend: ["svelte"], - backend: "none", - }); - }); - - it("scaffolds with Solid frontend", async () => { - const projectName = "app-solid"; - await runCli( - [ - projectName, - "--frontend", - "solid", - "--backend", - "none", - "--runtime", - "none", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--addons", - "none", - "--examples", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: false, - }); - assertBtsConfig(projectDir, { - frontend: ["solid"], - backend: "none", - }); - }); - - it("scaffolds with React Native (NativeWind)", async () => { - const projectName = "app-native"; - await runCli( - [ - projectName, - "--frontend", - "native-nativewind", - "--backend", - "none", - "--runtime", - "none", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--addons", - "none", - "--examples", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasNative: true, - hasServer: false, - }); - assertBtsConfig(projectDir, { - frontend: ["native-nativewind"], - backend: "none", - }); - }); - }); - - describe("backend combinations", () => { - it("scaffolds with Express backend", async () => { - const projectName = "app-express"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "express", - "--runtime", - "node", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: true, - }); - assertBtsConfig(projectDir, { - frontend: ["tanstack-router"], - backend: "express", - }); - }); - - it("scaffolds with Fastify backend", async () => { - const projectName = "app-fastify"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "fastify", - "--runtime", - "node", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: true, - }); - assertBtsConfig(projectDir, { - frontend: ["tanstack-router"], - backend: "fastify", - }); - }); - - it("scaffolds with Elysia backend", async () => { - const projectName = "app-elysia"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "elysia", - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: true, - }); - assertBtsConfig(projectDir, { - frontend: ["tanstack-router"], - backend: "elysia", - }); - }); - }); - - describe("database and ORM combinations", () => { - it("scaffolds with SQLite + Drizzle", async () => { - const projectName = "app-sqlite-drizzle"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "sqlite", - "--orm", - "drizzle", - "--api", - "trpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: true, - hasDatabase: true, - }); - assertBtsConfig(projectDir, { - database: "sqlite", - orm: "drizzle", - }); - }); - - it("scaffolds with PostgreSQL + Prisma", async () => { - const projectName = "app-postgres-prisma"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "postgres", - "--orm", - "prisma", - "--api", - "trpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: true, - hasDatabase: true, - }); - assertBtsConfig(projectDir, { - database: "postgres", - orm: "prisma", - }); - }); - - it("scaffolds with PostgreSQL + Drizzle", async () => { - const projectName = "app-postgres-drizzle"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "postgres", - "--orm", - "drizzle", - "--api", - "trpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: true, - hasDatabase: true, - }); - assertBtsConfig(projectDir, { - database: "postgres", - orm: "drizzle", - }); - }); - - it("scaffolds with MongoDB + Mongoose", async () => { - const projectName = "app-mongo-mongoose"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "mongodb", - "--orm", - "mongoose", - "--api", - "trpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: true, - hasDatabase: true, - }); - assertBtsConfig(projectDir, { - database: "mongodb", - orm: "mongoose", - }); - }); - }); - - describe("addon combinations", () => { - it("scaffolds with Biome addon", async () => { - const projectName = "app-biome"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "biome", - "--db-setup", - "none", - "--examples", - "none", - "--package-manager", - "bun", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: true, - hasBiome: true, - }); - assertBtsConfig(projectDir, { - addons: ["biome"], - }); - }); - - it("scaffolds with multiple addons", async () => { - const projectName = "app-multi-addons"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "turborepo", - "biome", - "--db-setup", - "none", - "--examples", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: true, - hasTurborepo: true, - hasBiome: true, - }); - assertBtsConfig(projectDir, { - addons: ["turborepo", "biome"], - }); - }); - }); - - describe("API types", () => { - it("scaffolds with tRPC API", async () => { - const projectName = "app-trpc"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "sqlite", - "--orm", - "drizzle", - "--api", - "trpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - api: "trpc", - }); - }); - - it("scaffolds with oRPC API", async () => { - const projectName = "app-orpc"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "sqlite", - "--orm", - "drizzle", - "--api", - "orpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - api: "orpc", - }); - }); - }); - - describe("validation and error cases", () => { - it("rejects invalid project names", async () => { - await runCliExpectingError( - [ - "", - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - }); - - it("rejects incompatible database and ORM combinations", async () => { - await runCliExpectingError( - [ - "invalid-combo-database-orm", - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "mongodb", - "--orm", - "drizzle", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - }); - - it("rejects incompatible frontend and API combinations", async () => { - await runCliExpectingError( - [ - "invalid-combo-frontend-api", - - "--frontend", - "nuxt", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "trpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - }); - - it("rejects multiple web frontends", async () => { - await runCliExpectingError( - [ - "invalid-combo-multiple-web", - - "--frontend", - "tanstack-router", - "next", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - }); - - it("rejects Turso db-setup with non-SQLite database", async () => { - await runCliExpectingError( - [ - "invalid-combo-turso-sqlite", - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "postgres", - "--orm", - "prisma", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "turso", - "--examples", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - }); - - it("rejects convex + better-auth combination", async () => { - await runCliExpectingError( - [ - "invalid-combo-convex-better-auth", - - "--frontend", - "tanstack-router", - "--backend", - "convex", - "--auth", - "better-auth", - "--db-setup", - "none", - "--addons", - "none", - "--examples", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - }); - - it("rejects nuxt + convex + clerk combination", async () => { - await runCliExpectingError( - [ - "invalid-combo-nuxt-convex-clerk", - - "--frontend", - "nuxt", - "--backend", - "convex", - "--auth", - "clerk", - "--db-setup", - "none", - "--addons", - "none", - "--examples", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - }); - - it("rejects svelte + convex + clerk combination", async () => { - await runCliExpectingError( - [ - "invalid-combo-svlete-convex-clerk", - - "--frontend", - "svelte", - "--backend", - "convex", - "--auth", - "clerk", - "--db-setup", - "none", - "--addons", - "none", - "--examples", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - }); - }); - - describe("YOLO mode", () => { - it("bypasses db-setup/database validation (Turso + Postgres + Prisma)", async () => { - const projectName = "app-yolo-turso-postgres"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "postgres", - "--orm", - "prisma", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "turso", - "--examples", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - "--yolo", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - await assertScaffoldedProject(projectDir); - await assertBtsConfig(projectDir, { - database: "postgres", - orm: "prisma", - }); - }); - - it("bypasses web-deploy requires web frontend (none + wrangler)", async () => { - const projectName = "app-yolo-webdeploy-no-frontend"; - await runCli( - [ - projectName, - "--frontend", - "none", - "--backend", - "none", - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--db-setup", - "none", - "--web-deploy", - "wrangler", - "--server-deploy", - "none", - "--addons", - "none", - "--examples", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - "--yolo", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - await assertScaffoldedProject(projectDir); - await assertBtsConfig(projectDir, { - backend: "none", - webDeploy: "wrangler", - }); - }); - }); - - describe("runtime compatibility", () => { - it("scaffolds with Cloudflare Workers runtime", async () => { - const projectName = "app-workers"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "workers", - "--server-deploy", - "wrangler", - "--database", - "sqlite", - "--orm", - "drizzle", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - backend: "hono", - runtime: "workers", - orm: "drizzle", - }); - }); - - it("rejects incompatible runtime and backend combinations", async () => { - await runCliExpectingError( - [ - "invalid-combo-runtime-backend", - - "--frontend", - "tanstack-router", - "--backend", - "express", - "--runtime", - "workers", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - }); - - it("rejects incompatible runtime and ORM combinations", async () => { - await runCliExpectingError( - [ - "invalid-combo-runtime-orm", - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "workers", - "--database", - "postgres", - "--orm", - "prisma", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - }); - }); - - describe("package managers", () => { - it("scaffolds with npm package manager", async () => { - const projectName = "app-npm"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "npm", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - packageManager: "npm", - }); - }); - - it("scaffolds with pnpm package manager", async () => { - const projectName = "app-pnpm"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "pnpm", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - packageManager: "pnpm", - }); - - expect(existsSync(join(projectDir, "pnpm-workspace.yaml"))).toBe(true); - }); - }); - - describe("comprehensive missing combinations", () => { - it("scaffolds Nuxt + AI example", async () => { - const projectName = "app-nuxt-ai"; - await runCli( - [ - projectName, - - "--frontend", - "nuxt", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "sqlite", - "--orm", - "drizzle", - "--api", - "orpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "ai", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - frontend: ["nuxt"], - backend: "hono", - examples: ["ai"], - api: "orpc", - }); - }); - it("scaffolds with todo example", async () => { - const projectName = "app-example-todo"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "sqlite", - "--orm", - "drizzle", - "--api", - "trpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "todo", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - examples: ["todo"], - }); - }); - - it("scaffolds with ai example", async () => { - const projectName = "app-example-ai"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "sqlite", - "--orm", - "drizzle", - "--api", - "trpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "ai", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - examples: ["ai"], - }); - }); - - it("scaffolds convex with todo example (default)", async () => { - const projectName = "app-convex-todo"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "convex", - "--runtime", - "none", - "--api", - "none", - "--database", - "none", - "--orm", - "none", - "--auth", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--addons", - "none", - "--examples", - "todo", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - backend: "convex", - examples: ["todo"], - auth: "none", - }); - }); - - it("scaffolds with git enabled", async () => { - const projectName = "app-with-git"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--git", - "--no-install", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - expect(existsSync(join(projectDir, ".git"))).toBe(true); - }); - - it("scaffolds with install enabled", async () => { - const projectName = "app-with-install"; - await runCli( - [ - projectName, - - "--directory-conflict", - "overwrite", - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - expect(existsSync(join(projectDir, "node_modules"))).toBe(true); - }); - - it("scaffolds with PWA addon", async () => { - const projectName = "app-addon-pwa"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "pwa", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - addons: ["pwa"], - }); - }); - - it("scaffolds with Tauri addon", async () => { - const projectName = "app-addon-tauri"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "tauri", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - addons: ["tauri"], - }); - }); - - it("scaffolds with Husky addon", async () => { - const projectName = "app-addon-husky"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "husky", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - addons: ["husky"], - }); - }); - - it("scaffolds with authentication enabled", async () => { - const projectName = "app-with-auth"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "sqlite", - "--orm", - "drizzle", - "--api", - "trpc", - "--auth", - "better-auth", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertProjectStructure(projectDir, { - hasWeb: true, - hasServer: true, - hasAuth: true, - hasDatabase: true, - }); - assertBtsConfig(projectDir, { - auth: "better-auth", - }); - }); - - it("scaffolds with MySQL + Prisma", async () => { - const projectName = "app-mysql-prisma"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "mysql", - "--orm", - "prisma", - "--api", - "trpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--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 MySQL + Drizzle", async () => { - const projectName = "app-mysql-drizzle"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "mysql", - "--orm", - "drizzle", - "--api", - "trpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--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 + 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( - [ - projectName, - - "--frontend", - "next", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "sqlite", - "--orm", - "drizzle", - "--api", - "orpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - frontend: ["next"], - api: "orpc", - }); - }); - - it("scaffolds oRPC with Nuxt (compatible)", async () => { - const projectName = "app-orpc-nuxt"; - await runCli( - [ - projectName, - - "--frontend", - "nuxt", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "sqlite", - "--orm", - "drizzle", - "--api", - "orpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - frontend: ["nuxt"], - api: "orpc", - }); - }); - - it("scaffolds oRPC with Svelte", async () => { - const projectName = "app-orpc-svelte"; - await runCli( - [ - projectName, - - "--frontend", - "svelte", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "sqlite", - "--orm", - "drizzle", - "--api", - "orpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - frontend: ["svelte"], - api: "orpc", - }); - }); - - it("scaffolds oRPC with Solid", async () => { - const projectName = "app-orpc-solid"; - await runCli( - [ - projectName, - - "--frontend", - "solid", - "--backend", - "hono", - "--runtime", - "bun", - "--database", - "sqlite", - "--orm", - "drizzle", - "--api", - "orpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - frontend: ["solid"], - api: "orpc", - }); - }); - - it("scaffolds with Next.js backend", async () => { - const projectName = "app-backend-next"; - await runCli( - [ - projectName, - - "--frontend", - "next", - "--backend", - "next", - "--runtime", - "bun", - "--database", - "sqlite", - "--orm", - "drizzle", - "--api", - "trpc", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - frontend: ["next"], - backend: "next", - }); - }); - - it("scaffolds with Node runtime", async () => { - const projectName = "app-node-runtime"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "express", - "--runtime", - "node", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--web-deploy", - "none", - "--server-deploy", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - assertScaffoldedProject(projectDir); - assertBtsConfig(projectDir, { - 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)( - "build each scaffolded project", - () => { - const sanitize = (s: string) => s.replace(/[^a-z-]/g, "").slice(0, 30); - const FRONTENDS_ALL = [ - "tanstack-router", - "react-router", - "tanstack-start", - "next", - "nuxt", - "svelte", - "solid", - "native-nativewind", - "native-unistyles", - ] as const; - const BACKENDS_STANDARD = [ - "hono", - "express", - "fastify", - "elysia", - ] as const; - const CONVEX_COMPATIBLE_FRONTENDS = FRONTENDS_ALL.filter( - (f) => f !== "solid", - ); - - const projectNames = new Set(); - for (const backend of BACKENDS_STANDARD) { - for (const frontend of FRONTENDS_ALL) { - projectNames.add(`app-${backend}-${sanitize(frontend)}`); - } - } - for (const frontend of CONVEX_COMPATIBLE_FRONTENDS) { - projectNames.add(`app-convex-${sanitize(frontend)}`); - } - - const WEB_FRONTENDS_CLERK = [ - "tanstack-router", - "react-router", - "tanstack-start", - "next", - ]; - const NATIVE_FRONTENDS_CLERK = ["native-nativewind", "native-unistyles"]; - - for (const frontend of WEB_FRONTENDS_CLERK) { - projectNames.add(`app-convex-clerk-${sanitize(frontend)}`); - } - for (const frontend of NATIVE_FRONTENDS_CLERK) { - projectNames.add(`app-convex-clerk-${sanitize(frontend)}`); - } - projectNames.add("app-convex-clerk-web-native"); - projectNames.add("app-convex-clerk-next-native"); - projectNames.add("app-convex-clerk-start-native"); - [ - "app-default", - "app-min", - "app-turbo", - "app-convex", - "app-next", - "app-nuxt", - "app-svelte", - "app-solid", - "app-native", - "app-express", - "app-fastify", - "app-elysia", - "app-sqlite-drizzle", - "app-postgres-prisma", - "app-mongo-mongoose", - "app-biome", - "app-multi-addons", - "app-trpc", - "app-orpc", - "app-nuxt-ai", - "app-example-todo", - "app-example-ai", - "app-convex-todo", - "app-with-git", - "app-with-install", - "app-addon-pwa", - "app-addon-tauri", - "app-addon-husky", - "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); - }); - - const detectPackageManager = async ( - projectDir: string, - ): Promise<"bun" | "pnpm" | "npm"> => { - const bts = readBtsConfig(projectDir) as { packageManager?: string }; - const pkgJsonPath = join(projectDir, "package.json"); - const pkg = existsSync(pkgJsonPath) ? await readJson(pkgJsonPath) : {}; - const pkgMgrField = - (pkg.packageManager as string | undefined) || bts.packageManager; - - if (typeof pkgMgrField === "string") { - if (pkgMgrField.includes("pnpm")) return "pnpm"; - if (pkgMgrField.includes("npm")) return "npm"; - if (pkgMgrField.includes("bun")) return "bun"; - } - if (existsSync(join(projectDir, "pnpm-workspace.yaml"))) return "pnpm"; - return "bun"; - }; - - const runInstall = async (pm: "bun" | "pnpm" | "npm", cwd: string) => { - if (pm === "bun") - return execa("bun", ["install"], { cwd, stdio: "inherit" }); - if (pm === "pnpm") - return execa("pnpm", ["install", "--no-frozen-lockfile"], { - cwd, - stdio: "inherit", - }); - return execa("npm", ["install", "--no-audit", "--no-fund"], { - cwd, - stdio: "inherit", - }); - }; - - const runScript = async ( - pm: "bun" | "pnpm" | "npm", - cwd: string, - script: string, - extraArgs: string[] = [], - timeout?: number, - ) => { - const base = pm === "bun" ? ["run", script] : ["run", script]; - const cmd = pm === "bun" ? "bun" : pm; - return execa(cmd, [...base, ...extraArgs], { - cwd, - timeout, - env: { ...process.env, NODE_ENV: "production", CI: "true" }, - stdio: "inherit", - }); - }; - - for (const dirName of projectNames) { - it(`builds ${dirName}`, async () => { - const projectDir = join(workdir, dirName); - if (!existsSync(projectDir)) { - consola.info(`${dirName} not found, skipping`); - return; - } - const pm = await detectPackageManager(projectDir); - - consola.info(`Processing ${dirName} (pm=${pm})`); - try { - consola.start(`Installing dependencies for ${dirName}...`); - try { - const res = await runInstall(pm, projectDir); - expect(res.exitCode).toBe(0); - } catch (installErr) { - if (pm !== "bun") { - consola.warn( - `Primary install with ${pm} failed. Retrying with bun...`, - ); - const fallback = await runInstall("bun", projectDir); - expect(fallback.exitCode).toBe(0); - } else { - throw installErr; - } - } - - const pkgJsonPath = join(projectDir, "package.json"); - const pkg = await readJson(pkgJsonPath); - const scripts = pkg.scripts || {}; - consola.info(`Scripts: ${Object.keys(scripts).join(", ")}`); - - const bts = (await readBtsConfig(projectDir)) as { - backend?: string; - frontend?: string[]; - }; - if (bts.backend === "convex") { - const frontends = Array.isArray(bts.frontend) ? bts.frontend : []; - const WEB_FRONTENDS = new Set([ - "tanstack-router", - "react-router", - "tanstack-start", - "next", - "nuxt", - "svelte", - "solid", - ]); - const hasWebFrontend = frontends.some((f) => - WEB_FRONTENDS.has(f), - ); - if (!hasWebFrontend) { - consola.info( - "Skipping Convex native-only project (no web app)", - ); - return; - } - } - - if (scripts.build) { - consola.start(`Building ${dirName}...`); - const isTurbo = existsSync(join(projectDir, "turbo.json")); - const extraArgs = isTurbo ? ["--force"] : []; - const buildRes = await runScript( - pm, - projectDir, - "build", - extraArgs, - 300_000, - ); - expect(buildRes.exitCode).toBe(0); - consola.success(`${dirName} built successfully`); - } - - if (!scripts.build) { - consola.info(`No build script for ${dirName}, skipping`); - } - } catch (error) { - consola.error(`${dirName} failed`, error); - throw error; - } - }); - } - }, - ); - - describe("deploy combinations", () => { - it("scaffolds workers runtime + web deploy wrangler", async () => { - const projectName = "app-web-wrangler"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "workers", - "--web-deploy", - "wrangler", - "--server-deploy", - "wrangler", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - await assertScaffoldedProject(projectDir); - await assertBtsConfig(projectDir, { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "workers", - }); - }); - - it("scaffolds workers runtime + web deploy alchemy", async () => { - const projectName = "app-web-alchemy"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "workers", - "--web-deploy", - "alchemy", - "--server-deploy", - "alchemy", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - await assertScaffoldedProject(projectDir); - await assertBtsConfig(projectDir, { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "workers", - }); - }); - - it("scaffolds workers runtime + server deploy alchemy (server-only)", async () => { - const projectName = "app-server-only-alchemy"; - await runCli( - [ - projectName, - - "--directory-conflict", - "overwrite", - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "workers", - "--server-deploy", - "alchemy", - "--web-deploy", - "none", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - await assertScaffoldedProject(projectDir); - await assertBtsConfig(projectDir, { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "workers", - serverDeploy: "alchemy", - }); - consola.info("Verifying server-only Alchemy artifacts"); - expect( - existsSync(join(projectDir, "apps", "server", "alchemy.run.ts")), - ).toBe(true); - expect(existsSync(join(projectDir, "apps", "server", "env.d.ts"))).toBe( - true, - ); - expect(existsSync(join(projectDir, "alchemy.run.ts"))).toBe(false); - }); - - it("scaffolds workers runtime + server deploy wrangler", async () => { - const projectName = "app-server-wrangler"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "workers", - "--server-deploy", - "wrangler", - "--web-deploy", - "none", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - await assertScaffoldedProject(projectDir); - await assertBtsConfig(projectDir, { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "workers", - }); - }); - - it("scaffolds workers runtime + server deploy alchemy", async () => { - const projectName = "app-server-alchemy"; - await runCli( - [ - projectName, - - "--frontend", - "tanstack-router", - "--backend", - "hono", - "--runtime", - "workers", - "--server-deploy", - "alchemy", - "--web-deploy", - "none", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - await assertScaffoldedProject(projectDir); - await assertBtsConfig(projectDir, { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "workers", - }); - }); - - it("scaffolds web deploy wrangler with backend none (no server deploy)", async () => { - const projectName = "app-web-wrangler-only"; - await runCli( - [ - projectName, - "--frontend", - "tanstack-router", - "--backend", - "none", - "--runtime", - "none", - "--web-deploy", - "wrangler", - "--server-deploy", - "none", - "--database", - "none", - "--orm", - "none", - "--api", - "none", - "--auth", - "none", - "--addons", - "none", - "--examples", - "none", - "--db-setup", - "none", - "--package-manager", - "bun", - "--no-install", - "--no-git", - ], - workdir, - ); - - const projectDir = join(workdir, projectName); - await assertScaffoldedProject(projectDir); - await assertBtsConfig(projectDir, { - frontend: ["tanstack-router"], - backend: "none", - webDeploy: "wrangler", - }); - }); - }); -}); diff --git a/apps/cli/test/database-orm.test.ts b/apps/cli/test/database-orm.test.ts new file mode 100644 index 0000000..44dbcd6 --- /dev/null +++ b/apps/cli/test/database-orm.test.ts @@ -0,0 +1,237 @@ +import { describe, it } from "vitest"; +import type { Database, ORM } from "../src/types"; +import { + DATABASES, + expectError, + expectSuccess, + runTRPCTest, +} from "./test-utils"; + +describe("Database and ORM Combinations", () => { + describe("Valid Database-ORM Combinations", () => { + const validCombinations: Array<{ database: Database; orm: ORM }> = [ + // SQLite combinations + { database: "sqlite" as Database, orm: "drizzle" as ORM }, + { database: "sqlite" as Database, orm: "prisma" as ORM }, + + // PostgreSQL combinations + { database: "postgres" as Database, orm: "drizzle" as ORM }, + { database: "postgres" as Database, orm: "prisma" as ORM }, + + // MySQL combinations + { database: "mysql" as Database, orm: "drizzle" as ORM }, + { database: "mysql" as Database, orm: "prisma" as ORM }, + + // MongoDB combinations + { database: "mongodb" as Database, orm: "mongoose" as ORM }, + { database: "mongodb" as Database, orm: "prisma" as ORM }, + + // None combinations + { database: "none" as Database, orm: "none" as ORM }, + ]; + + for (const { database, orm } of validCombinations) { + it(`should work with ${database} + ${orm}`, async () => { + const result = await runTRPCTest({ + projectName: `${database}-${orm}`, + database, + orm, + backend: "hono", + runtime: "bun", + frontend: ["tanstack-router"], + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + } + }); + + describe("Invalid Database-ORM Combinations", () => { + const invalidCombinations: Array<{ + database: Database; + orm: ORM; + error: string; + }> = [ + // MongoDB with Drizzle (not supported) + { + database: "mongodb" as Database, + orm: "drizzle" as ORM, + error: "Drizzle ORM does not support MongoDB", + }, + + // Mongoose with non-MongoDB + { + database: "sqlite" as Database, + orm: "mongoose" as ORM, + error: "Mongoose ORM requires MongoDB database", + }, + { + database: "postgres" as Database, + orm: "mongoose" as ORM, + error: "Mongoose ORM requires MongoDB database", + }, + { + database: "mysql" as Database, + orm: "mongoose" as ORM, + error: "Mongoose ORM requires MongoDB database", + }, + + // Database without ORM + { + database: "sqlite" as Database, + orm: "none" as ORM, + error: "Database selection requires an ORM", + }, + { + database: "postgres" as Database, + orm: "none" as ORM, + error: "Database selection requires an ORM", + }, + + // ORM without database + { + database: "none" as Database, + orm: "drizzle" as ORM, + error: "ORM selection requires a database", + }, + { + database: "none" as Database, + orm: "prisma" as ORM, + error: "ORM selection requires a database", + }, + ]; + + for (const { database, orm, error } of invalidCombinations) { + it(`should fail with ${database} + ${orm}`, async () => { + const result = await runTRPCTest({ + projectName: `invalid-${database}-${orm}`, + database, + orm, + backend: "hono", + runtime: "bun", + frontend: ["tanstack-router"], + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, error); + }); + } + }); + + describe("Database-ORM with Authentication", () => { + it("should work with database + auth", async () => { + const result = await runTRPCTest({ + projectName: "db-auth", + database: "sqlite", + orm: "drizzle", + auth: "better-auth", + backend: "hono", + runtime: "bun", + frontend: ["tanstack-router"], + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with auth but no database (non-convex backend)", async () => { + const result = await runTRPCTest({ + projectName: "auth-no-db", + database: "none", + orm: "none", + auth: "better-auth", + backend: "hono", + runtime: "bun", + frontend: ["tanstack-router"], + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "Authentication requires a database"); + }); + + it("should work with auth but no database (convex backend)", async () => { + const result = await runTRPCTest({ + projectName: "convex-auth-no-db", + database: "none", + orm: "none", + auth: "none", + backend: "convex", + runtime: "none", + frontend: ["tanstack-router"], + api: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + }); + + describe("All Database Types", () => { + for (const database of DATABASES) { + if (database === "none") continue; + + it(`should have valid ORM options for ${database}`, async () => { + // Test with the most compatible ORM for each database + const ormMap = { + sqlite: "drizzle", + postgres: "drizzle", + mysql: "drizzle", + mongodb: "mongoose", + }; + + const orm = ormMap[database as keyof typeof ormMap]; + + const result = await runTRPCTest({ + projectName: `test-${database}`, + database: database as Database, + orm: orm as ORM, + backend: "hono", + runtime: "bun", + frontend: ["tanstack-router"], + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + } + }); +}); diff --git a/apps/cli/test/database-setup.test.ts b/apps/cli/test/database-setup.test.ts new file mode 100644 index 0000000..79c5d17 --- /dev/null +++ b/apps/cli/test/database-setup.test.ts @@ -0,0 +1,506 @@ +import { describe, it } from "vitest"; +import { + DB_SETUPS, + expectError, + expectSuccess, + runTRPCTest, + type TestConfig, +} from "./test-utils"; + +describe("Database Setup Configurations", () => { + describe("SQLite Database Setups", () => { + it("should work with Turso + SQLite", async () => { + const result = await runTRPCTest({ + projectName: "turso-sqlite", + database: "sqlite", + orm: "drizzle", + dbSetup: "turso", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + install: false, + }); + + expectSuccess(result); + }); + + it("should work with D1 + SQLite + Workers", async () => { + const result = await runTRPCTest({ + projectName: "d1-sqlite-workers", + database: "sqlite", + orm: "drizzle", + dbSetup: "d1", + backend: "hono", + runtime: "workers", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "wrangler", + manualDb: true, + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with Turso + non-SQLite database", async () => { + const result = await runTRPCTest({ + projectName: "turso-postgres-fail", + database: "postgres", + orm: "drizzle", + dbSetup: "turso", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + expectError: true, + }); + + expectError(result, "Turso setup requires SQLite database"); + }); + }); + + describe("PostgreSQL Database Setups", () => { + it("should work with Neon + PostgreSQL", async () => { + const result = await runTRPCTest({ + projectName: "neon-postgres", + database: "postgres", + orm: "drizzle", + dbSetup: "neon", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + install: false, + }); + + expectSuccess(result); + }); + + it("should work with Supabase + PostgreSQL", async () => { + const result = await runTRPCTest({ + projectName: "supabase-postgres", + database: "postgres", + orm: "drizzle", + dbSetup: "supabase", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + install: false, + }); + + expectSuccess(result); + }); + + it("should work with Prisma PostgreSQL setup", async () => { + const result = await runTRPCTest({ + projectName: "prisma-postgres-setup", + database: "postgres", + orm: "prisma", + dbSetup: "prisma-postgres", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with Neon + non-PostgreSQL database", async () => { + const result = await runTRPCTest({ + projectName: "neon-mysql-fail", + database: "mysql", + orm: "drizzle", + dbSetup: "neon", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + expectError: true, + }); + + expectError(result, "Neon setup requires PostgreSQL database"); + }); + }); + + describe("MySQL Database Setups", () => { + it("should work with PlanetScale + MySQL", async () => { + const result = await runTRPCTest({ + projectName: "planetscale-mysql", + database: "mysql", + orm: "drizzle", + dbSetup: "planetscale", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + install: false, + }); + + expectSuccess(result); + }); + + it("should work with PlanetScale + PostgreSQL", async () => { + const result = await runTRPCTest({ + projectName: "planetscale-postgres", + database: "postgres", + orm: "drizzle", + dbSetup: "planetscale", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + install: false, + }); + + expectSuccess(result); + }); + }); + + describe("MongoDB Database Setups", () => { + it("should work with MongoDB Atlas + MongoDB", async () => { + const result = await runTRPCTest({ + projectName: "mongodb-atlas", + database: "mongodb", + orm: "mongoose", + dbSetup: "mongodb-atlas", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with MongoDB Atlas + non-MongoDB database", async () => { + const result = await runTRPCTest({ + projectName: "mongodb-atlas-sqlite-fail", + database: "sqlite", + orm: "drizzle", + dbSetup: "mongodb-atlas", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + expectError: true, + }); + + expectError(result, "MongoDB Atlas setup requires MongoDB database"); + }); + }); + + describe("Docker Database Setup", () => { + it("should work with Docker + PostgreSQL", async () => { + const result = await runTRPCTest({ + projectName: "docker-postgres", + database: "postgres", + orm: "drizzle", + dbSetup: "docker", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + install: false, + }); + + expectSuccess(result); + }); + + it("should work with Docker + MySQL", async () => { + const result = await runTRPCTest({ + projectName: "docker-mysql", + database: "mysql", + orm: "drizzle", + dbSetup: "docker", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + install: false, + }); + + expectSuccess(result); + }); + + it("should work with Docker + MongoDB", async () => { + const result = await runTRPCTest({ + projectName: "docker-mongodb", + database: "mongodb", + orm: "mongoose", + dbSetup: "docker", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with Docker + SQLite", async () => { + const result = await runTRPCTest({ + projectName: "docker-sqlite-fail", + database: "sqlite", + orm: "drizzle", + dbSetup: "docker", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + expectError: true, + }); + + expectError( + result, + "Docker setup is not compatible with SQLite database", + ); + }); + }); + + describe("No Database Setup", () => { + it("should work with dbSetup none", async () => { + const result = await runTRPCTest({ + projectName: "no-db-setup", + database: "sqlite", + orm: "drizzle", + dbSetup: "none", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with dbSetup but no database", async () => { + const result = await runTRPCTest({ + projectName: "db-setup-no-db-fail", + database: "none", + orm: "none", + dbSetup: "turso", + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError( + result, + "Database setup requires a database. Please choose a database or set '--db-setup none'.", + ); + }); + }); + + describe("Special Runtime Constraints", () => { + it("should work with D1 + Workers runtime", async () => { + const result = await runTRPCTest({ + projectName: "d1-workers-valid", + database: "sqlite", + orm: "drizzle", + dbSetup: "none", + backend: "hono", + runtime: "workers", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "wrangler", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with D1 + non-Workers runtime", async () => { + const result = await runTRPCTest({ + projectName: "d1-node-fail", + database: "sqlite", + orm: "drizzle", + dbSetup: "d1", + backend: "hono", + runtime: "node", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError( + result, + "Cloudflare D1 setup requires SQLite database and Cloudflare Workers runtime", + ); + }); + }); + + describe("All Database Setup Types", () => { + for (const dbSetup of DB_SETUPS) { + if (dbSetup === "none") continue; + + it(`should work with ${dbSetup} in appropriate setup`, async () => { + const config: TestConfig = { + projectName: `test-${dbSetup}`, + dbSetup, + backend: "hono", + runtime: "bun", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + webDeploy: "none", + serverDeploy: "none", + manualDb: true, + install: false, + }; + + // Set appropriate database and ORM for each setup + switch (dbSetup) { + case "turso": + config.database = "sqlite"; + config.orm = "drizzle"; + break; + case "neon": + case "supabase": + case "prisma-postgres": + config.database = "postgres"; + config.orm = "drizzle"; + break; + case "planetscale": + config.database = "mysql"; + config.orm = "drizzle"; + break; + case "mongodb-atlas": + config.database = "mongodb"; + config.orm = "mongoose"; + break; + case "d1": + config.database = "sqlite"; + config.orm = "drizzle"; + config.runtime = "workers"; + config.serverDeploy = "wrangler"; + break; + case "docker": + config.database = "postgres"; + config.orm = "drizzle"; + break; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + }); + } + }); +}); diff --git a/apps/cli/test/deployment.test.ts b/apps/cli/test/deployment.test.ts new file mode 100644 index 0000000..93cea93 --- /dev/null +++ b/apps/cli/test/deployment.test.ts @@ -0,0 +1,560 @@ +import { describe, it } from "vitest"; +import { + expectError, + expectSuccess, + runTRPCTest, + SERVER_DEPLOYS, + type TestConfig, + WEB_DEPLOYS, +} from "./test-utils"; + +describe("Deployment Configurations", () => { + describe("Web Deployment", () => { + describe("Valid Web Deploy Configurations", () => { + for (const webDeploy of WEB_DEPLOYS) { + if (webDeploy === "none") continue; + + it(`should work with ${webDeploy} web deploy + web frontend`, async () => { + const result = await runTRPCTest({ + projectName: `${webDeploy}-web-deploy`, + webDeploy: webDeploy, + serverDeploy: "none", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + install: false, + }); + + expectSuccess(result); + }); + } + }); + + it("should work with web deploy none", async () => { + const result = await runTRPCTest({ + projectName: "no-web-deploy", + webDeploy: "none", + serverDeploy: "none", + frontend: ["tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with web deploy but no web frontend", async () => { + const result = await runTRPCTest({ + projectName: "web-deploy-no-web-frontend-fail", + webDeploy: "wrangler", + serverDeploy: "none", + frontend: ["native-nativewind"], // Native frontend only + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + expectError: true, + }); + + expectError(result, "'--web-deploy' requires a web frontend"); + }); + + it("should work with web deploy + mixed web and native frontends", async () => { + const result = await runTRPCTest({ + projectName: "web-deploy-mixed-frontends", + webDeploy: "wrangler", + serverDeploy: "none", + frontend: ["tanstack-router", "native-nativewind"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with web deploy + all web frontends", async () => { + const webFrontends = [ + "tanstack-router", + "react-router", + "tanstack-start", + "next", + "nuxt", + "svelte", + "solid", + ] as const; + + for (const frontend of webFrontends) { + const config: TestConfig = { + projectName: `web-deploy-${frontend}`, + webDeploy: "wrangler", + serverDeploy: "none", + frontend: [frontend], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + install: false, + }; + + // Handle API compatibility + if (["nuxt", "svelte", "solid"].includes(frontend)) { + config.api = "orpc"; + } else { + config.api = "trpc"; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + } + }); + }); + + describe("Server Deployment", () => { + describe("Valid Server Deploy Configurations", () => { + for (const serverDeploy of SERVER_DEPLOYS) { + if (serverDeploy === "none") continue; + + it(`should work with ${serverDeploy} server deploy + backend`, async () => { + const result = await runTRPCTest({ + projectName: `${serverDeploy}-server-deploy`, + webDeploy: "none", + serverDeploy: serverDeploy, + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + install: false, + }); + + expectSuccess(result); + }); + } + }); + + it("should work with server deploy none", async () => { + const result = await runTRPCTest({ + projectName: "no-server-deploy", + webDeploy: "none", + serverDeploy: "none", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with server deploy but no backend", async () => { + const result = await runTRPCTest({ + projectName: "server-deploy-no-backend-fail", + webDeploy: "none", + serverDeploy: "wrangler", + backend: "none", + runtime: "none", + database: "none", + orm: "none", + auth: "none", + api: "none", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + expectError: true, + }); + + expectError( + result, + "Backend 'none' requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.", + ); + }); + + it("should work with server deploy + all compatible backends", async () => { + const backends = [ + "hono", + "express", + "fastify", + "next", + "elysia", + ] as const; + + for (const backend of backends) { + const config: TestConfig = { + projectName: `server-deploy-${backend}`, + webDeploy: "none", + serverDeploy: "wrangler", + backend, + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + install: false, + }; + + // Set appropriate runtime + if (backend === "elysia") { + config.runtime = "bun"; + } else { + config.runtime = "bun"; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + } + }); + + it("should fail with server deploy + convex backend", async () => { + const result = await runTRPCTest({ + projectName: "server-deploy-convex-fail", + webDeploy: "none", + serverDeploy: "wrangler", + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + auth: "clerk", + api: "none", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + expectError: true, + }); + + expectError(result, "Convex backend requires '--server-deploy none'"); + }); + }); + + describe("Workers Runtime Deployment Constraints", () => { + it("should work with workers runtime + server deploy", async () => { + const result = await runTRPCTest({ + projectName: "workers-server-deploy", + webDeploy: "none", + runtime: "workers", + serverDeploy: "wrangler", + backend: "hono", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "d1", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with workers runtime + no server deploy", async () => { + const result = await runTRPCTest({ + projectName: "workers-no-server-deploy-fail", + runtime: "workers", + serverDeploy: "none", + backend: "hono", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + expectError: true, + }); + + expectError( + result, + "Cloudflare Workers runtime requires a server deployment", + ); + }); + }); + + describe("Combined Web and Server Deployment", () => { + it("should work with both web and server deploy", async () => { + const result = await runTRPCTest({ + projectName: "web-server-deploy-combo", + webDeploy: "wrangler", + serverDeploy: "wrangler", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with different deploy providers", async () => { + const result = await runTRPCTest({ + projectName: "different-deploy-providers", + webDeploy: "wrangler", + serverDeploy: "alchemy", + backend: "hono", + runtime: "workers", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with web deploy only", async () => { + const result = await runTRPCTest({ + projectName: "web-deploy-only", + webDeploy: "wrangler", + serverDeploy: "none", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with server deploy only", async () => { + const result = await runTRPCTest({ + projectName: "server-deploy-only", + webDeploy: "none", + serverDeploy: "wrangler", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + install: false, + }); + + expectSuccess(result); + }); + }); + + describe("Deployment with Special Backend Constraints", () => { + it("should work with deployment + next backend", async () => { + const result = await runTRPCTest({ + projectName: "deploy-next-backend", + webDeploy: "wrangler", + serverDeploy: "wrangler", + backend: "next", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["next"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with deployment + fullstack setup", async () => { + const result = await runTRPCTest({ + projectName: "deploy-fullstack", + webDeploy: "wrangler", + serverDeploy: "wrangler", + backend: "hono", + runtime: "workers", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + install: false, + }); + + expectSuccess(result); + }); + }); + + describe("All Deployment Options", () => { + const deployOptions: ReadonlyArray<{ + webDeploy: TestConfig["webDeploy"]; + serverDeploy: TestConfig["serverDeploy"]; + }> = [ + { webDeploy: "wrangler", serverDeploy: "wrangler" }, + { webDeploy: "wrangler", serverDeploy: "alchemy" }, + { webDeploy: "alchemy", serverDeploy: "wrangler" }, + { webDeploy: "alchemy", serverDeploy: "alchemy" }, + { webDeploy: "wrangler", serverDeploy: "none" }, + { webDeploy: "alchemy", serverDeploy: "none" }, + { webDeploy: "none", serverDeploy: "wrangler" }, + { webDeploy: "none", serverDeploy: "alchemy" }, + { webDeploy: "none", serverDeploy: "none" }, + ]; + + for (const { webDeploy, serverDeploy } of deployOptions) { + it(`should work with webDeploy: ${webDeploy}, serverDeploy: ${serverDeploy}`, async () => { + const config: TestConfig = { + projectName: `deploy-${webDeploy}-${serverDeploy}`, + webDeploy, + serverDeploy, + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + install: false, + }; + + // Handle special cases + if ( + webDeploy !== "none" && + !config.frontend?.some((f) => + [ + "tanstack-router", + "react-router", + "tanstack-start", + "next", + "nuxt", + "svelte", + "solid", + ].includes(f), + ) + ) { + config.frontend = ["tanstack-router"]; // Ensure web frontend for web deploy + } + + if (serverDeploy !== "none" && config.backend === "none") { + config.backend = "hono"; // Ensure backend for server deploy + } + + const result = await runTRPCTest(config); + expectSuccess(result); + }); + } + }); + + describe("Deployment Edge Cases", () => { + it("should handle deployment with complex configurations", async () => { + const result = await runTRPCTest({ + projectName: "complex-deployment", + webDeploy: "wrangler", + serverDeploy: "wrangler", + backend: "hono", + runtime: "workers", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], // Single web frontend (compatible with PWA) + addons: ["pwa", "turborepo"], + examples: ["todo"], + install: false, + }); + + expectSuccess(result); + }); + + it("should handle deployment constraints properly", async () => { + // This should fail because we have web deploy but only native frontend + const result = await runTRPCTest({ + projectName: "deployment-constraints-fail", + webDeploy: "wrangler", + serverDeploy: "none", + backend: "none", // No backend but we have server deploy + runtime: "none", + database: "none", + orm: "none", + auth: "none", + api: "none", + frontend: ["native-nativewind"], // Only native frontend + addons: ["none"], + examples: ["none"], + dbSetup: "none", + expectError: true, + }); + + expectError(result, "'--web-deploy' requires a web frontend"); + }); + }); +}); diff --git a/apps/cli/test/examples.test.ts b/apps/cli/test/examples.test.ts new file mode 100644 index 0000000..962d23b --- /dev/null +++ b/apps/cli/test/examples.test.ts @@ -0,0 +1,444 @@ +import { describe, it } from "vitest"; +import { + EXAMPLES, + expectError, + expectSuccess, + runTRPCTest, + type TestConfig, +} from "./test-utils"; + +describe("Example Configurations", () => { + describe("Todo Example", () => { + it("should work with todo example + database + backend", async () => { + const result = await runTRPCTest({ + projectName: "todo-with-db", + examples: ["todo"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with todo example + convex backend", async () => { + const result = await runTRPCTest({ + projectName: "todo-convex", + examples: ["todo"], + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + auth: "clerk", + api: "none", + frontend: ["tanstack-router"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with todo example + no backend", async () => { + const result = await runTRPCTest({ + projectName: "todo-no-backend", + examples: ["todo"], + backend: "none", + runtime: "none", + database: "none", + orm: "none", + auth: "none", + api: "none", + frontend: ["tanstack-router"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with todo example + backend + no database", async () => { + const result = await runTRPCTest({ + projectName: "todo-backend-no-db-fail", + examples: ["todo"], + backend: "hono", + runtime: "bun", + database: "none", + orm: "none", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError( + result, + "The 'todo' example requires a database if a backend (other than Convex) is present", + ); + }); + }); + + describe("AI Example", () => { + it("should work with AI example + React frontend", async () => { + const result = await runTRPCTest({ + projectName: "ai-react", + examples: ["ai"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with AI example + Next.js", async () => { + const result = await runTRPCTest({ + projectName: "ai-next", + examples: ["ai"], + backend: "next", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["next"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with AI example + Nuxt", async () => { + const result = await runTRPCTest({ + projectName: "ai-nuxt", + examples: ["ai"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "orpc", // tRPC not supported with Nuxt + frontend: ["nuxt"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should work with AI example + Svelte", async () => { + const result = await runTRPCTest({ + projectName: "ai-svelte", + examples: ["ai"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "orpc", // tRPC not supported with Svelte + frontend: ["svelte"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with AI example + Solid frontend", async () => { + const result = await runTRPCTest({ + projectName: "ai-solid-fail", + examples: ["ai"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "orpc", + frontend: ["solid"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError( + result, + "The 'ai' example is not compatible with the Solid frontend", + ); + }); + + it("should work with AI example + Convex", async () => { + const result = await runTRPCTest({ + projectName: "ai-convex", + examples: ["ai"], + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + auth: "clerk", + api: "none", + frontend: ["tanstack-router"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + }); + + describe("Multiple Examples", () => { + it("should work with both todo and AI examples", async () => { + const result = await runTRPCTest({ + projectName: "todo-ai-combo", + examples: ["todo", "ai"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with both examples if one is incompatible", async () => { + const result = await runTRPCTest({ + projectName: "todo-ai-solid-fail", + examples: ["todo", "ai"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "orpc", + frontend: ["solid"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError( + result, + "The 'ai' example is not compatible with the Solid frontend", + ); + }); + }); + + describe("Examples with None Option", () => { + it("should work with examples none", async () => { + const result = await runTRPCTest({ + projectName: "no-examples", + examples: ["none"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with none + other examples", async () => { + const result = await runTRPCTest({ + projectName: "none-with-examples-fail", + examples: ["none", "todo"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "Cannot combine 'none' with other examples"); + }); + }); + + describe("Examples with API None", () => { + it("should fail with examples when API is none (non-convex backend)", async () => { + const result = await runTRPCTest({ + projectName: "examples-api-none-fail", + examples: ["todo"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "none", + frontend: ["tanstack-router"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError( + result, + "Cannot use '--examples' when '--api' is set to 'none'", + ); + }); + + it("should work with examples when API is none (convex backend)", async () => { + const result = await runTRPCTest({ + projectName: "examples-api-none-convex", + examples: ["todo"], + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + auth: "clerk", + api: "none", + frontend: ["tanstack-router"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + }); + + describe("All Example Types", () => { + for (const example of EXAMPLES) { + if (example === "none") continue; + + it(`should work with ${example} example in appropriate setup`, async () => { + const config: TestConfig = { + projectName: `test-${example}`, + examples: [example], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }; + + const result = await runTRPCTest(config); + expectSuccess(result); + }); + } + }); + + describe("Example Edge Cases", () => { + it("should work with empty examples array", async () => { + const result = await runTRPCTest({ + projectName: "empty-examples", + examples: ["none"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should handle complex example constraints", async () => { + // Todo example with backend but no database should fail + const result = await runTRPCTest({ + projectName: "complex-example-constraints", + examples: ["todo"], + backend: "express", // Non-convex backend + runtime: "bun", + database: "none", // No database + orm: "none", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError( + result, + "The 'todo' example requires a database if a backend (other than Convex) is present", + ); + }); + }); +}); diff --git a/apps/cli/test/frontend.test.ts b/apps/cli/test/frontend.test.ts new file mode 100644 index 0000000..d3a3528 --- /dev/null +++ b/apps/cli/test/frontend.test.ts @@ -0,0 +1,454 @@ +import { describe, it } from "vitest"; +import { + expectError, + expectSuccess, + runTRPCTest, + type TestConfig, +} from "./test-utils"; + +describe("Frontend Configurations", () => { + describe("Single Frontend Options", () => { + const singleFrontends = [ + "tanstack-router", + "react-router", + "tanstack-start", + "next", + "nuxt", + "native-nativewind", + "native-unistyles", + "svelte", + "solid", + ] satisfies ReadonlyArray< + | "tanstack-router" + | "react-router" + | "tanstack-start" + | "next" + | "nuxt" + | "native-nativewind" + | "native-unistyles" + | "svelte" + | "solid" + >; + + for (const frontend of singleFrontends) { + it(`should work with ${frontend}`, async () => { + const config: TestConfig = { + projectName: `${frontend}-app`, + frontend: [frontend], + install: false, + }; + + // Set compatible defaults based on frontend + if (frontend === "solid") { + // Solid is not compatible with Convex backend + config.backend = "hono"; + config.runtime = "bun"; + config.database = "sqlite"; + config.orm = "drizzle"; + config.auth = "none"; + config.api = "orpc"; // tRPC not supported with solid + config.addons = ["none"]; + config.examples = ["none"]; + config.dbSetup = "none"; + config.webDeploy = "none"; + config.serverDeploy = "none"; + } else if (["nuxt", "svelte"].includes(frontend)) { + config.backend = "hono"; + config.runtime = "bun"; + config.database = "sqlite"; + config.orm = "drizzle"; + config.auth = "none"; + config.api = "orpc"; // tRPC not supported with nuxt/svelte + config.addons = ["none"]; + config.examples = ["none"]; + config.dbSetup = "none"; + config.webDeploy = "none"; + config.serverDeploy = "none"; + } else { + config.backend = "hono"; + config.runtime = "bun"; + config.database = "sqlite"; + config.orm = "drizzle"; + config.auth = "none"; + config.api = "trpc"; + config.addons = ["none"]; + config.examples = ["none"]; + config.dbSetup = "none"; + config.webDeploy = "none"; + config.serverDeploy = "none"; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + }); + } + }); + + describe("Frontend Compatibility with API", () => { + it("should work with React frontends + tRPC", async () => { + const result = await runTRPCTest({ + projectName: "react-trpc", + frontend: ["tanstack-router"], + api: "trpc", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with Nuxt + tRPC", async () => { + const result = await runTRPCTest({ + projectName: "nuxt-trpc-fail", + frontend: ["nuxt"], + api: "trpc", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "tRPC API is not supported with 'nuxt' frontend"); + }); + + it("should fail with Svelte + tRPC", async () => { + const result = await runTRPCTest({ + projectName: "svelte-trpc-fail", + frontend: ["svelte"], + api: "trpc", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "tRPC API is not supported with 'svelte' frontend"); + }); + + it("should fail with Solid + tRPC", async () => { + const result = await runTRPCTest({ + projectName: "solid-trpc-fail", + frontend: ["solid"], + api: "trpc", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "tRPC API is not supported with 'solid' frontend"); + }); + + it("should work with non-React frontends + oRPC", async () => { + const frontends = ["nuxt", "svelte", "solid"] as const; + + for (const frontend of frontends) { + const result = await runTRPCTest({ + projectName: `${frontend}-orpc`, + frontend: [frontend], + api: "orpc", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + } + }); + }); + + describe("Frontend Compatibility with Backend", () => { + it("should fail Solid + Convex", async () => { + const result = await runTRPCTest({ + projectName: "solid-convex-fail", + frontend: ["solid"], + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + auth: "none", + api: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError( + result, + "The following frontends are not compatible with '--backend convex': solid. Please choose a different frontend or backend.", + ); + }); + + it("should work with React frontends + Convex", async () => { + const result = await runTRPCTest({ + projectName: "react-convex", + frontend: ["tanstack-router"], + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + auth: "clerk", + api: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + }); + + describe("Frontend Compatibility with Auth", () => { + it("should fail incompatible frontends with Clerk + Convex", async () => { + const incompatibleFrontends = ["nuxt", "svelte", "solid"] as const; + + for (const frontend of incompatibleFrontends) { + const result = await runTRPCTest({ + projectName: `${frontend}-clerk-convex-fail`, + frontend: [frontend], + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + auth: "clerk", + api: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "Clerk authentication is not compatible"); + } + }); + + it("should work with compatible frontends + Clerk + Convex", async () => { + const compatibleFrontends = [ + "tanstack-router", + "react-router", + "tanstack-start", + "next", + ] as const; + + for (const frontend of compatibleFrontends) { + const result = await runTRPCTest({ + projectName: `${frontend}-clerk-convex`, + frontend: [frontend], + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + auth: "clerk", + api: "none", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + } + }); + }); + + describe("Multiple Frontend Constraints", () => { + it("should fail with multiple web frontends", async () => { + const result = await runTRPCTest({ + projectName: "multiple-web-fail", + frontend: ["tanstack-router", "react-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "Cannot select multiple web frameworks"); + }); + + it("should fail with multiple native frontends", async () => { + const result = await runTRPCTest({ + projectName: "multiple-native-fail", + frontend: ["native-nativewind", "native-unistyles"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "Cannot select multiple native frameworks"); + }); + + it("should work with one web + one native frontend", async () => { + const result = await runTRPCTest({ + projectName: "web-native-combo", + frontend: ["tanstack-router", "native-nativewind"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + }); + + describe("Frontend with None Option", () => { + it("should work with frontend none", async () => { + const result = await runTRPCTest({ + projectName: "no-frontend", + frontend: ["none"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with none + other frontends", async () => { + const result = await runTRPCTest({ + projectName: "none-with-other-fail", + frontend: ["none", "tanstack-router"], + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "Cannot combine 'none' with other frontend options"); + }); + }); + + describe("Web Deploy Constraints", () => { + it("should work with web frontend + web deploy", async () => { + const result = await runTRPCTest({ + projectName: "web-deploy", + frontend: ["tanstack-router"], + webDeploy: "wrangler", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should fail with web deploy but no web frontend", async () => { + const result = await runTRPCTest({ + projectName: "web-deploy-no-frontend-fail", + frontend: ["native-nativewind"], + webDeploy: "wrangler", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + addons: ["none"], + examples: ["none"], + dbSetup: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "'--web-deploy' requires a web frontend"); + }); + }); +}); diff --git a/apps/cli/test/index.test.ts b/apps/cli/test/index.test.ts new file mode 100644 index 0000000..a9ae2f4 --- /dev/null +++ b/apps/cli/test/index.test.ts @@ -0,0 +1,83 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { expectSuccess, runTRPCTest } from "./test-utils"; + +describe("CLI Test Suite", () => { + beforeAll(async () => { + // Ensure CLI is built before running tests + console.log("Setting up CLI tests..."); + }); + + afterAll(async () => { + console.log("CLI tests completed."); + }); + + describe("Smoke Tests", () => { + it("should create a basic project successfully", async () => { + const result = await runTRPCTest({ + projectName: "smoke-test-basic", + yes: true, + install: false, + }); + + expectSuccess(result); + }); + + it("should handle help command", async () => { + // This test would need to be implemented differently since it's not a project creation + // For now, we'll just test that the basic functionality works + expect(true).toBe(true); + }); + + it("should validate project name requirements", async () => { + const result = await runTRPCTest({ + projectName: "valid-project-name", + yes: true, + install: false, + }); + + expectSuccess(result); + }); + }); + + describe("Performance Tests", () => { + it("should complete project creation within reasonable time", async () => { + const startTime = Date.now(); + + const result = await runTRPCTest({ + projectName: "performance-test", + yes: true, + install: false, + }); + + const endTime = Date.now(); + const duration = endTime - startTime; + + expectSuccess(result); + + // Should complete within 30 seconds (without installation) + expect(duration).toBeLessThan(30000); + }); + }); + + describe("Stability Tests", () => { + it("should handle multiple rapid project creations", async () => { + const promises = []; + + for (let i = 0; i < 3; i++) { + promises.push( + runTRPCTest({ + projectName: `stability-test-${i}`, + yes: true, + install: false, + }), + ); + } + + const results = await Promise.all(promises); + + for (const result of results) { + expectSuccess(result); + } + }); + }); +}); diff --git a/apps/cli/test/integration.test.ts b/apps/cli/test/integration.test.ts new file mode 100644 index 0000000..cb7aa9a --- /dev/null +++ b/apps/cli/test/integration.test.ts @@ -0,0 +1,541 @@ +import { describe, it } from "vitest"; +import type { Backend, Runtime } from "../src/types"; +import { + expectError, + expectSuccess, + runTRPCTest, + type TestConfig, +} from "./test-utils"; + +describe("Integration Tests - Real World Scenarios", () => { + describe("Complete Stack Configurations", () => { + it("should create full-stack React app with tRPC", async () => { + const result = await runTRPCTest({ + projectName: "fullstack-react-trpc", + backend: "hono", + runtime: "workers", + database: "postgres", + orm: "drizzle", + auth: "better-auth", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["biome", "turborepo"], + examples: ["todo", "ai"], + dbSetup: "none", + webDeploy: "wrangler", + serverDeploy: "wrangler", + install: false, + }); + + expectSuccess(result); + }); + + it("should create Nuxt app with oRPC", async () => { + const result = await runTRPCTest({ + projectName: "nuxt-orpc-app", + backend: "hono", + runtime: "workers", + database: "sqlite", + orm: "drizzle", + auth: "better-auth", + api: "orpc", + frontend: ["nuxt"], + addons: ["biome", "husky"], + examples: ["ai"], // AI works with Nuxt + dbSetup: "none", + webDeploy: "alchemy", + serverDeploy: "alchemy", + install: false, + }); + + expectSuccess(result); + }); + + it("should create Svelte app with oRPC", async () => { + const result = await runTRPCTest({ + projectName: "svelte-orpc-app", + backend: "hono", + runtime: "bun", + database: "mysql", + orm: "prisma", + auth: "better-auth", + api: "orpc", + frontend: ["svelte"], + addons: ["turborepo", "oxlint"], + examples: ["todo"], // Todo works with Svelte + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should create Convex app with Clerk auth", async () => { + const result = await runTRPCTest({ + projectName: "convex-clerk-app", + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + auth: "clerk", + api: "none", + frontend: ["tanstack-router"], + addons: ["biome", "turborepo"], + examples: ["todo", "ai"], + dbSetup: "none", + webDeploy: "wrangler", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should create mobile app with React Native", async () => { + const result = await runTRPCTest({ + projectName: "mobile-app", + backend: "hono", + runtime: "bun", + database: "postgres", + orm: "drizzle", + auth: "better-auth", + api: "trpc", + frontend: ["native-nativewind"], + addons: ["biome", "turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should create hybrid web + mobile app", async () => { + const result = await runTRPCTest({ + projectName: "hybrid-web-mobile", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "better-auth", + api: "trpc", + frontend: ["tanstack-router", "native-unistyles"], + addons: ["biome", "turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should create Cloudflare Workers app", async () => { + const result = await runTRPCTest({ + projectName: "cloudflare-workers-app", + backend: "hono", + runtime: "workers", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["biome"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "wrangler", + install: false, + }); + + expectSuccess(result); + }); + + it("should create MongoDB + Mongoose app", async () => { + const result = await runTRPCTest({ + projectName: "mongodb-mongoose-app", + backend: "express", + runtime: "node", + database: "mongodb", + orm: "mongoose", + auth: "better-auth", + api: "trpc", + frontend: ["react-router"], + addons: ["husky", "turborepo"], + examples: ["todo"], + dbSetup: "none", + webDeploy: "alchemy", + serverDeploy: "alchemy", + install: false, + }); + + expectSuccess(result); + }); + + it("should create Next.js fullstack app", async () => { + const result = await runTRPCTest({ + projectName: "nextjs-fullstack", + backend: "next", + runtime: "node", + database: "postgres", + orm: "prisma", + auth: "better-auth", + api: "trpc", + frontend: ["next"], + addons: ["biome", "turborepo", "pwa"], + examples: ["ai"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should create Solid.js app with oRPC", async () => { + const result = await runTRPCTest({ + projectName: "solid-orpc-app", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "better-auth", + api: "orpc", + frontend: ["solid"], + addons: ["biome", "pwa"], + examples: ["todo"], // AI not compatible with Solid + dbSetup: "none", + webDeploy: "wrangler", + serverDeploy: "wrangler", + install: false, + }); + + expectSuccess(result); + }); + }); + + describe("Frontend-only Configurations", () => { + it("should create frontend-only React app", async () => { + const result = await runTRPCTest({ + projectName: "frontend-only-react", + backend: "none", + runtime: "none", + database: "none", + orm: "none", + auth: "none", + api: "none", + frontend: ["tanstack-router"], + addons: ["biome", "pwa"], + examples: ["none"], + dbSetup: "none", + webDeploy: "wrangler", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should create frontend-only Nuxt app", async () => { + const result = await runTRPCTest({ + projectName: "frontend-only-nuxt", + backend: "none", + runtime: "none", + database: "none", + orm: "none", + auth: "none", + api: "none", + frontend: ["nuxt"], + addons: ["biome", "husky"], + examples: ["none"], + dbSetup: "none", + webDeploy: "alchemy", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + }); + + describe("Complex Error Scenarios", () => { + it("should fail with incompatible stack combination", async () => { + // MongoDB + Drizzle is not supported + const result = await runTRPCTest({ + projectName: "incompatible-stack-fail", + backend: "hono", + runtime: "bun", + database: "mongodb", + orm: "drizzle", // Not compatible with MongoDB + auth: "better-auth", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "Drizzle ORM does not support MongoDB"); + }); + + it("should fail with workers + incompatible database", async () => { + const result = await runTRPCTest({ + projectName: "workers-mongodb-fail", + backend: "hono", + runtime: "workers", + database: "mongodb", // Not compatible with Workers + orm: "mongoose", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "wrangler", + expectError: true, + }); + + expectError( + result, + "Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database", + ); + }); + + it("should fail with tRPC + incompatible frontend", async () => { + const result = await runTRPCTest({ + projectName: "trpc-nuxt-fail", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["nuxt"], // tRPC not compatible with Nuxt + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "tRPC API is not supported with 'nuxt' frontend"); + }); + + it("should fail with Clerk + incompatible frontend", async () => { + const result = await runTRPCTest({ + projectName: "clerk-svelte-fail", + backend: "convex", + runtime: "none", + database: "none", + orm: "none", + auth: "clerk", + api: "none", + frontend: ["svelte"], // Clerk + Convex not compatible with Svelte + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "Clerk authentication is not compatible"); + }); + + it("should fail with addon incompatibility", async () => { + const result = await runTRPCTest({ + projectName: "pwa-native-fail", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["native-nativewind"], + addons: ["pwa"], // PWA not compatible with native-only + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "pwa addon requires one of these frontends"); + }); + + it("should fail with example incompatibility", async () => { + const result = await runTRPCTest({ + projectName: "ai-solid-fail", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "orpc", + frontend: ["solid"], + addons: ["none"], + examples: ["ai"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + expectError: true, + }); + + expectError( + result, + "The 'ai' example is not compatible with the Solid frontend", + ); + }); + + it("should fail with deployment constraint violation", async () => { + const result = await runTRPCTest({ + projectName: "web-deploy-no-frontend-fail", + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["native-nativewind"], // Only native, no web + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "wrangler", // Requires web frontend + serverDeploy: "none", + expectError: true, + }); + + expectError(result, "'--web-deploy' requires a web frontend"); + }); + }); + + describe("Edge Case Combinations", () => { + it("should handle maximum complexity configuration", async () => { + const result = await runTRPCTest({ + projectName: "max-complexity", + backend: "hono", + runtime: "bun", + database: "postgres", + orm: "drizzle", + auth: "better-auth", + api: "trpc", + frontend: ["tanstack-router", "native-nativewind"], + addons: ["biome", "husky", "turborepo"], + examples: ["todo", "ai"], + dbSetup: "none", + webDeploy: "wrangler", + serverDeploy: "wrangler", + install: false, + }); + + expectSuccess(result); + }); + + it("should handle minimal configuration", async () => { + const result = await runTRPCTest({ + projectName: "minimal-config", + backend: "none", + runtime: "none", + database: "none", + orm: "none", + auth: "none", + api: "none", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + }); + + it("should handle all package managers", async () => { + const packageManagers = ["npm", "pnpm", "bun"]; + + for (const packageManager of packageManagers) { + const result = await runTRPCTest({ + projectName: `pkg-manager-${packageManager}`, + backend: "hono", + runtime: "bun", + database: "sqlite", + orm: "drizzle", + auth: "none", + api: "trpc", + frontend: ["tanstack-router"], + addons: ["none"], + examples: ["none"], + dbSetup: "none", + webDeploy: "none", + serverDeploy: "none", + install: false, + }); + + expectSuccess(result); + } + }); + + it("should handle different runtime environments", async () => { + const runtimeConfigs = [ + { runtime: "bun", backend: "hono" }, + { runtime: "node", backend: "express" }, + { runtime: "workers", backend: "hono" }, + { runtime: "none", backend: "convex" }, + ]; + + for (const { runtime, backend } of runtimeConfigs) { + const config: TestConfig = { + projectName: `runtime-${runtime}-${backend}`, + runtime: runtime as Runtime, + backend: backend as Backend, + frontend: ["tanstack-router"], + install: false, + }; + + // Set appropriate defaults + if (backend === "convex") { + config.database = "none"; + config.orm = "none"; + config.auth = "clerk"; + config.api = "none"; + config.addons = ["none"]; + config.examples = ["none"]; + config.dbSetup = "none"; + config.webDeploy = "none"; + config.serverDeploy = "none"; + } else { + config.database = "sqlite"; + config.orm = "drizzle"; + config.auth = "none"; + config.api = "trpc"; + config.addons = ["none"]; + config.examples = ["none"]; + config.dbSetup = "none"; + config.webDeploy = "none"; + config.serverDeploy = "none"; + } + + // Handle workers runtime requirements + if (runtime === "workers") { + config.serverDeploy = "wrangler"; + } + + const result = await runTRPCTest(config); + expectSuccess(result); + } + }); + }); +}); diff --git a/apps/cli/test/programmatic-api.test.ts b/apps/cli/test/programmatic-api.test.ts deleted file mode 100644 index 9819c74..0000000 --- a/apps/cli/test/programmatic-api.test.ts +++ /dev/null @@ -1,561 +0,0 @@ -import { join } from "node:path"; -import { ensureDir, existsSync, readFile, remove } from "fs-extra"; -import { parse as parseJsonc } from "jsonc-parser"; -import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { init } from "../src/index"; -import type { BetterTStackConfig } from "../src/types"; - -let testCounter = 0; -let tmpDir: string; -let originalCwd: string; - -async function createTmpDir() { - testCounter++; - const dir = join(__dirname, "..", `.prog-test-${testCounter}`); - if (existsSync(dir)) { - await remove(dir); - } - await ensureDir(dir); - return dir; -} - -function assertProjectExists(dir: string) { - expect(existsSync(dir)).toBe(true); - expect(existsSync(join(dir, "package.json"))).toBe(true); - expect(existsSync(join(dir, "bts.jsonc"))).toBe(true); -} - -async function assertBtsConfig( - dir: string, - expectedConfig: Partial<{ - frontend: string[]; - backend: string; - database: string; - orm: string; - api: string; - runtime: string; - addons: string[]; - }>, -) { - const configPath = join(dir, "bts.jsonc"); - expect(existsSync(configPath)).toBe(true); - - const configContent = await readFile(configPath, "utf-8"); - const config: BetterTStackConfig = parseJsonc(configContent); - - if (expectedConfig.frontend) { - expect(config.frontend).toEqual(expectedConfig.frontend); - } - if (expectedConfig.backend) { - expect(config.backend).toBe(expectedConfig.backend); - } - if (expectedConfig.database) { - expect(config.database).toBe(expectedConfig.database); - } - if (expectedConfig.orm) { - expect(config.orm).toBe(expectedConfig.orm); - } - if (expectedConfig.api) { - expect(config.api).toBe(expectedConfig.api); - } - if (expectedConfig.runtime) { - expect(config.runtime).toBe(expectedConfig.runtime); - } - if (expectedConfig.addons) { - expect(config.addons).toEqual(expectedConfig.addons); - } -} - -describe("Programmatic API - Fast Tests", () => { - beforeEach(async () => { - originalCwd = process.cwd(); - tmpDir = await createTmpDir(); - process.chdir(tmpDir); - }); - - afterEach(async () => { - process.chdir(originalCwd); - if (existsSync(tmpDir)) { - await remove(tmpDir); - } - }); - - describe("Core functionality", () => { - test("creates minimal project successfully", async () => { - const result = await init("test-app", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "bun", - database: "sqlite", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - }); - - expect(result.success).toBe(true); - expect(result.projectConfig.projectName).toBe("test-app"); - expect(result.projectDirectory).toContain("test-app"); - expect(result.reproducibleCommand).toContain("test-app"); - expect(typeof result.elapsedTimeMs).toBe("number"); - expect(result.elapsedTimeMs).toBeGreaterThan(0); - - assertProjectExists(result.projectDirectory); - }, 15000); - - test("returns complete result structure", async () => { - const result = await init("result-test", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "bun", - database: "sqlite", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - }); - - expect(result).toHaveProperty("success"); - expect(result).toHaveProperty("projectConfig"); - expect(result).toHaveProperty("reproducibleCommand"); - expect(result).toHaveProperty("timeScaffolded"); - expect(result).toHaveProperty("elapsedTimeMs"); - expect(result).toHaveProperty("projectDirectory"); - expect(result).toHaveProperty("relativePath"); - }, 15000); - - test("handles project with custom name", async () => { - const result = await init("custom-name", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "bun", - database: "sqlite", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - }); - - expect(result.success).toBe(true); - expect(result.projectConfig.projectName).toBe("custom-name"); - expect(result.projectDirectory).toContain("custom-name"); - }, 15000); - }); - - describe("Configuration options", () => { - test("creates project with Next.js frontend", async () => { - const result = await init("next-app", { - frontend: ["next"], - backend: "hono", - runtime: "bun", - database: "sqlite", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - }); - - expect(result.success).toBe(true); - await assertBtsConfig(result.projectDirectory, { - frontend: ["next"], - }); - }, 15000); - - test("creates project with Fastify backend", async () => { - const result = await init("fastify-app", { - frontend: ["tanstack-router"], - backend: "fastify", - runtime: "bun", - database: "sqlite", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - }); - - expect(result.success).toBe(true); - await assertBtsConfig(result.projectDirectory, { - backend: "fastify", - }); - }, 15000); - - test("creates project with PostgreSQL + Prisma", async () => { - const result = await init("pg-app", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "bun", - database: "postgres", - orm: "prisma", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - }); - - expect(result.success).toBe(true); - await assertBtsConfig(result.projectDirectory, { - database: "postgres", - orm: "prisma", - }); - }, 15000); - - test("creates project with oRPC API", async () => { - const result = await init("orpc-app", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "bun", - database: "sqlite", - orm: "drizzle", - api: "orpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - }); - - expect(result.success).toBe(true); - await assertBtsConfig(result.projectDirectory, { - api: "orpc", - }); - }, 15000); - - test("creates project with Node runtime", async () => { - const result = await init("node-app", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "node", - database: "sqlite", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - }); - - expect(result.success).toBe(true); - await assertBtsConfig(result.projectDirectory, { - runtime: "node", - }); - }, 15000); - - test("creates project with Biome addon", async () => { - const result = await init("biome-app", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "bun", - database: "sqlite", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["biome"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - }); - - expect(result.success).toBe(true); - await assertBtsConfig(result.projectDirectory, { - addons: ["biome"], - }); - }, 15000); - - test("creates project with analytics disabled", async () => { - const result = await init("no-analytics-app", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "bun", - database: "sqlite", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - disableAnalytics: true, - install: false, - git: false, - }); - - expect(result.success).toBe(true); - expect(result.projectConfig.projectName).toBe("no-analytics-app"); - }, 15000); - }); - - describe("Error scenarios", () => { - test("handles invalid project name", async () => { - await expect( - init("", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "bun", - database: "sqlite", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - }), - ).rejects.toThrow("Project name cannot be empty"); - }); - - test("handles invalid characters in project name", async () => { - await expect( - init("invalid", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "bun", - database: "sqlite", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - }), - ).rejects.toThrow("invalid characters"); - }); - - test("handles incompatible database + ORM combination", async () => { - await expect( - init("incompatible", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "bun", - database: "mongodb", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - yolo: false, - }), - ).rejects.toThrow(/Drizzle ORM does not support MongoDB/); - }); - - test("handles auth without database", async () => { - await expect( - init("auth-no-db", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "bun", - database: "none", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - yolo: false, - }), - ).rejects.toThrow(/ORM selection requires a database/); - }); - - test("handles directory conflict with error strategy", async () => { - const result1 = await init("conflict-test", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "bun", - database: "sqlite", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - }); - - expect(result1.success).toBe(true); - - const result2 = await init("conflict-test", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "bun", - database: "sqlite", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - directoryConflict: "error", - }); - - expect(result2.success).toBe(false); - expect(result2.error).toMatch(/already exists/); - }, 20000); - }); - - describe("Advanced features", () => { - test("creates project with multiple addons", async () => { - const result = await init("multi-addon", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "bun", - database: "sqlite", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["biome", "turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - }); - - expect(result.success).toBe(true); - await assertBtsConfig(result.projectDirectory, { - addons: ["biome", "turborepo"], - }); - }, 15000); - - test("creates project with authentication enabled", async () => { - const result = await init("auth-app", { - frontend: ["tanstack-router"], - backend: "hono", - runtime: "bun", - database: "sqlite", - orm: "drizzle", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - }); - - expect(result.success).toBe(true); - await assertBtsConfig(result.projectDirectory, { - database: "sqlite", - orm: "drizzle", - }); - expect(result.projectConfig.auth).toBe("better-auth"); - }, 15000); - - test("validates reproducible command format", async () => { - const result = await init("repro-test", { - frontend: ["next"], - backend: "fastify", - runtime: "bun", - database: "postgres", - orm: "prisma", - api: "trpc", - auth: "better-auth", - dbSetup: "none", - webDeploy: "none", - serverDeploy: "none", - addons: ["turborepo"], - examples: ["none"], - packageManager: "bun", - install: false, - git: false, - }); - - expect(result.success).toBe(true); - expect(result.reproducibleCommand).toContain("repro-test"); - expect(result.reproducibleCommand).toContain("--frontend next"); - expect(result.reproducibleCommand).toContain("--backend fastify"); - expect(result.reproducibleCommand).toContain("--database postgres"); - expect(result.reproducibleCommand).toContain("--orm prisma"); - expect(result.reproducibleCommand).toContain("--no-install"); - expect(result.reproducibleCommand).toContain("--no-git"); - }, 15000); - }); -}); diff --git a/apps/cli/test/test-utils.ts b/apps/cli/test/test-utils.ts new file mode 100644 index 0000000..6597850 --- /dev/null +++ b/apps/cli/test/test-utils.ts @@ -0,0 +1,245 @@ +import { rm } from "node:fs/promises"; +import { join } from "node:path"; +import { ensureDir } from "fs-extra"; +import { trpcServer } from "trpc-cli"; +import { expect } from "vitest"; +import { router } from "../src/index"; +import type { CreateInput, InitResult } from "../src/types"; +import { + AddonsSchema, + APISchema, + AuthSchema, + BackendSchema, + DatabaseSchema, + DatabaseSetupSchema, + ExamplesSchema, + FrontendSchema, + ORMSchema, + PackageManagerSchema, + RuntimeSchema, + ServerDeploySchema, + WebDeploySchema, +} from "../src/types"; + +// Create tRPC caller for direct function calls instead of subprocess +const t = trpcServer.initTRPC.create(); +const defaultContext = {}; + +/** + * Clean up the entire .smoke directory + */ +export async function cleanupSmokeDirectory() { + const smokeDir = join(process.cwd(), ".smoke"); + try { + await rm(smokeDir, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors + } +} + +export interface TestResult { + success: boolean; + result?: InitResult; + error?: string; + projectDir?: string; +} + +export interface TestConfig extends CreateInput { + projectName?: string; + expectError?: boolean; + expectedErrorMessage?: string; +} + +/** + * Run tRPC test using direct function calls instead of subprocess + * This delegates all validation to the CLI's existing logic - much simpler! + */ +export async function runTRPCTest(config: TestConfig): Promise { + const smokeDir = join(process.cwd(), ".smoke"); + await ensureDir(smokeDir); + + // Store original environment + const originalProgrammatic = process.env.BTS_PROGRAMMATIC; + + try { + // Set programmatic mode to ensure errors are thrown instead of process.exit + process.env.BTS_PROGRAMMATIC = "1"; + + const caller = t.createCallerFactory(router)(defaultContext); + const projectName = config.projectName || "default-app"; + const projectPath = join(smokeDir, projectName); + + // Determine if we should use --yes or not + // Only core stack flags conflict with --yes flag (from CLI error message) + const coreStackFlags: (keyof TestConfig)[] = [ + "database", + "orm", + "backend", + "runtime", + "frontend", + "addons", + "examples", + "auth", + "dbSetup", + "api", + "webDeploy", + "serverDeploy", + ]; + const hasSpecificCoreConfig = coreStackFlags.some( + (flag) => config[flag] !== undefined, + ); + + // Only use --yes if no core stack flags are provided and not explicitly disabled + const willUseYesFlag = + config.yes !== undefined ? config.yes : !hasSpecificCoreConfig; + + // Provide defaults for missing core stack options to avoid prompts + // But don't provide core stack defaults when yes: true is explicitly set + const coreStackDefaults = willUseYesFlag + ? {} + : { + frontend: ["tanstack-router"] as Frontend[], + backend: "hono" as Backend, + runtime: "bun" as Runtime, + api: "trpc" as API, + database: "sqlite" as Database, + orm: "drizzle" as ORM, + auth: "none" as Auth, + addons: ["none"] as Addons[], + examples: ["none"] as Examples[], + dbSetup: "none" as DatabaseSetup, + webDeploy: "none" as WebDeploy, + serverDeploy: "none" as ServerDeploy, + }; + + // Build options object - let the CLI handle all validation + const options: CreateInput = { + renderTitle: false, + install: config.install ?? false, + git: config.git ?? true, + packageManager: config.packageManager ?? "bun", + directoryConflict: "overwrite", + verbose: true, // Need verbose to get the result + disableAnalytics: true, + yes: willUseYesFlag, + ...coreStackDefaults, + ...config, + }; + + // Remove our test-specific properties + const { + projectName: _, + expectError: __, + expectedErrorMessage: ___, + ...cleanOptions + } = options as TestConfig; + + const result = await caller.init([projectPath, cleanOptions]); + + return { + success: result?.success ?? false, + result: result?.success ? result : undefined, + error: result?.success ? undefined : result?.error, + projectDir: result?.success ? result?.projectDirectory : undefined, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } finally { + // Always restore original environment + if (originalProgrammatic === undefined) { + delete process.env.BTS_PROGRAMMATIC; + } else { + process.env.BTS_PROGRAMMATIC = originalProgrammatic; + } + } +} + +export function expectSuccess(result: TestResult) { + if (!result.success) { + console.error("Test failed:"); + console.error("Error:", result.error); + if (result.result) { + console.error("Result:", result.result); + } + } + expect(result.success).toBe(true); + expect(result.result).toBeDefined(); +} + +export function expectError(result: TestResult, expectedMessage?: string) { + expect(result.success).toBe(false); + if (expectedMessage) { + expect(result.error).toContain(expectedMessage); + } +} + +// Helper function to create properly typed test configs +export function createTestConfig( + config: Partial & { projectName: string }, +): TestConfig { + return config as TestConfig; +} + +/** + * Extract enum values from a Zod enum schema + */ +function extractEnumValues(schema: { + options: readonly T[]; +}): readonly T[] { + return schema.options; +} + +// Inferred types and values from Zod schemas - no duplication with types.ts! +export type PackageManager = (typeof PackageManagerSchema)["options"][number]; +export type Database = (typeof DatabaseSchema)["options"][number]; +export type ORM = (typeof ORMSchema)["options"][number]; +export type Backend = (typeof BackendSchema)["options"][number]; +export type Runtime = (typeof RuntimeSchema)["options"][number]; +export type Frontend = (typeof FrontendSchema)["options"][number]; +export type Addons = (typeof AddonsSchema)["options"][number]; +export type Examples = (typeof ExamplesSchema)["options"][number]; +export type Auth = (typeof AuthSchema)["options"][number]; +export type API = (typeof APISchema)["options"][number]; +export type WebDeploy = (typeof WebDeploySchema)["options"][number]; +export type ServerDeploy = (typeof ServerDeploySchema)["options"][number]; +export type DatabaseSetup = (typeof DatabaseSetupSchema)["options"][number]; + +// Test data generators inferred from Zod schemas +export const PACKAGE_MANAGERS = extractEnumValues(PackageManagerSchema); +export const DATABASES = extractEnumValues(DatabaseSchema); +export const ORMS = extractEnumValues(ORMSchema); +export const BACKENDS = extractEnumValues(BackendSchema); +export const RUNTIMES = extractEnumValues(RuntimeSchema); +export const FRONTENDS = extractEnumValues(FrontendSchema); +export const ADDONS = extractEnumValues(AddonsSchema); +export const EXAMPLES = extractEnumValues(ExamplesSchema); +export const AUTH_PROVIDERS = extractEnumValues(AuthSchema); +export const API_TYPES = extractEnumValues(APISchema); +export const WEB_DEPLOYS = extractEnumValues(WebDeploySchema); +export const SERVER_DEPLOYS = extractEnumValues(ServerDeploySchema); +export const DB_SETUPS = extractEnumValues(DatabaseSetupSchema); + +// Convenience functions for common test patterns +export function createBasicConfig( + overrides: Partial = {}, +): TestConfig { + return { + projectName: "test-app", + yes: true, // Use defaults + install: false, + git: true, + ...overrides, + }; +} + +export function createCustomConfig(config: Partial): TestConfig { + return { + projectName: "test-app", + install: false, + git: true, + ...config, + }; +}