mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
cli: organize file structure
This commit is contained in:
@@ -27,7 +27,7 @@ Follow the prompts to configure your project or use the `--yes` flag for default
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **TypeScript** | End-to-end type safety across all parts of your application |
|
||||
| **Frontend** | • React with TanStack Router<br>• React with React Router<br>• React with TanStack Start (SSR)<br>• Next.js<br>• SvelteKit<br>• Nuxt (Vue)<br>• SolidJS<br>• React Native with NativeWind (via Expo)<br>• React Native with Unistyles (via Expo)<br>• None |
|
||||
| **Backend** | • Hono<br>• Express<br>• Elysia<br>• Next.js API routes<br>• Convex<br>• None |
|
||||
| **Backend** | • Hono<br>• Express<br>• Elysia<br>• Next.js API routes<br>• Convex<br>• Fastify<br>• None |
|
||||
| **API Layer** | • tRPC (type-safe APIs)<br>• oRPC (OpenAPI-compatible type-safe APIs)<br>• None |
|
||||
| **Runtime** | • Bun<br>• Node.js |
|
||||
| **Database** | • SQLite<br>• PostgreSQL<br>• MySQL<br>• MongoDB<br>• None |
|
||||
@@ -60,7 +60,7 @@ Options:
|
||||
--install Install dependencies
|
||||
--no-install Skip installing dependencies
|
||||
--db-setup <setup> Database setup (turso, neon, prisma-postgres, mongodb-atlas, none)
|
||||
--backend <framework> Backend framework (hono, express, elysia, next, convex)
|
||||
--backend <framework> Backend framework (hono, express, elysia, next, convex, fastify, none)
|
||||
--runtime <runtime> Runtime (bun, node, none)
|
||||
--api <type> API type (trpc, orpc, none)
|
||||
-h, --help Display help
|
||||
|
||||
134
apps/cli/src/cli.ts
Normal file
134
apps/cli/src/cli.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
import type { YargsArgv } from "./types";
|
||||
import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
|
||||
|
||||
export async function parseCliArguments(): Promise<YargsArgv> {
|
||||
const argv = await yargs(hideBin(process.argv))
|
||||
.scriptName("create-better-t-stack")
|
||||
.usage(
|
||||
"$0 [project-directory] [options]",
|
||||
"Create a new Better-T Stack project",
|
||||
)
|
||||
.positional("project-directory", {
|
||||
describe: "Project name/directory",
|
||||
type: "string",
|
||||
})
|
||||
.option("yes", {
|
||||
alias: "y",
|
||||
type: "boolean",
|
||||
describe: "Use default configuration and skip prompts",
|
||||
default: false,
|
||||
})
|
||||
.option("database", {
|
||||
type: "string",
|
||||
describe: "Database type",
|
||||
choices: ["none", "sqlite", "postgres", "mysql", "mongodb"],
|
||||
})
|
||||
.option("orm", {
|
||||
type: "string",
|
||||
describe: "ORM type",
|
||||
choices: ["drizzle", "prisma", "mongoose", "none"],
|
||||
})
|
||||
.option("auth", {
|
||||
type: "boolean",
|
||||
describe: "Include authentication (use --no-auth to exclude)",
|
||||
})
|
||||
.option("frontend", {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Frontend types",
|
||||
choices: [
|
||||
"tanstack-router",
|
||||
"react-router",
|
||||
"tanstack-start",
|
||||
"next",
|
||||
"nuxt",
|
||||
"native-nativewind",
|
||||
"native-unistyles",
|
||||
"svelte",
|
||||
"solid",
|
||||
"none",
|
||||
],
|
||||
})
|
||||
.option("addons", {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Additional addons",
|
||||
choices: [
|
||||
"pwa",
|
||||
"tauri",
|
||||
"starlight",
|
||||
"biome",
|
||||
"husky",
|
||||
"turborepo",
|
||||
"none",
|
||||
],
|
||||
})
|
||||
.option("examples", {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Examples to include",
|
||||
choices: ["todo", "ai", "none"],
|
||||
})
|
||||
.option("git", {
|
||||
type: "boolean",
|
||||
describe: "Initialize git repository (use --no-git to skip)",
|
||||
})
|
||||
.option("package-manager", {
|
||||
alias: "pm",
|
||||
type: "string",
|
||||
describe: "Package manager",
|
||||
choices: ["npm", "pnpm", "bun"],
|
||||
})
|
||||
.option("install", {
|
||||
type: "boolean",
|
||||
describe: "Install dependencies (use --no-install to skip)",
|
||||
})
|
||||
.option("db-setup", {
|
||||
type: "string",
|
||||
describe: "Database setup",
|
||||
choices: [
|
||||
"turso",
|
||||
"neon",
|
||||
"prisma-postgres",
|
||||
"mongodb-atlas",
|
||||
"supabase",
|
||||
"none",
|
||||
],
|
||||
})
|
||||
.option("backend", {
|
||||
type: "string",
|
||||
describe: "Backend framework",
|
||||
choices: [
|
||||
"hono",
|
||||
"express",
|
||||
"fastify",
|
||||
"next",
|
||||
"elysia",
|
||||
"convex",
|
||||
"none",
|
||||
],
|
||||
})
|
||||
.option("runtime", {
|
||||
type: "string",
|
||||
describe: "Runtime",
|
||||
choices: ["bun", "node", "none"],
|
||||
})
|
||||
.option("api", {
|
||||
type: "string",
|
||||
describe: "API type",
|
||||
choices: ["trpc", "orpc", "none"],
|
||||
})
|
||||
.completion()
|
||||
.recommendCommands()
|
||||
.version(getLatestCLIVersion())
|
||||
.alias("version", "v")
|
||||
.help()
|
||||
.alias("help", "h")
|
||||
.strict()
|
||||
.wrap(null)
|
||||
.parse();
|
||||
|
||||
return argv as YargsArgv;
|
||||
}
|
||||
@@ -4,9 +4,12 @@ import consola from "consola";
|
||||
import { execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectConfig } from "../types";
|
||||
import { commandExists } from "../utils/command-exists";
|
||||
import { type EnvVariable, addEnvVariablesToFile } from "./env-setup";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { commandExists } from "../../utils/command-exists";
|
||||
import {
|
||||
type EnvVariable,
|
||||
addEnvVariablesToFile,
|
||||
} from "../project-generation/env-setup";
|
||||
|
||||
type MongoDBConfig = {
|
||||
connectionString: string;
|
||||
@@ -4,9 +4,12 @@ import { consola } from "consola";
|
||||
import { execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectPackageManager } from "../types";
|
||||
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
|
||||
import { type EnvVariable, addEnvVariablesToFile } from "./env-setup";
|
||||
import type { PackageManager } from "../../types";
|
||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
||||
import {
|
||||
type EnvVariable,
|
||||
addEnvVariablesToFile,
|
||||
} from "../project-generation/env-setup";
|
||||
|
||||
type NeonConfig = {
|
||||
connectionString: string;
|
||||
@@ -31,7 +34,7 @@ const NEON_REGIONS: NeonRegion[] = [
|
||||
];
|
||||
|
||||
async function executeNeonCommand(
|
||||
packageManager: ProjectPackageManager,
|
||||
packageManager: PackageManager,
|
||||
commandArgsString: string,
|
||||
spinnerText?: string,
|
||||
) {
|
||||
@@ -52,7 +55,7 @@ async function executeNeonCommand(
|
||||
}
|
||||
}
|
||||
|
||||
async function isNeonAuthenticated(packageManager: ProjectPackageManager) {
|
||||
async function isNeonAuthenticated(packageManager: PackageManager) {
|
||||
try {
|
||||
const commandArgsString = "neonctl projects list";
|
||||
const result = await executeNeonCommand(packageManager, commandArgsString);
|
||||
@@ -65,7 +68,7 @@ async function isNeonAuthenticated(packageManager: ProjectPackageManager) {
|
||||
}
|
||||
}
|
||||
|
||||
async function authenticateWithNeon(packageManager: ProjectPackageManager) {
|
||||
async function authenticateWithNeon(packageManager: PackageManager) {
|
||||
try {
|
||||
await executeNeonCommand(
|
||||
packageManager,
|
||||
@@ -82,7 +85,7 @@ async function authenticateWithNeon(packageManager: ProjectPackageManager) {
|
||||
async function createNeonProject(
|
||||
projectName: string,
|
||||
regionId: string,
|
||||
packageManager: ProjectPackageManager,
|
||||
packageManager: PackageManager,
|
||||
) {
|
||||
try {
|
||||
const commandArgsString = `neonctl projects create --name ${projectName} --region-id ${regionId} --output json`;
|
||||
@@ -146,7 +149,7 @@ function displayManualSetupInstructions() {
|
||||
DATABASE_URL="your_connection_string"`);
|
||||
}
|
||||
|
||||
import type { ProjectConfig } from "../types";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
|
||||
export async function setupNeonPostgres(config: ProjectConfig): Promise<void> {
|
||||
const { packageManager, projectDir } = config;
|
||||
@@ -4,10 +4,13 @@ import { consola } from "consola";
|
||||
import { execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectPackageManager } from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
|
||||
import { type EnvVariable, addEnvVariablesToFile } from "./env-setup";
|
||||
import type { PackageManager } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
||||
import {
|
||||
type EnvVariable,
|
||||
addEnvVariablesToFile,
|
||||
} from "../project-generation/env-setup";
|
||||
|
||||
type PrismaConfig = {
|
||||
databaseUrl: string;
|
||||
@@ -15,7 +18,7 @@ type PrismaConfig = {
|
||||
|
||||
async function initPrismaDatabase(
|
||||
serverDir: string,
|
||||
packageManager: ProjectPackageManager,
|
||||
packageManager: PackageManager,
|
||||
): Promise<PrismaConfig | null> {
|
||||
const s = spinner();
|
||||
try {
|
||||
@@ -141,7 +144,7 @@ export default prisma;
|
||||
}
|
||||
}
|
||||
|
||||
import type { ProjectConfig } from "../types";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
|
||||
export async function setupPrismaPostgres(config: ProjectConfig) {
|
||||
const { packageManager, projectDir } = config;
|
||||
@@ -4,9 +4,12 @@ import { consola } from "consola";
|
||||
import { type ExecaError, execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectConfig, ProjectPackageManager } from "../types";
|
||||
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
|
||||
import { type EnvVariable, addEnvVariablesToFile } from "./env-setup";
|
||||
import type { PackageManager, ProjectConfig } from "../../types";
|
||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
||||
import {
|
||||
type EnvVariable,
|
||||
addEnvVariablesToFile,
|
||||
} from "../project-generation/env-setup";
|
||||
|
||||
async function writeSupabaseEnvFile(
|
||||
projectDir: string,
|
||||
@@ -50,7 +53,7 @@ function extractDbUrl(output: string): string | null {
|
||||
|
||||
async function initializeSupabase(
|
||||
serverDir: string,
|
||||
packageManager: ProjectPackageManager,
|
||||
packageManager: PackageManager,
|
||||
): Promise<boolean> {
|
||||
log.info("Initializing Supabase project...");
|
||||
try {
|
||||
@@ -86,7 +89,7 @@ async function initializeSupabase(
|
||||
|
||||
async function startSupabase(
|
||||
serverDir: string,
|
||||
packageManager: ProjectPackageManager,
|
||||
packageManager: PackageManager,
|
||||
): Promise<string | null> {
|
||||
log.info("Starting Supabase services (this may take a moment)...");
|
||||
const supabaseStartCommand = getPackageExecutionCommand(
|
||||
@@ -12,8 +12,12 @@ import {
|
||||
import consola from "consola";
|
||||
import { $ } from "execa";
|
||||
import pc from "picocolors";
|
||||
import { commandExists } from "../utils/command-exists";
|
||||
import { type EnvVariable, addEnvVariablesToFile } from "./env-setup";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { commandExists } from "../../utils/command-exists";
|
||||
import {
|
||||
type EnvVariable,
|
||||
addEnvVariablesToFile,
|
||||
} from "../project-generation/env-setup";
|
||||
|
||||
type TursoConfig = {
|
||||
dbUrl: string;
|
||||
@@ -202,8 +206,6 @@ DATABASE_URL=your_database_url
|
||||
DATABASE_AUTH_TOKEN=your_auth_token`);
|
||||
}
|
||||
|
||||
import type { ProjectConfig } from "../types";
|
||||
|
||||
export async function setupTurso(config: ProjectConfig): Promise<void> {
|
||||
const { orm, projectDir } = config;
|
||||
const _isDrizzle = orm === "drizzle";
|
||||
@@ -1,19 +1,19 @@
|
||||
import { cancel, log } from "@clack/prompts";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectConfig } from "../types";
|
||||
import { setupAddons } from "./addons-setup";
|
||||
import { setupApi } from "./api-setup";
|
||||
import { setupAuth } from "./auth-setup";
|
||||
import { setupBackendDependencies } from "./backend-framework-setup";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { setupAddons } from "../setup/addons-setup";
|
||||
import { setupApi } from "../setup/api-setup";
|
||||
import { setupAuth } from "../setup/auth-setup";
|
||||
import { setupBackendDependencies } from "../setup/backend-setup";
|
||||
import { setupDatabase } from "../setup/db-setup";
|
||||
import { setupExamples } from "../setup/examples-setup";
|
||||
import { setupRuntime } from "../setup/runtime-setup";
|
||||
import { createReadme } from "./create-readme";
|
||||
import { setupDatabase } from "./db-setup";
|
||||
import { setupEnvironmentVariables } from "./env-setup";
|
||||
import { setupExamples } from "./examples-setup";
|
||||
import { installDependencies } from "./install-dependencies";
|
||||
import { displayPostInstallInstructions } from "./post-installation";
|
||||
import { initializeGit, updatePackageConfigurations } from "./project-config";
|
||||
import { setupRuntime } from "./runtime-setup";
|
||||
import {
|
||||
copyBaseTemplate,
|
||||
handleExtras,
|
||||
@@ -2,14 +2,14 @@ import path from "node:path";
|
||||
import consola from "consola";
|
||||
import fs from "fs-extra";
|
||||
import type {
|
||||
ProjectAddons,
|
||||
ProjectApi,
|
||||
API,
|
||||
Addons,
|
||||
Database,
|
||||
Frontend,
|
||||
ORM,
|
||||
ProjectConfig,
|
||||
ProjectDatabase,
|
||||
ProjectFrontend,
|
||||
ProjectOrm,
|
||||
ProjectRuntime,
|
||||
} from "../types";
|
||||
Runtime,
|
||||
} from "../../types";
|
||||
|
||||
export async function createReadme(projectDir: string, options: ProjectConfig) {
|
||||
const readmePath = path.join(projectDir, "README.md");
|
||||
@@ -206,14 +206,14 @@ ${generateScriptsList(
|
||||
}
|
||||
|
||||
function generateFeaturesList(
|
||||
database: ProjectDatabase,
|
||||
database: Database,
|
||||
auth: boolean,
|
||||
addons: ProjectAddons[],
|
||||
orm: ProjectOrm,
|
||||
runtime: ProjectRuntime,
|
||||
frontend: ProjectFrontend[],
|
||||
addons: Addons[],
|
||||
orm: ORM,
|
||||
runtime: Runtime,
|
||||
frontend: Frontend[],
|
||||
backend: string,
|
||||
api: ProjectApi,
|
||||
api: API,
|
||||
): string {
|
||||
const isConvex = backend === "convex";
|
||||
const hasTanstackRouter = frontend.includes("tanstack-router");
|
||||
@@ -332,10 +332,10 @@ function generateFeaturesList(
|
||||
}
|
||||
|
||||
function generateDatabaseSetup(
|
||||
database: ProjectDatabase,
|
||||
database: Database,
|
||||
auth: boolean,
|
||||
packageManagerRunCmd: string,
|
||||
orm: ProjectOrm,
|
||||
orm: ORM,
|
||||
): string {
|
||||
if (database === "none") {
|
||||
return "";
|
||||
@@ -405,11 +405,11 @@ ${packageManagerRunCmd} db:push
|
||||
|
||||
function generateScriptsList(
|
||||
packageManagerRunCmd: string,
|
||||
database: ProjectDatabase,
|
||||
orm: ProjectOrm,
|
||||
database: Database,
|
||||
orm: ORM,
|
||||
_auth: boolean,
|
||||
hasNative: boolean,
|
||||
addons: ProjectAddons[],
|
||||
addons: Addons[],
|
||||
backend: string,
|
||||
): string {
|
||||
const isConvex = backend === "convex";
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { ProjectConfig } from "../types";
|
||||
import { generateAuthSecret } from "./auth-setup";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { generateAuthSecret } from "../setup/auth-setup";
|
||||
|
||||
export interface EnvVariable {
|
||||
key: string;
|
||||
@@ -2,7 +2,7 @@ import { log, spinner } from "@clack/prompts";
|
||||
import consola from "consola";
|
||||
import { $ } from "execa";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectAddons, ProjectPackageManager } from "../types";
|
||||
import type { Addons, PackageManager } from "../../types";
|
||||
|
||||
export async function installDependencies({
|
||||
projectDir,
|
||||
@@ -10,8 +10,8 @@ export async function installDependencies({
|
||||
addons = [],
|
||||
}: {
|
||||
projectDir: string;
|
||||
packageManager: ProjectPackageManager;
|
||||
addons?: ProjectAddons[];
|
||||
packageManager: PackageManager;
|
||||
addons?: Addons[];
|
||||
}) {
|
||||
const s = spinner();
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function installDependencies({
|
||||
|
||||
async function runBiomeCheck(
|
||||
projectDir: string,
|
||||
packageManager: ProjectPackageManager,
|
||||
packageManager: PackageManager,
|
||||
) {
|
||||
const s = spinner();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { consola } from "consola";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectDatabase, ProjectOrm, ProjectRuntime } from "../types";
|
||||
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
|
||||
import type { Database, ORM, Runtime } from "../../types";
|
||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
||||
|
||||
import type { ProjectConfig } from "../types";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
|
||||
export function displayPostInstallInstructions(
|
||||
config: ProjectConfig & { depsInstalled: boolean },
|
||||
@@ -157,10 +157,10 @@ function getLintingInstructions(runCmd?: string): string {
|
||||
}
|
||||
|
||||
function getDatabaseInstructions(
|
||||
database: ProjectDatabase,
|
||||
orm?: ProjectOrm,
|
||||
database: Database,
|
||||
orm?: ORM,
|
||||
runCmd?: string,
|
||||
runtime?: ProjectRuntime,
|
||||
runtime?: Runtime,
|
||||
): string {
|
||||
const instructions = [];
|
||||
|
||||
@@ -3,7 +3,7 @@ import { log } from "@clack/prompts";
|
||||
import { $, execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectConfig } from "../types";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
|
||||
export async function updatePackageConfigurations(
|
||||
projectDir: string,
|
||||
@@ -1,9 +1,9 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import { globby } from "globby";
|
||||
import { PKG_ROOT } from "../constants";
|
||||
import type { ProjectConfig } from "../types";
|
||||
import { processTemplate } from "../utils/template-processor";
|
||||
import { PKG_ROOT } from "../../constants";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { processTemplate } from "../../utils/template-processor";
|
||||
|
||||
async function processAndCopyFiles(
|
||||
sourcePattern: string | string[],
|
||||
@@ -1,11 +1,11 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { ProjectFrontend } from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
import type { Frontend } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
import { setupStarlight } from "./starlight-setup";
|
||||
import { setupTauri } from "./tauri-setup";
|
||||
|
||||
import type { ProjectConfig } from "../types";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
|
||||
export async function setupAddons(config: ProjectConfig) {
|
||||
const { addons, frontend, projectDir } = config;
|
||||
@@ -49,10 +49,7 @@ export async function setupAddons(config: ProjectConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
function getWebAppDir(
|
||||
projectDir: string,
|
||||
frontends: ProjectFrontend[],
|
||||
): string {
|
||||
function getWebAppDir(projectDir: string, frontends: Frontend[]): string {
|
||||
if (
|
||||
frontends.some((f) =>
|
||||
["react-router", "tanstack-router", "nuxt", "svelte", "solid"].includes(
|
||||
@@ -109,7 +106,7 @@ async function setupHusky(projectDir: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function setupPwa(projectDir: string, frontends: ProjectFrontend[]) {
|
||||
async function setupPwa(projectDir: string, frontends: Frontend[]) {
|
||||
const isCompatibleFrontend = frontends.some((f) =>
|
||||
["react-router", "tanstack-router", "solid"].includes(f),
|
||||
);
|
||||
@@ -1,8 +1,8 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { AvailableDependencies } from "../constants";
|
||||
import type { ProjectConfig, ProjectFrontend } from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
import type { AvailableDependencies } from "../../constants";
|
||||
import type { Frontend, ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
|
||||
export async function setupApi(config: ProjectConfig): Promise<void> {
|
||||
const { api, projectName, frontend, backend, packageManager, projectDir } =
|
||||
@@ -120,7 +120,7 @@ export async function setupApi(config: ProjectConfig): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const reactBasedFrontends: ProjectFrontend[] = [
|
||||
const reactBasedFrontends: Frontend[] = [
|
||||
"react-router",
|
||||
"tanstack-router",
|
||||
"tanstack-start",
|
||||
@@ -2,8 +2,8 @@ import path from "node:path";
|
||||
import consola from "consola";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectConfig } from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
|
||||
export async function setupAuth(config: ProjectConfig): Promise<void> {
|
||||
const { auth, frontend, backend, projectDir } = config;
|
||||
@@ -1,8 +1,8 @@
|
||||
import path from "node:path";
|
||||
import type { AvailableDependencies } from "../constants";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
import type { AvailableDependencies } from "../../constants";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
|
||||
import type { ProjectConfig } from "../types";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
|
||||
export async function setupBackendDependencies(
|
||||
config: ProjectConfig,
|
||||
@@ -3,15 +3,15 @@ import { spinner } from "@clack/prompts";
|
||||
import consola from "consola";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
import { setupMongoDBAtlas } from "./mongodb-atlas-setup";
|
||||
import { setupPrismaPostgres } from "./prisma-postgres-setup";
|
||||
import { setupSupabase } from "./supabase-setup";
|
||||
import { setupTurso } from "./turso-setup";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup";
|
||||
import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup";
|
||||
import { setupSupabase } from "../database-providers/supabase-setup";
|
||||
import { setupTurso } from "../database-providers/turso-setup";
|
||||
|
||||
import { setupNeonPostgres } from "./neon-setup";
|
||||
import { setupNeonPostgres } from "../database-providers/neon-setup";
|
||||
|
||||
import type { ProjectConfig } from "../types";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
|
||||
export async function setupDatabase(config: ProjectConfig): Promise<void> {
|
||||
const { database, orm, dbSetup, backend, projectDir } = config;
|
||||
@@ -1,8 +1,8 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { AvailableDependencies } from "../constants";
|
||||
import type { ProjectConfig } from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
import type { AvailableDependencies } from "../../constants";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
|
||||
export async function setupExamples(config: ProjectConfig): Promise<void> {
|
||||
const { examples, frontend, backend, projectDir } = config;
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { ProjectBackend, ProjectConfig } from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
import type { Backend, ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
|
||||
export async function setupRuntime(config: ProjectConfig): Promise<void> {
|
||||
const { runtime, backend, projectDir } = config;
|
||||
@@ -25,7 +25,7 @@ export async function setupRuntime(config: ProjectConfig): Promise<void> {
|
||||
|
||||
async function setupBunRuntime(
|
||||
serverDir: string,
|
||||
_backend: ProjectBackend,
|
||||
_backend: Backend,
|
||||
): Promise<void> {
|
||||
const packageJsonPath = path.join(serverDir, "package.json");
|
||||
if (!(await fs.pathExists(packageJsonPath))) return;
|
||||
@@ -48,7 +48,7 @@ async function setupBunRuntime(
|
||||
|
||||
async function setupNodeRuntime(
|
||||
serverDir: string,
|
||||
backend: ProjectBackend,
|
||||
backend: Backend,
|
||||
): Promise<void> {
|
||||
const packageJsonPath = path.join(serverDir, "package.json");
|
||||
if (!(await fs.pathExists(packageJsonPath))) return;
|
||||
@@ -3,8 +3,8 @@ import { spinner } from "@clack/prompts";
|
||||
import consola from "consola";
|
||||
import { execa } from "execa";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectConfig } from "../types";
|
||||
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
||||
|
||||
export async function setupStarlight(config: ProjectConfig): Promise<void> {
|
||||
const { packageManager, projectDir } = config;
|
||||
@@ -4,10 +4,10 @@ import { consola } from "consola";
|
||||
import { execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
||||
|
||||
import type { ProjectConfig } from "../types";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
|
||||
export async function setupTauri(config: ProjectConfig): Promise<void> {
|
||||
const { packageManager, frontend, projectDir } = config;
|
||||
@@ -11,30 +11,16 @@ import {
|
||||
import { consola } from "consola";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
import { parseCliArguments } from "./cli";
|
||||
import { DEFAULT_CONFIG } from "./constants";
|
||||
import { createProject } from "./helpers/create-project";
|
||||
import { createProject } from "./helpers/project-generation/create-project";
|
||||
import { gatherConfig } from "./prompts/config-prompts";
|
||||
import { getProjectName } from "./prompts/project-name";
|
||||
import type {
|
||||
ProjectAddons,
|
||||
ProjectApi,
|
||||
ProjectBackend,
|
||||
ProjectConfig,
|
||||
ProjectDBSetup,
|
||||
ProjectDatabase,
|
||||
ProjectExamples,
|
||||
ProjectFrontend,
|
||||
ProjectOrm,
|
||||
ProjectPackageManager,
|
||||
ProjectRuntime,
|
||||
YargsArgv,
|
||||
} from "./types";
|
||||
import type { ProjectConfig } from "./types";
|
||||
import { displayConfig } from "./utils/display-config";
|
||||
import { generateReproducibleCommand } from "./utils/generate-reproducible-command";
|
||||
import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
|
||||
import { renderTitle } from "./utils/render-title";
|
||||
import { processAndValidateFlags } from "./validation";
|
||||
|
||||
const exit = () => process.exit(0);
|
||||
process.on("SIGINT", exit);
|
||||
@@ -44,133 +30,7 @@ async function main() {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const argv = await yargs(hideBin(process.argv))
|
||||
.scriptName("create-better-t-stack")
|
||||
.usage(
|
||||
"$0 [project-directory] [options]",
|
||||
"Create a new Better-T Stack project",
|
||||
)
|
||||
.positional("project-directory", {
|
||||
describe: "Project name/directory",
|
||||
type: "string",
|
||||
})
|
||||
.option("yes", {
|
||||
alias: "y",
|
||||
type: "boolean",
|
||||
describe: "Use default configuration and skip prompts",
|
||||
default: false,
|
||||
})
|
||||
.option("database", {
|
||||
type: "string",
|
||||
describe: "Database type",
|
||||
choices: ["none", "sqlite", "postgres", "mysql", "mongodb"],
|
||||
})
|
||||
.option("orm", {
|
||||
type: "string",
|
||||
describe: "ORM type",
|
||||
choices: ["drizzle", "prisma", "mongoose", "none"],
|
||||
})
|
||||
.option("auth", {
|
||||
type: "boolean",
|
||||
describe: "Include authentication (use --no-auth to exclude)",
|
||||
})
|
||||
.option("frontend", {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Frontend types",
|
||||
choices: [
|
||||
"tanstack-router",
|
||||
"react-router",
|
||||
"tanstack-start",
|
||||
"next",
|
||||
"nuxt",
|
||||
"native-nativewind",
|
||||
"native-unistyles",
|
||||
"svelte",
|
||||
"solid",
|
||||
"none",
|
||||
],
|
||||
})
|
||||
.option("addons", {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Additional addons",
|
||||
choices: [
|
||||
"pwa",
|
||||
"tauri",
|
||||
"starlight",
|
||||
"biome",
|
||||
"husky",
|
||||
"turborepo",
|
||||
"none",
|
||||
],
|
||||
})
|
||||
.option("examples", {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Examples to include",
|
||||
choices: ["todo", "ai", "none"],
|
||||
})
|
||||
.option("git", {
|
||||
type: "boolean",
|
||||
describe: "Initialize git repository (use --no-git to skip)",
|
||||
})
|
||||
.option("package-manager", {
|
||||
alias: "pm",
|
||||
type: "string",
|
||||
describe: "Package manager",
|
||||
choices: ["npm", "pnpm", "bun"],
|
||||
})
|
||||
.option("install", {
|
||||
type: "boolean",
|
||||
describe: "Install dependencies (use --no-install to skip)",
|
||||
})
|
||||
.option("db-setup", {
|
||||
type: "string",
|
||||
describe: "Database setup",
|
||||
choices: [
|
||||
"turso",
|
||||
"neon",
|
||||
"prisma-postgres",
|
||||
"mongodb-atlas",
|
||||
"supabase",
|
||||
"none",
|
||||
],
|
||||
})
|
||||
.option("backend", {
|
||||
type: "string",
|
||||
describe: "Backend framework",
|
||||
choices: [
|
||||
"hono",
|
||||
"express",
|
||||
"fastify",
|
||||
"next",
|
||||
"elysia",
|
||||
"convex",
|
||||
"none",
|
||||
],
|
||||
})
|
||||
.option("runtime", {
|
||||
type: "string",
|
||||
describe: "Runtime",
|
||||
choices: ["bun", "node", "none"],
|
||||
})
|
||||
.option("api", {
|
||||
type: "string",
|
||||
describe: "API type",
|
||||
choices: ["trpc", "orpc", "none"],
|
||||
})
|
||||
.completion()
|
||||
.recommendCommands()
|
||||
.version(getLatestCLIVersion())
|
||||
.alias("version", "v")
|
||||
.help()
|
||||
.alias("help", "h")
|
||||
.strict()
|
||||
.wrap(null)
|
||||
.parse();
|
||||
|
||||
const options = argv as YargsArgv;
|
||||
const options = await parseCliArguments();
|
||||
const cliProjectNameArg = options.projectDirectory;
|
||||
|
||||
renderTitle();
|
||||
@@ -312,42 +172,13 @@ async function main() {
|
||||
};
|
||||
|
||||
if (config.backend === "convex") {
|
||||
config.auth = false;
|
||||
config.database = "none";
|
||||
config.orm = "none";
|
||||
config.api = "none";
|
||||
config.runtime = "none";
|
||||
config.dbSetup = "none";
|
||||
config.examples = ["todo"];
|
||||
log.info(
|
||||
"Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo",
|
||||
);
|
||||
} else if (config.backend === "none") {
|
||||
config.auth = false;
|
||||
config.database = "none";
|
||||
config.orm = "none";
|
||||
config.api = "none";
|
||||
config.runtime = "none";
|
||||
config.dbSetup = "none";
|
||||
config.examples = [];
|
||||
log.info(
|
||||
"Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
|
||||
);
|
||||
} else if (config.database === "none") {
|
||||
config.orm = "none";
|
||||
log.info(
|
||||
"Due to '--database none', '--orm' has been automatically set to 'none'.",
|
||||
);
|
||||
|
||||
config.auth = false;
|
||||
log.info(
|
||||
"Due to '--database none', '--auth' has been automatically set to 'false'.",
|
||||
);
|
||||
|
||||
config.dbSetup = "none";
|
||||
log.info(
|
||||
"Due to '--database none', '--db-setup' has been automatically set to 'none'.",
|
||||
);
|
||||
}
|
||||
|
||||
log.info(
|
||||
@@ -401,518 +232,6 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
function processAndValidateFlags(
|
||||
options: YargsArgv,
|
||||
projectName?: string,
|
||||
): Partial<ProjectConfig> {
|
||||
const config: Partial<ProjectConfig> = {};
|
||||
const providedFlags: Set<string> = new Set(
|
||||
Object.keys(options).filter((key) => key !== "_" && key !== "$0"),
|
||||
);
|
||||
|
||||
if (options.api) {
|
||||
config.api = options.api as ProjectApi;
|
||||
if (options.api === "none") {
|
||||
if (
|
||||
options.examples &&
|
||||
!(options.examples.length === 1 && options.examples[0] === "none")
|
||||
) {
|
||||
consola.fatal(
|
||||
"Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.backend) {
|
||||
config.backend = options.backend as ProjectBackend;
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("backend") &&
|
||||
config.backend &&
|
||||
config.backend !== "convex" &&
|
||||
config.backend !== "none"
|
||||
) {
|
||||
if (providedFlags.has("runtime") && options.runtime === "none") {
|
||||
consola.fatal(
|
||||
`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.database) {
|
||||
config.database = options.database as ProjectDatabase;
|
||||
}
|
||||
if (options.orm) {
|
||||
config.orm = options.orm as ProjectOrm;
|
||||
}
|
||||
if (options.auth !== undefined) {
|
||||
config.auth = options.auth;
|
||||
}
|
||||
if (options.git !== undefined) {
|
||||
config.git = options.git;
|
||||
}
|
||||
if (options.install !== undefined) {
|
||||
config.install = options.install;
|
||||
}
|
||||
if (options.runtime) {
|
||||
config.runtime = options.runtime as ProjectRuntime;
|
||||
}
|
||||
if (options.dbSetup) {
|
||||
config.dbSetup = options.dbSetup as ProjectDBSetup;
|
||||
}
|
||||
if (options.packageManager) {
|
||||
config.packageManager = options.packageManager as ProjectPackageManager;
|
||||
}
|
||||
|
||||
if (projectName) {
|
||||
config.projectName = projectName;
|
||||
} else if (options.projectDirectory) {
|
||||
config.projectName = path.basename(
|
||||
path.resolve(process.cwd(), options.projectDirectory),
|
||||
);
|
||||
}
|
||||
|
||||
if (options.frontend && options.frontend.length > 0) {
|
||||
if (options.frontend.includes("none")) {
|
||||
if (options.frontend.length > 1) {
|
||||
consola.fatal(`Cannot combine 'none' with other frontend options.`);
|
||||
process.exit(1);
|
||||
}
|
||||
config.frontend = [];
|
||||
} else {
|
||||
const validOptions = options.frontend.filter(
|
||||
(f): f is ProjectFrontend => f !== "none",
|
||||
);
|
||||
const webFrontends = validOptions.filter(
|
||||
(f) =>
|
||||
f === "tanstack-router" ||
|
||||
f === "react-router" ||
|
||||
f === "tanstack-start" ||
|
||||
f === "next" ||
|
||||
f === "nuxt" ||
|
||||
f === "svelte" ||
|
||||
f === "solid",
|
||||
);
|
||||
const nativeFrontends = validOptions.filter(
|
||||
(f) => f === "native-nativewind" || f === "native-unistyles",
|
||||
);
|
||||
|
||||
if (webFrontends.length > 1) {
|
||||
consola.fatal(
|
||||
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (nativeFrontends.length > 1) {
|
||||
consola.fatal(
|
||||
"Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.frontend = validOptions;
|
||||
}
|
||||
}
|
||||
if (options.addons && options.addons.length > 0) {
|
||||
if (options.addons.includes("none")) {
|
||||
if (options.addons.length > 1) {
|
||||
consola.fatal(`Cannot combine 'none' with other addons.`);
|
||||
process.exit(1);
|
||||
}
|
||||
config.addons = [];
|
||||
} else {
|
||||
config.addons = options.addons.filter(
|
||||
(addon): addon is ProjectAddons => addon !== "none",
|
||||
);
|
||||
}
|
||||
}
|
||||
if (options.examples && options.examples.length > 0) {
|
||||
if (options.examples.includes("none")) {
|
||||
if (options.examples.length > 1) {
|
||||
consola.fatal("Cannot combine 'none' with other examples.");
|
||||
process.exit(1);
|
||||
}
|
||||
config.examples = [];
|
||||
} else {
|
||||
config.examples = options.examples.filter(
|
||||
(ex): ex is ProjectExamples => ex !== "none",
|
||||
);
|
||||
if (options.examples.includes("none") && config.backend !== "convex") {
|
||||
config.examples = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.backend === "convex") {
|
||||
const incompatibleFlags: string[] = [];
|
||||
|
||||
if (providedFlags.has("auth") && options.auth === true)
|
||||
incompatibleFlags.push("--auth");
|
||||
if (providedFlags.has("database") && options.database !== "none")
|
||||
incompatibleFlags.push(`--database ${options.database}`);
|
||||
if (providedFlags.has("orm") && options.orm !== "none")
|
||||
incompatibleFlags.push(`--orm ${options.orm}`);
|
||||
if (providedFlags.has("api") && options.api !== "none")
|
||||
incompatibleFlags.push(`--api ${options.api}`);
|
||||
if (providedFlags.has("runtime") && options.runtime !== "none")
|
||||
incompatibleFlags.push(`--runtime ${options.runtime}`);
|
||||
if (providedFlags.has("dbSetup") && options.dbSetup !== "none")
|
||||
incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
|
||||
|
||||
if (incompatibleFlags.length > 0) {
|
||||
consola.fatal(
|
||||
`The following flags are incompatible with '--backend convex': ${incompatibleFlags.join(
|
||||
", ",
|
||||
)}. Please remove them.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (providedFlags.has("frontend") && options.frontend) {
|
||||
const incompatibleFrontends = options.frontend.filter(
|
||||
(f) => f === "nuxt" || f === "solid",
|
||||
);
|
||||
if (incompatibleFrontends.length > 0) {
|
||||
consola.fatal(
|
||||
`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(
|
||||
", ",
|
||||
)}. Please choose a different frontend or backend.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
config.auth = false;
|
||||
config.database = "none";
|
||||
config.orm = "none";
|
||||
config.api = "none";
|
||||
config.runtime = "none";
|
||||
config.dbSetup = "none";
|
||||
config.examples = ["todo"];
|
||||
} else if (config.backend === "none") {
|
||||
const incompatibleFlags: string[] = [];
|
||||
|
||||
if (providedFlags.has("auth") && options.auth === true)
|
||||
incompatibleFlags.push("--auth");
|
||||
if (providedFlags.has("database") && options.database !== "none")
|
||||
incompatibleFlags.push(`--database ${options.database}`);
|
||||
if (providedFlags.has("orm") && options.orm !== "none")
|
||||
incompatibleFlags.push(`--orm ${options.orm}`);
|
||||
if (providedFlags.has("api") && options.api !== "none")
|
||||
incompatibleFlags.push(`--api ${options.api}`);
|
||||
if (providedFlags.has("runtime") && options.runtime !== "none")
|
||||
incompatibleFlags.push(`--runtime ${options.runtime}`);
|
||||
if (providedFlags.has("dbSetup") && options.dbSetup !== "none")
|
||||
incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
|
||||
|
||||
if (incompatibleFlags.length > 0) {
|
||||
consola.fatal(
|
||||
`The following flags are incompatible with '--backend none': ${incompatibleFlags.join(
|
||||
", ",
|
||||
)}. Please remove them.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
config.auth = false;
|
||||
config.database = "none";
|
||||
config.orm = "none";
|
||||
config.api = "none";
|
||||
config.runtime = "none";
|
||||
config.dbSetup = "none";
|
||||
if (
|
||||
options.examples &&
|
||||
!options.examples.includes("none") &&
|
||||
options.examples.length > 0
|
||||
) {
|
||||
consola.fatal(
|
||||
"Cannot select examples when backend is 'none'. Please remove the --examples flag or set --examples none.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.examples = [];
|
||||
} else {
|
||||
const effectiveDatabase =
|
||||
config.database ?? (options.yes ? DEFAULT_CONFIG.database : undefined);
|
||||
const effectiveOrm =
|
||||
config.orm ?? (options.yes ? DEFAULT_CONFIG.orm : undefined);
|
||||
const _effectiveAuth =
|
||||
config.auth ?? (options.yes ? DEFAULT_CONFIG.auth : undefined);
|
||||
const _effectiveDbSetup =
|
||||
config.dbSetup ?? (options.yes ? DEFAULT_CONFIG.dbSetup : undefined);
|
||||
const _effectiveExamples =
|
||||
config.examples ?? (options.yes ? DEFAULT_CONFIG.examples : undefined);
|
||||
const effectiveFrontend =
|
||||
config.frontend ?? (options.yes ? DEFAULT_CONFIG.frontend : undefined);
|
||||
const effectiveApi =
|
||||
config.api ?? (options.yes ? DEFAULT_CONFIG.api : undefined);
|
||||
const effectiveBackend =
|
||||
config.backend ?? (options.yes ? DEFAULT_CONFIG.backend : undefined);
|
||||
|
||||
if (effectiveDatabase === "none") {
|
||||
if (providedFlags.has("orm") && options.orm !== "none") {
|
||||
consola.fatal(
|
||||
`Cannot use ORM '--orm ${options.orm}' when database is 'none'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.orm = "none";
|
||||
log.info(
|
||||
"Due to '--database none', '--orm' has been automatically set to 'none'.",
|
||||
);
|
||||
|
||||
if (providedFlags.has("auth") && options.auth === true) {
|
||||
consola.fatal(
|
||||
"Authentication requires a database. Cannot use --auth when database is 'none'.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.auth = false;
|
||||
log.info(
|
||||
"Due to '--database none', '--auth' has been automatically set to 'false'.",
|
||||
);
|
||||
|
||||
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") {
|
||||
consola.fatal(
|
||||
`Database setup '--db-setup ${options.dbSetup}' requires a database. Cannot use when database is 'none'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.dbSetup = "none";
|
||||
log.info(
|
||||
"Due to '--database none', '--db-setup' has been automatically set to 'none'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (config.orm === "mongoose" && !providedFlags.has("database")) {
|
||||
if (effectiveDatabase && effectiveDatabase !== "mongodb") {
|
||||
consola.fatal(
|
||||
`Mongoose ORM requires MongoDB. Cannot use --orm mongoose with --database ${effectiveDatabase}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.database = "mongodb";
|
||||
}
|
||||
|
||||
if (effectiveDatabase === "mongodb" && effectiveOrm === "drizzle") {
|
||||
consola.fatal(
|
||||
"Drizzle ORM is not compatible with MongoDB. Please use --orm prisma or --orm mongoose.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
effectiveOrm === "mongoose" &&
|
||||
effectiveDatabase &&
|
||||
effectiveDatabase !== "mongodb"
|
||||
) {
|
||||
consola.fatal(
|
||||
`Mongoose ORM requires MongoDB. Cannot use --orm mongoose with --database ${effectiveDatabase}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.dbSetup && config.dbSetup !== "none") {
|
||||
const dbSetup = config.dbSetup;
|
||||
|
||||
if (!effectiveDatabase || effectiveDatabase === "none") {
|
||||
consola.fatal(
|
||||
`Database setup '--db-setup ${dbSetup}' requires a database. Cannot use when database is 'none'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (dbSetup === "turso") {
|
||||
if (effectiveDatabase && effectiveDatabase !== "sqlite") {
|
||||
consola.fatal(
|
||||
`Turso setup requires SQLite. Cannot use --db-setup turso with --database ${effectiveDatabase}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (effectiveOrm !== "drizzle") {
|
||||
consola.fatal(
|
||||
`Turso setup requires Drizzle ORM. Cannot use --db-setup turso with --orm ${
|
||||
effectiveOrm ?? "none"
|
||||
}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (dbSetup === "supabase") {
|
||||
if (effectiveDatabase !== "postgres") {
|
||||
consola.fatal(
|
||||
`Supabase setup requires PostgreSQL. Cannot use --db-setup supabase with --database ${effectiveDatabase}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (dbSetup === "prisma-postgres") {
|
||||
if (effectiveDatabase !== "postgres") {
|
||||
consola.fatal(
|
||||
`Prisma PostgreSQL setup requires PostgreSQL. Cannot use --db-setup prisma-postgres with --database ${effectiveDatabase}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (effectiveOrm !== "prisma") {
|
||||
consola.fatal(
|
||||
`Prisma PostgreSQL setup requires Prisma ORM. Cannot use --db-setup prisma-postgres with --orm ${effectiveOrm}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (dbSetup === "mongodb-atlas") {
|
||||
if (effectiveDatabase !== "mongodb") {
|
||||
consola.fatal(
|
||||
`MongoDB Atlas setup requires MongoDB. Cannot use --db-setup mongodb-atlas with --database ${effectiveDatabase}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (effectiveOrm !== "prisma" && effectiveOrm !== "mongoose") {
|
||||
consola.fatal(
|
||||
`MongoDB Atlas setup requires Prisma or Mongoose ORM. Cannot use --db-setup mongodb-atlas with --orm ${effectiveOrm}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (dbSetup === "neon") {
|
||||
if (effectiveDatabase !== "postgres") {
|
||||
consola.fatal(
|
||||
`Neon PostgreSQL setup requires PostgreSQL. Cannot use --db-setup neon with --database ${effectiveDatabase}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const includesNuxt = effectiveFrontend?.includes("nuxt");
|
||||
const includesSvelte = effectiveFrontend?.includes("svelte");
|
||||
const includesSolid = effectiveFrontend?.includes("solid");
|
||||
|
||||
if (
|
||||
(includesNuxt || includesSvelte || includesSolid) &&
|
||||
effectiveApi === "trpc"
|
||||
) {
|
||||
consola.fatal(
|
||||
`tRPC API is not supported with '${
|
||||
includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"
|
||||
}' frontend. Please use --api orpc or --api none or remove '${
|
||||
includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"
|
||||
}' from --frontend.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.addons && config.addons.length > 0) {
|
||||
const webSpecificAddons = ["pwa", "tauri"];
|
||||
const hasWebSpecificAddons = config.addons.some((addon) =>
|
||||
webSpecificAddons.includes(addon),
|
||||
);
|
||||
const hasCompatibleWebFrontend = effectiveFrontend?.some((f) => {
|
||||
const isPwaCompatible =
|
||||
f === "tanstack-router" ||
|
||||
f === "react-router" ||
|
||||
f === "solid" ||
|
||||
f === "next";
|
||||
const isTauriCompatible =
|
||||
f === "tanstack-router" ||
|
||||
f === "react-router" ||
|
||||
f === "nuxt" ||
|
||||
f === "svelte" ||
|
||||
f === "solid" ||
|
||||
f === "next";
|
||||
|
||||
if (
|
||||
config.addons?.includes("pwa") &&
|
||||
config.addons?.includes("tauri")
|
||||
) {
|
||||
return isPwaCompatible && isTauriCompatible;
|
||||
}
|
||||
if (config.addons?.includes("pwa")) {
|
||||
return isPwaCompatible;
|
||||
}
|
||||
if (config.addons?.includes("tauri")) {
|
||||
return isTauriCompatible;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (hasWebSpecificAddons && !hasCompatibleWebFrontend) {
|
||||
let incompatibleReason = "Selected frontend is not compatible.";
|
||||
if (config.addons.includes("pwa")) {
|
||||
incompatibleReason =
|
||||
"PWA requires tanstack-router, react-router, next, or solid.";
|
||||
}
|
||||
if (config.addons.includes("tauri")) {
|
||||
incompatibleReason =
|
||||
"Tauri requires tanstack-router, react-router, nuxt, svelte, solid, or next.";
|
||||
}
|
||||
consola.fatal(
|
||||
`Incompatible addon/frontend combination: ${incompatibleReason}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.addons.includes("husky") && !config.addons.includes("biome")) {
|
||||
consola.warn(
|
||||
"Husky addon is recommended to be used with Biome for lint-staged configuration.",
|
||||
);
|
||||
}
|
||||
config.addons = [...new Set(config.addons)];
|
||||
}
|
||||
|
||||
const onlyNativeFrontend =
|
||||
effectiveFrontend &&
|
||||
effectiveFrontend.length === 1 &&
|
||||
(effectiveFrontend[0] === "native-nativewind" ||
|
||||
effectiveFrontend[0] === "native-unistyles");
|
||||
|
||||
if (
|
||||
onlyNativeFrontend &&
|
||||
config.examples &&
|
||||
config.examples.length > 0 &&
|
||||
!config.examples.includes("none")
|
||||
) {
|
||||
consola.fatal(
|
||||
"Examples are not supported when only a native frontend (NativeWind or Unistyles) is selected.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
config.examples &&
|
||||
config.examples.length > 0 &&
|
||||
!config.examples.includes("none")
|
||||
) {
|
||||
if (
|
||||
config.examples.includes("todo") &&
|
||||
effectiveBackend !== "convex" &&
|
||||
effectiveBackend !== "none" &&
|
||||
effectiveDatabase === "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
"The 'todo' example requires a database if a backend (other than Convex) is present. Cannot use --examples todo when database is 'none' and a backend is selected.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.examples.includes("ai") && effectiveBackend === "elysia") {
|
||||
consola.fatal(
|
||||
"The 'ai' example is not compatible with the Elysia backend.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.examples.includes("ai") && includesSolid) {
|
||||
consola.fatal(
|
||||
"The 'ai' example is not compatible with the Solid frontend.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
consola.error("Aborting installation due to unexpected error...");
|
||||
if (err instanceof Error) {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { cancel, isCancel, multiselect } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { ProjectAddons, ProjectFrontend } from "../types";
|
||||
import type { Addons, Frontend } from "../types";
|
||||
|
||||
type AddonOption = {
|
||||
value: ProjectAddons;
|
||||
value: Addons;
|
||||
label: string;
|
||||
hint: string;
|
||||
};
|
||||
|
||||
export async function getAddonsChoice(
|
||||
addons?: ProjectAddons[],
|
||||
frontends?: ProjectFrontend[],
|
||||
): Promise<ProjectAddons[]> {
|
||||
addons?: Addons[],
|
||||
frontends?: Frontend[],
|
||||
): Promise<Addons[]> {
|
||||
if (addons !== undefined) return addons;
|
||||
|
||||
const hasCompatiblePwaFrontend =
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectApi, ProjectBackend, ProjectFrontend } from "../types";
|
||||
import type { API, Backend, Frontend } from "../types";
|
||||
|
||||
export async function getApiChoice(
|
||||
Api?: ProjectApi | undefined,
|
||||
frontend?: ProjectFrontend[],
|
||||
backend?: ProjectBackend,
|
||||
): Promise<ProjectApi> {
|
||||
Api?: API | undefined,
|
||||
frontend?: Frontend[],
|
||||
backend?: Backend,
|
||||
): Promise<API> {
|
||||
if (backend === "convex" || backend === "none") {
|
||||
return "none";
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export async function getApiChoice(
|
||||
];
|
||||
}
|
||||
|
||||
const apiType = await select<ProjectApi>({
|
||||
const apiType = await select<API>({
|
||||
message: "Select API type",
|
||||
options: apiOptions,
|
||||
initialValue: apiOptions[0].value,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { cancel, confirm, isCancel } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { ProjectBackend } from "../types";
|
||||
import type { Backend } from "../types";
|
||||
|
||||
export async function getAuthChoice(
|
||||
auth: boolean | undefined,
|
||||
hasDatabase: boolean,
|
||||
backend?: ProjectBackend,
|
||||
backend?: Backend,
|
||||
): Promise<boolean> {
|
||||
if (backend === "convex") {
|
||||
return false;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { ProjectBackend, ProjectFrontend } from "../types";
|
||||
import type { Backend, Frontend } from "../types";
|
||||
|
||||
export async function getBackendFrameworkChoice(
|
||||
backendFramework?: ProjectBackend,
|
||||
frontends?: ProjectFrontend[],
|
||||
): Promise<ProjectBackend> {
|
||||
backendFramework?: Backend,
|
||||
frontends?: Frontend[],
|
||||
): Promise<Backend> {
|
||||
if (backendFramework !== undefined) return backendFramework;
|
||||
|
||||
const hasIncompatibleFrontend = frontends?.some(
|
||||
@@ -14,7 +14,7 @@ export async function getBackendFrameworkChoice(
|
||||
);
|
||||
|
||||
const backendOptions: Array<{
|
||||
value: ProjectBackend;
|
||||
value: Backend;
|
||||
label: string;
|
||||
hint: string;
|
||||
}> = [
|
||||
@@ -26,7 +26,7 @@ export async function getBackendFrameworkChoice(
|
||||
{
|
||||
value: "next" as const,
|
||||
label: "Next.js",
|
||||
hint: "Full-stack framework with API routes",
|
||||
hint: "Nextjs API routes",
|
||||
},
|
||||
{
|
||||
value: "express" as const,
|
||||
@@ -56,7 +56,7 @@ export async function getBackendFrameworkChoice(
|
||||
backendOptions.push({
|
||||
value: "none" as const,
|
||||
label: "None",
|
||||
hint: "No backend server (e.g., for a static site or client-only app)",
|
||||
hint: "No backend server",
|
||||
});
|
||||
|
||||
let initialValue = DEFAULT_CONFIG.backend;
|
||||
@@ -64,8 +64,8 @@ export async function getBackendFrameworkChoice(
|
||||
initialValue = "hono";
|
||||
}
|
||||
|
||||
const response = await select<ProjectBackend>({
|
||||
message: "Select backend framework",
|
||||
const response = await select<Backend>({
|
||||
message: "Select backend",
|
||||
options: backendOptions,
|
||||
initialValue,
|
||||
});
|
||||
@@ -1,26 +1,26 @@
|
||||
import { cancel, group } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type {
|
||||
ProjectAddons,
|
||||
ProjectApi,
|
||||
ProjectBackend,
|
||||
API,
|
||||
Addons,
|
||||
Backend,
|
||||
Database,
|
||||
DatabaseSetup,
|
||||
Examples,
|
||||
Frontend,
|
||||
ORM,
|
||||
PackageManager,
|
||||
ProjectConfig,
|
||||
ProjectDBSetup,
|
||||
ProjectDatabase,
|
||||
ProjectExamples,
|
||||
ProjectFrontend,
|
||||
ProjectOrm,
|
||||
ProjectPackageManager,
|
||||
ProjectRuntime,
|
||||
Runtime,
|
||||
} from "../types";
|
||||
import { getAddonsChoice } from "./addons";
|
||||
import { getApiChoice } from "./api";
|
||||
import { getAuthChoice } from "./auth";
|
||||
import { getBackendFrameworkChoice } from "./backend-framework";
|
||||
import { getBackendFrameworkChoice } from "./backend";
|
||||
import { getDatabaseChoice } from "./database";
|
||||
import { getDBSetupChoice } from "./db-setup";
|
||||
import { getDBSetupChoice } from "./database-setup";
|
||||
import { getExamplesChoice } from "./examples";
|
||||
import { getFrontendChoice } from "./frontend-option";
|
||||
import { getFrontendChoice } from "./frontend";
|
||||
import { getGitChoice } from "./git";
|
||||
import { getinstallChoice } from "./install";
|
||||
import { getORMChoice } from "./orm";
|
||||
@@ -28,18 +28,18 @@ import { getPackageManagerChoice } from "./package-manager";
|
||||
import { getRuntimeChoice } from "./runtime";
|
||||
|
||||
type PromptGroupResults = {
|
||||
frontend: ProjectFrontend[];
|
||||
backend: ProjectBackend;
|
||||
runtime: ProjectRuntime;
|
||||
database: ProjectDatabase;
|
||||
orm: ProjectOrm;
|
||||
api: ProjectApi;
|
||||
frontend: Frontend[];
|
||||
backend: Backend;
|
||||
runtime: Runtime;
|
||||
database: Database;
|
||||
orm: ORM;
|
||||
api: API;
|
||||
auth: boolean;
|
||||
addons: ProjectAddons[];
|
||||
examples: ProjectExamples[];
|
||||
dbSetup: ProjectDBSetup;
|
||||
addons: Addons[];
|
||||
examples: Examples[];
|
||||
dbSetup: DatabaseSetup;
|
||||
git: boolean;
|
||||
packageManager: ProjectPackageManager;
|
||||
packageManager: PackageManager;
|
||||
install: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectBackend, ProjectDBSetup, ProjectOrm } from "../types";
|
||||
import type { Backend, DatabaseSetup, ORM } from "../types";
|
||||
|
||||
export async function getDBSetupChoice(
|
||||
databaseType: string,
|
||||
dbSetup: ProjectDBSetup | undefined,
|
||||
orm?: ProjectOrm,
|
||||
backend?: ProjectBackend,
|
||||
): Promise<ProjectDBSetup> {
|
||||
dbSetup: DatabaseSetup | undefined,
|
||||
orm?: ORM,
|
||||
backend?: Backend,
|
||||
): Promise<DatabaseSetup> {
|
||||
if (backend === "convex") {
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (dbSetup !== undefined) return dbSetup as ProjectDBSetup;
|
||||
if (dbSetup !== undefined) return dbSetup as DatabaseSetup;
|
||||
|
||||
if (databaseType === "none") {
|
||||
return "none";
|
||||
@@ -22,7 +22,7 @@ export async function getDBSetupChoice(
|
||||
return "none";
|
||||
}
|
||||
|
||||
let options: Array<{ value: ProjectDBSetup; label: string; hint: string }> =
|
||||
let options: Array<{ value: DatabaseSetup; label: string; hint: string }> =
|
||||
[];
|
||||
|
||||
if (databaseType === "sqlite") {
|
||||
@@ -70,7 +70,7 @@ export async function getDBSetupChoice(
|
||||
return "none";
|
||||
}
|
||||
|
||||
const response = await select<ProjectDBSetup>({
|
||||
const response = await select<DatabaseSetup>({
|
||||
message: `Select ${databaseType} setup option`,
|
||||
options,
|
||||
initialValue: "none",
|
||||
@@ -1,19 +1,19 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { ProjectBackend, ProjectDatabase } from "../types";
|
||||
import type { Backend, Database } from "../types";
|
||||
|
||||
export async function getDatabaseChoice(
|
||||
database?: ProjectDatabase,
|
||||
backend?: ProjectBackend,
|
||||
): Promise<ProjectDatabase> {
|
||||
database?: Database,
|
||||
backend?: Backend,
|
||||
): Promise<Database> {
|
||||
if (backend === "convex" || backend === "none") {
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (database !== undefined) return database;
|
||||
|
||||
const response = await select<ProjectDatabase>({
|
||||
const response = await select<Database>({
|
||||
message: "Select database",
|
||||
options: [
|
||||
{
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { cancel, isCancel, multiselect } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type {
|
||||
ProjectApi,
|
||||
ProjectBackend,
|
||||
ProjectDatabase,
|
||||
ProjectExamples,
|
||||
ProjectFrontend,
|
||||
} from "../types";
|
||||
import type { API, Backend, Database, Examples, Frontend } from "../types";
|
||||
|
||||
export async function getExamplesChoice(
|
||||
examples?: ProjectExamples[],
|
||||
database?: ProjectDatabase,
|
||||
frontends?: ProjectFrontend[],
|
||||
backend?: ProjectBackend,
|
||||
api?: ProjectApi,
|
||||
): Promise<ProjectExamples[]> {
|
||||
examples?: Examples[],
|
||||
database?: Database,
|
||||
frontends?: Frontend[],
|
||||
backend?: Backend,
|
||||
api?: API,
|
||||
): Promise<Examples[]> {
|
||||
if (api === "none") {
|
||||
return [];
|
||||
}
|
||||
@@ -56,8 +50,8 @@ export async function getExamplesChoice(
|
||||
|
||||
if (!hasWebFrontend && !noFrontendSelected) return [];
|
||||
|
||||
let response: ProjectExamples[] | symbol = [];
|
||||
const options: { value: ProjectExamples; label: string; hint: string }[] = [
|
||||
let response: Examples[] | symbol = [];
|
||||
const options: { value: Examples; label: string; hint: string }[] = [
|
||||
{
|
||||
value: "todo" as const,
|
||||
label: "Todo App",
|
||||
@@ -73,7 +67,7 @@ export async function getExamplesChoice(
|
||||
});
|
||||
}
|
||||
|
||||
response = await multiselect<ProjectExamples>({
|
||||
response = await multiselect<Examples>({
|
||||
message: "Include examples",
|
||||
options: options,
|
||||
required: false,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { cancel, isCancel, multiselect, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { ProjectBackend, ProjectFrontend } from "../types";
|
||||
import type { Backend, Frontend } from "../types";
|
||||
|
||||
export async function getFrontendChoice(
|
||||
frontendOptions?: ProjectFrontend[],
|
||||
backend?: ProjectBackend,
|
||||
): Promise<ProjectFrontend[]> {
|
||||
frontendOptions?: Frontend[],
|
||||
backend?: Backend,
|
||||
): Promise<Frontend[]> {
|
||||
if (frontendOptions !== undefined) return frontendOptions;
|
||||
|
||||
const frontendTypes = await multiselect({
|
||||
@@ -32,7 +32,7 @@ export async function getFrontendChoice(
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const result: ProjectFrontend[] = [];
|
||||
const result: Frontend[] = [];
|
||||
|
||||
if (frontendTypes.includes("web")) {
|
||||
const allWebOptions = [
|
||||
@@ -80,8 +80,8 @@ export async function getFrontendChoice(
|
||||
return true;
|
||||
});
|
||||
|
||||
const webFramework = await select<ProjectFrontend>({
|
||||
message: "Choose frontend framework",
|
||||
const webFramework = await select<Frontend>({
|
||||
message: "Choose frontend",
|
||||
options: webOptions,
|
||||
initialValue: DEFAULT_CONFIG.frontend[0],
|
||||
});
|
||||
@@ -95,7 +95,7 @@ export async function getFrontendChoice(
|
||||
}
|
||||
|
||||
if (frontendTypes.includes("native")) {
|
||||
const nativeFramework = await select<ProjectFrontend>({
|
||||
const nativeFramework = await select<Frontend>({
|
||||
message: "Choose native framework",
|
||||
options: [
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { ProjectBackend, ProjectDatabase, ProjectOrm } from "../types";
|
||||
import type { Backend, Database, ORM } from "../types";
|
||||
|
||||
const ormOptions = {
|
||||
prisma: {
|
||||
@@ -22,11 +22,11 @@ const ormOptions = {
|
||||
};
|
||||
|
||||
export async function getORMChoice(
|
||||
orm: ProjectOrm | undefined,
|
||||
orm: ORM | undefined,
|
||||
hasDatabase: boolean,
|
||||
database?: ProjectDatabase,
|
||||
backend?: ProjectBackend,
|
||||
): Promise<ProjectOrm> {
|
||||
database?: Database,
|
||||
backend?: Backend,
|
||||
): Promise<ORM> {
|
||||
if (backend === "convex") {
|
||||
return "none";
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function getORMChoice(
|
||||
: [ormOptions.drizzle, ormOptions.prisma]),
|
||||
];
|
||||
|
||||
const response = await select<ProjectOrm>({
|
||||
const response = await select<ORM>({
|
||||
message: "Select ORM",
|
||||
options,
|
||||
initialValue: database === "mongodb" ? "prisma" : DEFAULT_CONFIG.orm,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectPackageManager } from "../types";
|
||||
import type { PackageManager } from "../types";
|
||||
import { getUserPkgManager } from "../utils/get-package-manager";
|
||||
|
||||
export async function getPackageManagerChoice(
|
||||
packageManager?: ProjectPackageManager,
|
||||
): Promise<ProjectPackageManager> {
|
||||
packageManager?: PackageManager,
|
||||
): Promise<PackageManager> {
|
||||
if (packageManager !== undefined) return packageManager;
|
||||
|
||||
const detectedPackageManager = getUserPkgManager();
|
||||
|
||||
const response = await select<ProjectPackageManager>({
|
||||
const response = await select<PackageManager>({
|
||||
message: "Choose package manager",
|
||||
options: [
|
||||
{ value: "npm", label: "npm", hint: "Node Package Manager" },
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { ProjectBackend, ProjectRuntime } from "../types";
|
||||
import type { Backend, Runtime } from "../types";
|
||||
|
||||
export async function getRuntimeChoice(
|
||||
runtime?: ProjectRuntime,
|
||||
backend?: ProjectBackend,
|
||||
): Promise<ProjectRuntime> {
|
||||
runtime?: Runtime,
|
||||
backend?: Backend,
|
||||
): Promise<Runtime> {
|
||||
if (backend === "convex" || backend === "none") {
|
||||
return "none";
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export async function getRuntimeChoice(
|
||||
return "node";
|
||||
}
|
||||
|
||||
const response = await select<ProjectRuntime>({
|
||||
const response = await select<Runtime>({
|
||||
message: "Select runtime",
|
||||
options: [
|
||||
{
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
export type ProjectDatabase =
|
||||
| "sqlite"
|
||||
| "postgres"
|
||||
| "mongodb"
|
||||
| "mysql"
|
||||
| "none";
|
||||
export type ProjectOrm = "drizzle" | "prisma" | "mongoose" | "none";
|
||||
export type ProjectPackageManager = "npm" | "pnpm" | "bun";
|
||||
export type ProjectAddons =
|
||||
export type Database = "sqlite" | "postgres" | "mongodb" | "mysql" | "none";
|
||||
export type ORM = "drizzle" | "prisma" | "mongoose" | "none";
|
||||
export type PackageManager = "npm" | "pnpm" | "bun";
|
||||
export type Addons =
|
||||
| "pwa"
|
||||
| "biome"
|
||||
| "tauri"
|
||||
@@ -14,7 +9,7 @@ export type ProjectAddons =
|
||||
| "starlight"
|
||||
| "turborepo"
|
||||
| "none";
|
||||
export type ProjectBackend =
|
||||
export type Backend =
|
||||
| "hono"
|
||||
| "express"
|
||||
| "fastify"
|
||||
@@ -22,9 +17,9 @@ export type ProjectBackend =
|
||||
| "elysia"
|
||||
| "convex"
|
||||
| "none";
|
||||
export type ProjectRuntime = "node" | "bun" | "none";
|
||||
export type ProjectExamples = "todo" | "ai" | "none";
|
||||
export type ProjectFrontend =
|
||||
export type Runtime = "node" | "bun" | "none";
|
||||
export type Examples = "todo" | "ai" | "none";
|
||||
export type Frontend =
|
||||
| "react-router"
|
||||
| "tanstack-router"
|
||||
| "tanstack-start"
|
||||
@@ -35,51 +30,51 @@ export type ProjectFrontend =
|
||||
| "svelte"
|
||||
| "solid"
|
||||
| "none";
|
||||
export type ProjectDBSetup =
|
||||
export type DatabaseSetup =
|
||||
| "turso"
|
||||
| "prisma-postgres"
|
||||
| "mongodb-atlas"
|
||||
| "neon"
|
||||
| "supabase"
|
||||
| "none";
|
||||
export type ProjectApi = "trpc" | "orpc" | "none";
|
||||
export type API = "trpc" | "orpc" | "none";
|
||||
|
||||
export interface ProjectConfig {
|
||||
projectName: string;
|
||||
projectDir: string;
|
||||
relativePath: string;
|
||||
backend: ProjectBackend;
|
||||
runtime: ProjectRuntime;
|
||||
database: ProjectDatabase;
|
||||
orm: ProjectOrm;
|
||||
backend: Backend;
|
||||
runtime: Runtime;
|
||||
database: Database;
|
||||
orm: ORM;
|
||||
auth: boolean;
|
||||
addons: ProjectAddons[];
|
||||
examples: ProjectExamples[];
|
||||
addons: Addons[];
|
||||
examples: Examples[];
|
||||
git: boolean;
|
||||
packageManager: ProjectPackageManager;
|
||||
packageManager: PackageManager;
|
||||
install: boolean;
|
||||
dbSetup: ProjectDBSetup;
|
||||
frontend: ProjectFrontend[];
|
||||
api: ProjectApi;
|
||||
dbSetup: DatabaseSetup;
|
||||
frontend: Frontend[];
|
||||
api: API;
|
||||
}
|
||||
|
||||
export type YargsArgv = {
|
||||
projectDirectory?: string;
|
||||
|
||||
yes?: boolean;
|
||||
database?: ProjectDatabase;
|
||||
orm?: ProjectOrm;
|
||||
database?: Database;
|
||||
orm?: ORM;
|
||||
auth?: boolean;
|
||||
frontend?: ProjectFrontend[];
|
||||
addons?: ProjectAddons[];
|
||||
examples?: ProjectExamples[];
|
||||
frontend?: Frontend[];
|
||||
addons?: Addons[];
|
||||
examples?: Examples[];
|
||||
git?: boolean;
|
||||
packageManager?: ProjectPackageManager;
|
||||
packageManager?: PackageManager;
|
||||
install?: boolean;
|
||||
dbSetup?: ProjectDBSetup;
|
||||
backend?: ProjectBackend;
|
||||
runtime?: ProjectRuntime;
|
||||
api?: ProjectApi;
|
||||
dbSetup?: DatabaseSetup;
|
||||
backend?: Backend;
|
||||
runtime?: Runtime;
|
||||
api?: API;
|
||||
|
||||
_: (string | number)[];
|
||||
$0: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ProjectPackageManager } from "../types";
|
||||
import type { PackageManager } from "../types";
|
||||
|
||||
/**
|
||||
* Returns the appropriate command for running a package without installing it globally,
|
||||
@@ -9,7 +9,7 @@ import type { ProjectPackageManager } from "../types";
|
||||
* @returns The full command string (e.g., "npx prisma generate --schema=./prisma/schema.prisma").
|
||||
*/
|
||||
export function getPackageExecutionCommand(
|
||||
packageManager: ProjectPackageManager | null | undefined,
|
||||
packageManager: PackageManager | null | undefined,
|
||||
commandWithArgs: string,
|
||||
): string {
|
||||
switch (packageManager) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ProjectPackageManager } from "../types";
|
||||
import type { PackageManager } from "../types";
|
||||
|
||||
export const getUserPkgManager: () => ProjectPackageManager = () => {
|
||||
export const getUserPkgManager: () => PackageManager = () => {
|
||||
const userAgent = process.env.npm_config_user_agent;
|
||||
|
||||
if (userAgent?.startsWith("pnpm")) {
|
||||
|
||||
519
apps/cli/src/validation.ts
Normal file
519
apps/cli/src/validation.ts
Normal file
@@ -0,0 +1,519 @@
|
||||
import path from "node:path";
|
||||
import { log } from "@clack/prompts";
|
||||
import { consola } from "consola";
|
||||
import type {
|
||||
API,
|
||||
Addons,
|
||||
Backend,
|
||||
Database,
|
||||
DatabaseSetup,
|
||||
Examples,
|
||||
Frontend,
|
||||
ORM,
|
||||
PackageManager,
|
||||
ProjectConfig,
|
||||
Runtime,
|
||||
YargsArgv,
|
||||
} from "./types";
|
||||
|
||||
export function processAndValidateFlags(
|
||||
options: YargsArgv,
|
||||
projectName?: string,
|
||||
): Partial<ProjectConfig> {
|
||||
const config: Partial<ProjectConfig> = {};
|
||||
const providedFlags: Set<string> = new Set(
|
||||
Object.keys(options).filter((key) => key !== "_" && key !== "$0"),
|
||||
);
|
||||
|
||||
if (options.api) {
|
||||
config.api = options.api as API;
|
||||
if (options.api === "none") {
|
||||
if (
|
||||
options.examples &&
|
||||
!(options.examples.length === 1 && options.examples[0] === "none")
|
||||
) {
|
||||
consola.fatal(
|
||||
"Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.backend) {
|
||||
config.backend = options.backend as Backend;
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("backend") &&
|
||||
config.backend &&
|
||||
config.backend !== "convex" &&
|
||||
config.backend !== "none"
|
||||
) {
|
||||
if (providedFlags.has("runtime") && options.runtime === "none") {
|
||||
consola.fatal(
|
||||
`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.database) {
|
||||
config.database = options.database as Database;
|
||||
}
|
||||
if (options.orm) {
|
||||
config.orm = options.orm as ORM;
|
||||
}
|
||||
if (options.auth !== undefined) {
|
||||
config.auth = options.auth;
|
||||
}
|
||||
if (options.git !== undefined) {
|
||||
config.git = options.git;
|
||||
}
|
||||
if (options.install !== undefined) {
|
||||
config.install = options.install;
|
||||
}
|
||||
if (options.runtime) {
|
||||
config.runtime = options.runtime as Runtime;
|
||||
}
|
||||
if (options.dbSetup) {
|
||||
config.dbSetup = options.dbSetup as DatabaseSetup;
|
||||
}
|
||||
if (options.packageManager) {
|
||||
config.packageManager = options.packageManager as PackageManager;
|
||||
}
|
||||
|
||||
if (projectName) {
|
||||
config.projectName = projectName;
|
||||
} else if (options.projectDirectory) {
|
||||
config.projectName = path.basename(
|
||||
path.resolve(process.cwd(), options.projectDirectory),
|
||||
);
|
||||
}
|
||||
|
||||
if (options.frontend && options.frontend.length > 0) {
|
||||
if (options.frontend.includes("none")) {
|
||||
if (options.frontend.length > 1) {
|
||||
consola.fatal(`Cannot combine 'none' with other frontend options.`);
|
||||
process.exit(1);
|
||||
}
|
||||
config.frontend = [];
|
||||
} else {
|
||||
const validOptions = options.frontend.filter(
|
||||
(f): f is Frontend => f !== "none",
|
||||
);
|
||||
const webFrontends = validOptions.filter(
|
||||
(f) =>
|
||||
f === "tanstack-router" ||
|
||||
f === "react-router" ||
|
||||
f === "tanstack-start" ||
|
||||
f === "next" ||
|
||||
f === "nuxt" ||
|
||||
f === "svelte" ||
|
||||
f === "solid",
|
||||
);
|
||||
const nativeFrontends = validOptions.filter(
|
||||
(f) => f === "native-nativewind" || f === "native-unistyles",
|
||||
);
|
||||
|
||||
if (webFrontends.length > 1) {
|
||||
consola.fatal(
|
||||
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (nativeFrontends.length > 1) {
|
||||
consola.fatal(
|
||||
"Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
config.frontend = validOptions;
|
||||
}
|
||||
}
|
||||
if (options.addons && options.addons.length > 0) {
|
||||
if (options.addons.includes("none")) {
|
||||
if (options.addons.length > 1) {
|
||||
consola.fatal(`Cannot combine 'none' with other addons.`);
|
||||
process.exit(1);
|
||||
}
|
||||
config.addons = [];
|
||||
} else {
|
||||
config.addons = options.addons.filter(
|
||||
(addon): addon is Addons => addon !== "none",
|
||||
);
|
||||
}
|
||||
}
|
||||
if (options.examples && options.examples.length > 0) {
|
||||
if (options.examples.includes("none")) {
|
||||
if (options.examples.length > 1) {
|
||||
consola.fatal("Cannot combine 'none' with other examples.");
|
||||
process.exit(1);
|
||||
}
|
||||
config.examples = [];
|
||||
} else {
|
||||
config.examples = options.examples.filter(
|
||||
(ex): ex is Examples => ex !== "none",
|
||||
);
|
||||
if (options.examples.includes("none") && config.backend !== "convex") {
|
||||
config.examples = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.backend === "convex") {
|
||||
const incompatibleFlags: string[] = [];
|
||||
|
||||
if (providedFlags.has("auth") && options.auth === true)
|
||||
incompatibleFlags.push("--auth");
|
||||
if (providedFlags.has("database") && options.database !== "none")
|
||||
incompatibleFlags.push(`--database ${options.database}`);
|
||||
if (providedFlags.has("orm") && options.orm !== "none")
|
||||
incompatibleFlags.push(`--orm ${options.orm}`);
|
||||
if (providedFlags.has("api") && options.api !== "none")
|
||||
incompatibleFlags.push(`--api ${options.api}`);
|
||||
if (providedFlags.has("runtime") && options.runtime !== "none")
|
||||
incompatibleFlags.push(`--runtime ${options.runtime}`);
|
||||
if (providedFlags.has("dbSetup") && options.dbSetup !== "none")
|
||||
incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
|
||||
|
||||
if (incompatibleFlags.length > 0) {
|
||||
consola.fatal(
|
||||
`The following flags are incompatible with '--backend convex': ${incompatibleFlags.join(
|
||||
", ",
|
||||
)}. Please remove them.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (providedFlags.has("frontend") && options.frontend) {
|
||||
const incompatibleFrontends = options.frontend.filter(
|
||||
(f) => f === "nuxt" || f === "solid",
|
||||
);
|
||||
if (incompatibleFrontends.length > 0) {
|
||||
consola.fatal(
|
||||
`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(
|
||||
", ",
|
||||
)}. Please choose a different frontend or backend.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
config.auth = false;
|
||||
config.database = "none";
|
||||
config.orm = "none";
|
||||
config.api = "none";
|
||||
config.runtime = "none";
|
||||
config.dbSetup = "none";
|
||||
config.examples = ["todo"];
|
||||
} else if (config.backend === "none") {
|
||||
const incompatibleFlags: string[] = [];
|
||||
|
||||
if (providedFlags.has("auth") && options.auth === true)
|
||||
incompatibleFlags.push("--auth");
|
||||
if (providedFlags.has("database") && options.database !== "none")
|
||||
incompatibleFlags.push(`--database ${options.database}`);
|
||||
if (providedFlags.has("orm") && options.orm !== "none")
|
||||
incompatibleFlags.push(`--orm ${options.orm}`);
|
||||
if (providedFlags.has("api") && options.api !== "none")
|
||||
incompatibleFlags.push(`--api ${options.api}`);
|
||||
if (providedFlags.has("runtime") && options.runtime !== "none")
|
||||
incompatibleFlags.push(`--runtime ${options.runtime}`);
|
||||
if (providedFlags.has("dbSetup") && options.dbSetup !== "none")
|
||||
incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
|
||||
|
||||
if (incompatibleFlags.length > 0) {
|
||||
consola.fatal(
|
||||
`The following flags are incompatible with '--backend none': ${incompatibleFlags.join(
|
||||
", ",
|
||||
)}. Please remove them.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
config.auth = false;
|
||||
config.database = "none";
|
||||
config.orm = "none";
|
||||
config.api = "none";
|
||||
config.runtime = "none";
|
||||
config.dbSetup = "none";
|
||||
config.examples = [];
|
||||
} else {
|
||||
if (config.database === "none") {
|
||||
if (providedFlags.has("orm") && options.orm !== "none") {
|
||||
consola.fatal(
|
||||
`'--orm ${options.orm}' is incompatible with '--database none'. Please use '--orm none' or choose a database.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (providedFlags.has("auth") && options.auth === true) {
|
||||
consola.fatal(
|
||||
`'--auth' requires a database. Cannot use '--auth' with '--database none'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") {
|
||||
consola.fatal(
|
||||
`'--db-setup ${options.dbSetup}' requires a database. Cannot use with '--database none'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
config.orm = "none";
|
||||
config.auth = false;
|
||||
config.dbSetup = "none";
|
||||
|
||||
log.info(
|
||||
"Due to '--database none', '--orm' has been automatically set to 'none'.",
|
||||
);
|
||||
log.info(
|
||||
"Due to '--database none', '--auth' has been automatically set to 'false'.",
|
||||
);
|
||||
log.info(
|
||||
"Due to '--database none', '--db-setup' has been automatically set to 'none'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (config.orm === "mongoose") {
|
||||
if (!providedFlags.has("database")) {
|
||||
config.database = "mongodb";
|
||||
log.info(
|
||||
"Due to '--orm mongoose', '--database' has been automatically set to 'mongodb'.",
|
||||
);
|
||||
} else if (config.database !== "mongodb") {
|
||||
consola.fatal(
|
||||
`'--orm mongoose' requires '--database mongodb'. Cannot use '--orm mongoose' with '--database ${config.database}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.dbSetup) {
|
||||
if (config.dbSetup === "turso") {
|
||||
if (!providedFlags.has("database")) {
|
||||
config.database = "sqlite";
|
||||
log.info(
|
||||
"Due to '--db-setup turso', '--database' has been automatically set to 'sqlite'.",
|
||||
);
|
||||
} else if (config.database !== "sqlite") {
|
||||
consola.fatal(
|
||||
`'--db-setup turso' requires '--database sqlite'. Cannot use with '--database ${config.database}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!providedFlags.has("orm")) {
|
||||
config.orm = "drizzle";
|
||||
log.info(
|
||||
"Due to '--db-setup turso', '--orm' has been automatically set to 'drizzle'.",
|
||||
);
|
||||
} else if (config.orm !== "drizzle") {
|
||||
consola.fatal(
|
||||
`'--db-setup turso' requires '--orm drizzle'. Cannot use with '--orm ${config.orm}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (config.dbSetup === "prisma-postgres") {
|
||||
if (!providedFlags.has("database")) {
|
||||
config.database = "postgres";
|
||||
log.info(
|
||||
"Due to '--db-setup prisma-postgres', '--database' has been automatically set to 'postgres'.",
|
||||
);
|
||||
} else if (config.database !== "postgres") {
|
||||
consola.fatal(
|
||||
`'--db-setup prisma-postgres' requires '--database postgres'. Cannot use with '--database ${config.database}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!providedFlags.has("orm")) {
|
||||
config.orm = "prisma";
|
||||
log.info(
|
||||
"Due to '--db-setup prisma-postgres', '--orm' has been automatically set to 'prisma'.",
|
||||
);
|
||||
} else if (config.orm !== "prisma") {
|
||||
consola.fatal(
|
||||
`'--db-setup prisma-postgres' requires '--orm prisma'. Cannot use with '--orm ${config.orm}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (config.dbSetup === "supabase") {
|
||||
if (!providedFlags.has("database")) {
|
||||
config.database = "postgres";
|
||||
log.info(
|
||||
"Due to '--db-setup supabase', '--database' has been automatically set to 'postgres'.",
|
||||
);
|
||||
} else if (config.database !== "postgres") {
|
||||
consola.fatal(
|
||||
`'--db-setup supabase' requires '--database postgres'. Cannot use with '--database ${config.database}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (config.dbSetup === "neon") {
|
||||
if (!providedFlags.has("database")) {
|
||||
config.database = "postgres";
|
||||
log.info(
|
||||
"Due to '--db-setup neon', '--database' has been automatically set to 'postgres'.",
|
||||
);
|
||||
} else if (config.database !== "postgres") {
|
||||
consola.fatal(
|
||||
`'--db-setup neon' requires '--database postgres'. Cannot use with '--database ${config.database}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (config.dbSetup === "mongodb-atlas") {
|
||||
if (!providedFlags.has("database")) {
|
||||
config.database = "mongodb";
|
||||
log.info(
|
||||
"Due to '--db-setup mongodb-atlas', '--database' has been automatically set to 'mongodb'.",
|
||||
);
|
||||
} else if (config.database !== "mongodb") {
|
||||
consola.fatal(
|
||||
`'--db-setup mongodb-atlas' requires '--database mongodb'. Cannot use with '--database ${config.database}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.database === "mongodb" && config.orm === "drizzle") {
|
||||
consola.fatal(
|
||||
`'--database mongodb' is incompatible with '--orm drizzle'. Use '--orm mongoose' or '--orm prisma' with MongoDB.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function validateConfigCompatibility(
|
||||
config: Partial<ProjectConfig>,
|
||||
): void {
|
||||
const effectiveDatabase = config.database;
|
||||
const effectiveBackend = config.backend;
|
||||
const effectiveFrontend = config.frontend;
|
||||
const effectiveApi = config.api;
|
||||
|
||||
const includesNuxt = effectiveFrontend?.includes("nuxt");
|
||||
const includesSvelte = effectiveFrontend?.includes("svelte");
|
||||
const includesSolid = effectiveFrontend?.includes("solid");
|
||||
|
||||
if (
|
||||
(includesNuxt || includesSvelte || includesSolid) &&
|
||||
effectiveApi === "trpc"
|
||||
) {
|
||||
consola.fatal(
|
||||
`tRPC API is not supported with '${
|
||||
includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"
|
||||
}' frontend. Please use --api orpc or --api none or remove '${
|
||||
includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"
|
||||
}' from --frontend.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.addons && config.addons.length > 0) {
|
||||
const webSpecificAddons = ["pwa", "tauri"];
|
||||
const hasWebSpecificAddons = config.addons.some((addon) =>
|
||||
webSpecificAddons.includes(addon),
|
||||
);
|
||||
const hasCompatibleWebFrontend = effectiveFrontend?.some((f) => {
|
||||
const isPwaCompatible =
|
||||
f === "tanstack-router" ||
|
||||
f === "react-router" ||
|
||||
f === "solid" ||
|
||||
f === "next";
|
||||
const isTauriCompatible =
|
||||
f === "tanstack-router" ||
|
||||
f === "react-router" ||
|
||||
f === "nuxt" ||
|
||||
f === "svelte" ||
|
||||
f === "solid" ||
|
||||
f === "next";
|
||||
|
||||
if (config.addons?.includes("pwa") && config.addons?.includes("tauri")) {
|
||||
return isPwaCompatible && isTauriCompatible;
|
||||
}
|
||||
if (config.addons?.includes("pwa")) {
|
||||
return isPwaCompatible;
|
||||
}
|
||||
if (config.addons?.includes("tauri")) {
|
||||
return isTauriCompatible;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (hasWebSpecificAddons && !hasCompatibleWebFrontend) {
|
||||
let incompatibleReason = "Selected frontend is not compatible.";
|
||||
if (config.addons.includes("pwa")) {
|
||||
incompatibleReason =
|
||||
"PWA requires tanstack-router, react-router, next, or solid.";
|
||||
}
|
||||
if (config.addons.includes("tauri")) {
|
||||
incompatibleReason =
|
||||
"Tauri requires tanstack-router, react-router, nuxt, svelte, solid, or next.";
|
||||
}
|
||||
consola.fatal(
|
||||
`Incompatible addon/frontend combination: ${incompatibleReason}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.addons.includes("husky") && !config.addons.includes("biome")) {
|
||||
consola.warn(
|
||||
"Husky addon is recommended to be used with Biome for lint-staged configuration.",
|
||||
);
|
||||
}
|
||||
config.addons = [...new Set(config.addons)];
|
||||
}
|
||||
|
||||
const onlyNativeFrontend =
|
||||
effectiveFrontend &&
|
||||
effectiveFrontend.length === 1 &&
|
||||
(effectiveFrontend[0] === "native-nativewind" ||
|
||||
effectiveFrontend[0] === "native-unistyles");
|
||||
|
||||
if (
|
||||
onlyNativeFrontend &&
|
||||
config.examples &&
|
||||
config.examples.length > 0 &&
|
||||
!config.examples.includes("none")
|
||||
) {
|
||||
consola.fatal(
|
||||
"Examples are not supported when only a native frontend (NativeWind or Unistyles) is selected.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
config.examples &&
|
||||
config.examples.length > 0 &&
|
||||
!config.examples.includes("none")
|
||||
) {
|
||||
if (
|
||||
config.examples.includes("todo") &&
|
||||
effectiveBackend !== "convex" &&
|
||||
effectiveBackend !== "none" &&
|
||||
effectiveDatabase === "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
"The 'todo' example requires a database if a backend (other than Convex) is present. Cannot use --examples todo when database is 'none' and a backend is selected.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.examples.includes("ai") && effectiveBackend === "elysia") {
|
||||
consola.fatal(
|
||||
"The 'ai' example is not compatible with the Elysia backend.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.examples.includes("ai") && includesSolid) {
|
||||
consola.fatal(
|
||||
"The 'ai' example is not compatible with the Solid frontend.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user