mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(web): improve docs and refactor cli (#476)
This commit is contained in:
@@ -139,14 +139,3 @@ export const ADDON_COMPATIBILITY: Record<Addons, readonly Frontend[]> = {
|
||||
fumadocs: [],
|
||||
none: [],
|
||||
} as const;
|
||||
|
||||
// TODO: need to refactor this
|
||||
export const WEB_FRAMEWORKS: readonly Frontend[] = [
|
||||
"tanstack-router",
|
||||
"react-router",
|
||||
"tanstack-start",
|
||||
"next",
|
||||
"nuxt",
|
||||
"svelte",
|
||||
"solid",
|
||||
];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type { API, Backend, Frontend } from "../types";
|
||||
import { allowedApisForFrontends } from "../utils/compatibility-rules";
|
||||
|
||||
export async function getApiChoice(
|
||||
Api?: API | undefined,
|
||||
@@ -11,46 +12,30 @@ export async function getApiChoice(
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (Api) return Api;
|
||||
const allowed = allowedApisForFrontends(frontend ?? []);
|
||||
|
||||
const includesNuxt = frontend?.includes("nuxt");
|
||||
const includesSvelte = frontend?.includes("svelte");
|
||||
const includesSolid = frontend?.includes("solid");
|
||||
|
||||
let apiOptions = [
|
||||
{
|
||||
value: "trpc" as const,
|
||||
label: "tRPC",
|
||||
hint: "End-to-end typesafe APIs made easy",
|
||||
},
|
||||
{
|
||||
value: "orpc" as const,
|
||||
label: "oRPC",
|
||||
hint: "End-to-end type-safe APIs that adhere to OpenAPI standards",
|
||||
},
|
||||
{
|
||||
value: "none" as const,
|
||||
label: "None",
|
||||
hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)",
|
||||
},
|
||||
];
|
||||
|
||||
if (includesNuxt || includesSvelte || includesSolid) {
|
||||
apiOptions = [
|
||||
{
|
||||
value: "orpc" as const,
|
||||
label: "oRPC",
|
||||
hint: `End-to-end type-safe APIs (Recommended for ${
|
||||
includesNuxt ? "Nuxt" : includesSvelte ? "Svelte" : "Solid"
|
||||
} frontend)`,
|
||||
},
|
||||
{
|
||||
value: "none" as const,
|
||||
label: "None",
|
||||
hint: "No API layer",
|
||||
},
|
||||
];
|
||||
if (Api) {
|
||||
return allowed.includes(Api) ? Api : allowed[0];
|
||||
}
|
||||
const apiOptions = allowed.map((a) =>
|
||||
a === "trpc"
|
||||
? {
|
||||
value: "trpc" as const,
|
||||
label: "tRPC",
|
||||
hint: "End-to-end typesafe APIs made easy",
|
||||
}
|
||||
: a === "orpc"
|
||||
? {
|
||||
value: "orpc" as const,
|
||||
label: "oRPC",
|
||||
hint: "End-to-end type-safe APIs that adhere to OpenAPI standards",
|
||||
}
|
||||
: {
|
||||
value: "none" as const,
|
||||
label: "None",
|
||||
hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)",
|
||||
},
|
||||
);
|
||||
|
||||
const apiType = await select<API>({
|
||||
message: "Select API type",
|
||||
|
||||
@@ -2,6 +2,10 @@ import { cancel, isCancel, multiselect } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { API, Backend, Database, Examples, Frontend } from "../types";
|
||||
import {
|
||||
isExampleAIAllowed,
|
||||
isExampleTodoAllowed,
|
||||
} from "../utils/compatibility-rules";
|
||||
|
||||
export async function getExamplesChoice(
|
||||
examples?: Examples[],
|
||||
@@ -30,15 +34,17 @@ export async function getExamplesChoice(
|
||||
if (noFrontendSelected) return [];
|
||||
|
||||
let response: Examples[] | symbol = [];
|
||||
const options: { value: Examples; label: string; hint: string }[] = [
|
||||
{
|
||||
const options: { value: Examples; label: string; hint: string }[] = [];
|
||||
|
||||
if (isExampleTodoAllowed(backend, database)) {
|
||||
options.push({
|
||||
value: "todo" as const,
|
||||
label: "Todo App",
|
||||
hint: "A simple CRUD example app",
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
if (backend !== "elysia" && !frontends?.includes("solid")) {
|
||||
if (isExampleAIAllowed(backend, frontends ?? [])) {
|
||||
options.push({
|
||||
value: "ai" as const,
|
||||
label: "AI Chat",
|
||||
@@ -46,11 +52,15 @@ export async function getExamplesChoice(
|
||||
});
|
||||
}
|
||||
|
||||
if (options.length === 0) return [];
|
||||
|
||||
response = await multiselect<Examples>({
|
||||
message: "Include examples",
|
||||
options: options,
|
||||
required: false,
|
||||
initialValues: DEFAULT_CONFIG.examples,
|
||||
initialValues: DEFAULT_CONFIG.examples?.filter((ex) =>
|
||||
options.some((o) => o.value === ex),
|
||||
),
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { cancel, isCancel, multiselect, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { Backend, Frontend } from "../types";
|
||||
import { isFrontendAllowedWithBackend } from "../utils/compatibility-rules";
|
||||
|
||||
export async function getFrontendChoice(
|
||||
frontendOptions?: Frontend[],
|
||||
@@ -73,12 +74,9 @@ export async function getFrontendChoice(
|
||||
},
|
||||
];
|
||||
|
||||
const webOptions = allWebOptions.filter((option) => {
|
||||
if (backend === "convex") {
|
||||
return option.value !== "solid";
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const webOptions = allWebOptions.filter((option) =>
|
||||
isFrontendAllowedWithBackend(option.value, backend),
|
||||
);
|
||||
|
||||
const webFramework = await select<Frontend>({
|
||||
message: "Choose web",
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import path from "node:path";
|
||||
import { cancel, isCancel, text } from "@clack/prompts";
|
||||
import consola from "consola";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import { ProjectNameSchema } from "../types";
|
||||
|
||||
function isPathWithinCwd(targetPath: string): boolean {
|
||||
const resolved = path.resolve(targetPath);
|
||||
const rel = path.relative(process.cwd(), resolved);
|
||||
return !rel.startsWith("..") && !path.isAbsolute(rel);
|
||||
}
|
||||
|
||||
function validateDirectoryName(name: string): string | undefined {
|
||||
if (name === ".") return undefined;
|
||||
|
||||
@@ -23,7 +30,11 @@ export async function getProjectName(initialName?: string): Promise<string> {
|
||||
const finalDirName = path.basename(initialName);
|
||||
const validationError = validateDirectoryName(finalDirName);
|
||||
if (!validationError) {
|
||||
return initialName;
|
||||
const projectDir = path.resolve(process.cwd(), initialName);
|
||||
if (isPathWithinCwd(projectDir)) {
|
||||
return initialName;
|
||||
}
|
||||
consola.error(pc.red("Project path must be within current directory"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +67,7 @@ export async function getProjectName(initialName?: string): Promise<string> {
|
||||
|
||||
if (nameToUse !== ".") {
|
||||
const projectDir = path.resolve(process.cwd(), nameToUse);
|
||||
if (!projectDir.startsWith(process.cwd())) {
|
||||
if (!isPathWithinCwd(projectDir)) {
|
||||
return "Project path must be within current directory";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG, WEB_FRAMEWORKS } from "../constants";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { Backend, Frontend, Runtime, WebDeploy } from "../types";
|
||||
import { WEB_FRAMEWORKS } from "../utils/compatibility";
|
||||
|
||||
function hasWebFrontend(frontends: Frontend[]): boolean {
|
||||
return frontends.some((f) => WEB_FRAMEWORKS.includes(f));
|
||||
|
||||
@@ -17,9 +17,7 @@ export type Backend = z.infer<typeof BackendSchema>;
|
||||
|
||||
export const RuntimeSchema = z
|
||||
.enum(["bun", "node", "workers", "none"])
|
||||
.describe(
|
||||
"Runtime environment (workers only available with hono backend and drizzle orm)",
|
||||
);
|
||||
.describe("Runtime environment");
|
||||
export type Runtime = z.infer<typeof RuntimeSchema>;
|
||||
|
||||
export const FrontendSchema = z
|
||||
@@ -176,5 +174,3 @@ export interface BetterTStackConfig {
|
||||
api: API;
|
||||
webDeploy: WebDeploy;
|
||||
}
|
||||
|
||||
export type AvailablePackageManagers = "npm" | "pnpm" | "bun";
|
||||
|
||||
315
apps/cli/src/utils/compatibility-rules.ts
Normal file
315
apps/cli/src/utils/compatibility-rules.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
import { consola } from "consola";
|
||||
import type {
|
||||
Addons,
|
||||
API,
|
||||
CLIInput,
|
||||
Frontend,
|
||||
ProjectConfig,
|
||||
WebDeploy,
|
||||
} from "../types";
|
||||
import { validateAddonCompatibility } from "./addon-compatibility";
|
||||
import { WEB_FRAMEWORKS } from "./compatibility";
|
||||
|
||||
export function isWebFrontend(value: Frontend): boolean {
|
||||
return WEB_FRAMEWORKS.includes(value);
|
||||
}
|
||||
|
||||
export function splitFrontends(values: Frontend[] = []): {
|
||||
web: Frontend[];
|
||||
native: Frontend[];
|
||||
} {
|
||||
const web = values.filter((f) => isWebFrontend(f));
|
||||
const native = values.filter(
|
||||
(f) => f === "native-nativewind" || f === "native-unistyles",
|
||||
);
|
||||
return { web, native };
|
||||
}
|
||||
|
||||
export function ensureSingleWebAndNative(frontends: Frontend[]) {
|
||||
const { web, native } = splitFrontends(frontends);
|
||||
if (web.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 (native.length > 1) {
|
||||
consola.fatal(
|
||||
"Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateWorkersCompatibility(
|
||||
providedFlags: Set<string>,
|
||||
options: CLIInput,
|
||||
config: Partial<ProjectConfig>,
|
||||
) {
|
||||
if (
|
||||
providedFlags.has("runtime") &&
|
||||
options.runtime === "workers" &&
|
||||
config.backend &&
|
||||
config.backend !== "hono"
|
||||
) {
|
||||
consola.fatal(
|
||||
`Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("backend") &&
|
||||
config.backend &&
|
||||
config.backend !== "hono" &&
|
||||
config.runtime === "workers"
|
||||
) {
|
||||
consola.fatal(
|
||||
`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("runtime") &&
|
||||
options.runtime === "workers" &&
|
||||
config.orm &&
|
||||
config.orm !== "drizzle" &&
|
||||
config.orm !== "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
`Cloudflare Workers runtime (--runtime workers) is only supported with Drizzle ORM (--orm drizzle) or no ORM (--orm none). Current ORM: ${config.orm}. Please use '--orm drizzle', '--orm none', or choose a different runtime.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("orm") &&
|
||||
config.orm &&
|
||||
config.orm !== "drizzle" &&
|
||||
config.orm !== "none" &&
|
||||
config.runtime === "workers"
|
||||
) {
|
||||
consola.fatal(
|
||||
`ORM '${config.orm}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Please use '--orm drizzle', '--orm none', or choose a different runtime.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("runtime") &&
|
||||
options.runtime === "workers" &&
|
||||
config.database === "mongodb"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("runtime") &&
|
||||
options.runtime === "workers" &&
|
||||
config.dbSetup === "docker"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Cloudflare Workers runtime (--runtime workers) is not compatible with Docker setup. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("database") &&
|
||||
config.database === "mongodb" &&
|
||||
config.runtime === "workers"
|
||||
) {
|
||||
consola.fatal(
|
||||
"MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
options.dbSetup === "docker" &&
|
||||
config.runtime === "workers"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Docker setup (--db-setup docker) is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export function coerceBackendPresets(config: Partial<ProjectConfig>) {
|
||||
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"] as ProjectConfig["examples"];
|
||||
}
|
||||
if (config.backend === "none") {
|
||||
config.auth = false;
|
||||
config.database = "none";
|
||||
config.orm = "none";
|
||||
config.api = "none";
|
||||
config.runtime = "none";
|
||||
config.dbSetup = "none";
|
||||
config.examples = [] as ProjectConfig["examples"];
|
||||
}
|
||||
}
|
||||
|
||||
export function incompatibleFlagsForBackend(
|
||||
backend: ProjectConfig["backend"] | undefined,
|
||||
providedFlags: Set<string>,
|
||||
options: CLIInput,
|
||||
): string[] {
|
||||
const list: string[] = [];
|
||||
if (backend === "convex") {
|
||||
if (providedFlags.has("auth") && options.auth === true) list.push("--auth");
|
||||
if (providedFlags.has("database") && options.database !== "none")
|
||||
list.push(`--database ${options.database}`);
|
||||
if (providedFlags.has("orm") && options.orm !== "none")
|
||||
list.push(`--orm ${options.orm}`);
|
||||
if (providedFlags.has("api") && options.api !== "none")
|
||||
list.push(`--api ${options.api}`);
|
||||
if (providedFlags.has("runtime") && options.runtime !== "none")
|
||||
list.push(`--runtime ${options.runtime}`);
|
||||
if (providedFlags.has("dbSetup") && options.dbSetup !== "none")
|
||||
list.push(`--db-setup ${options.dbSetup}`);
|
||||
}
|
||||
if (backend === "none") {
|
||||
if (providedFlags.has("auth") && options.auth === true) list.push("--auth");
|
||||
if (providedFlags.has("database") && options.database !== "none")
|
||||
list.push(`--database ${options.database}`);
|
||||
if (providedFlags.has("orm") && options.orm !== "none")
|
||||
list.push(`--orm ${options.orm}`);
|
||||
if (providedFlags.has("api") && options.api !== "none")
|
||||
list.push(`--api ${options.api}`);
|
||||
if (providedFlags.has("runtime") && options.runtime !== "none")
|
||||
list.push(`--runtime ${options.runtime}`);
|
||||
if (providedFlags.has("dbSetup") && options.dbSetup !== "none")
|
||||
list.push(`--db-setup ${options.dbSetup}`);
|
||||
if (providedFlags.has("examples") && options.examples) {
|
||||
const hasNonNoneExamples = options.examples.some((ex) => ex !== "none");
|
||||
if (hasNonNoneExamples) list.push("--examples");
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
export function validateApiFrontendCompatibility(
|
||||
api: API | undefined,
|
||||
frontends: Frontend[] = [],
|
||||
) {
|
||||
const includesNuxt = frontends.includes("nuxt");
|
||||
const includesSvelte = frontends.includes("svelte");
|
||||
const includesSolid = frontends.includes("solid");
|
||||
if ((includesNuxt || includesSvelte || includesSolid) && api === "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);
|
||||
}
|
||||
}
|
||||
|
||||
export function isFrontendAllowedWithBackend(
|
||||
frontend: Frontend,
|
||||
backend?: ProjectConfig["backend"],
|
||||
) {
|
||||
if (backend === "convex" && frontend === "solid") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function allowedApisForFrontends(frontends: Frontend[] = []): API[] {
|
||||
const includesNuxt = frontends.includes("nuxt");
|
||||
const includesSvelte = frontends.includes("svelte");
|
||||
const includesSolid = frontends.includes("solid");
|
||||
const base: API[] = ["trpc", "orpc", "none"];
|
||||
if (includesNuxt || includesSvelte || includesSolid) {
|
||||
return ["orpc", "none"];
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
export function isExampleTodoAllowed(
|
||||
backend?: ProjectConfig["backend"],
|
||||
database?: ProjectConfig["database"],
|
||||
) {
|
||||
return !(backend !== "convex" && backend !== "none" && database === "none");
|
||||
}
|
||||
|
||||
export function isExampleAIAllowed(
|
||||
backend?: ProjectConfig["backend"],
|
||||
frontends: Frontend[] = [],
|
||||
) {
|
||||
const includesSolid = frontends.includes("solid");
|
||||
if (backend === "elysia") return false;
|
||||
if (includesSolid) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function validateWebDeployRequiresWebFrontend(
|
||||
webDeploy: WebDeploy | undefined,
|
||||
hasWebFrontendFlag: boolean,
|
||||
) {
|
||||
if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) {
|
||||
consola.fatal(
|
||||
"'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateAddonsAgainstFrontends(
|
||||
addons: Addons[] = [],
|
||||
frontends: Frontend[] = [],
|
||||
) {
|
||||
for (const addon of addons) {
|
||||
if (addon === "none") continue;
|
||||
const { isCompatible, reason } = validateAddonCompatibility(
|
||||
addon,
|
||||
frontends,
|
||||
);
|
||||
if (!isCompatible) {
|
||||
consola.fatal(`Incompatible addon/frontend combination: ${reason}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validateExamplesCompatibility(
|
||||
examples: string[] | undefined,
|
||||
backend: ProjectConfig["backend"] | undefined,
|
||||
database: ProjectConfig["database"] | undefined,
|
||||
frontend?: Frontend[],
|
||||
) {
|
||||
const examplesArr = examples ?? [];
|
||||
if (examplesArr.length === 0 || examplesArr.includes("none")) return;
|
||||
if (
|
||||
examplesArr.includes("todo") &&
|
||||
backend !== "convex" &&
|
||||
backend !== "none" &&
|
||||
database === "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 (examplesArr.includes("ai") && backend === "elysia") {
|
||||
consola.fatal(
|
||||
"The 'ai' example is not compatible with the Elysia backend.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) {
|
||||
consola.fatal(
|
||||
"The 'ai' example is not compatible with the Solid frontend.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
11
apps/cli/src/utils/compatibility.ts
Normal file
11
apps/cli/src/utils/compatibility.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { Frontend } from "../types";
|
||||
|
||||
export const WEB_FRAMEWORKS: readonly Frontend[] = [
|
||||
"tanstack-router",
|
||||
"react-router",
|
||||
"tanstack-start",
|
||||
"next",
|
||||
"nuxt",
|
||||
"svelte",
|
||||
"solid",
|
||||
] as const;
|
||||
@@ -34,15 +34,15 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
|
||||
flags.push(`--package-manager ${config.packageManager}`);
|
||||
flags.push(config.install ? "--install" : "--no-install");
|
||||
|
||||
let baseCommand = "";
|
||||
let baseCommand = "npx create-better-t-stack@latest";
|
||||
const pkgManager = config.packageManager;
|
||||
|
||||
if (pkgManager === "npm") {
|
||||
baseCommand = "npx create-better-t-stack@latest";
|
||||
if (pkgManager === "bun") {
|
||||
baseCommand = "bun create better-t-stack@latest";
|
||||
} else if (pkgManager === "pnpm") {
|
||||
baseCommand = "pnpm create better-t-stack@latest";
|
||||
} else if (pkgManager === "bun") {
|
||||
baseCommand = "bun create better-t-stack@latest";
|
||||
} else if (pkgManager === "npm") {
|
||||
baseCommand = "npx create-better-t-stack@latest";
|
||||
}
|
||||
|
||||
const projectPathArg = config.relativePath ? ` ${config.relativePath}` : "";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { consola } from "consola";
|
||||
import { WEB_FRAMEWORKS } from "./constants";
|
||||
import {
|
||||
type Addons,
|
||||
type API,
|
||||
@@ -17,6 +16,17 @@ import {
|
||||
type Runtime,
|
||||
type WebDeploy,
|
||||
} from "./types";
|
||||
import {
|
||||
coerceBackendPresets,
|
||||
ensureSingleWebAndNative,
|
||||
incompatibleFlagsForBackend,
|
||||
isWebFrontend,
|
||||
validateAddonsAgainstFrontends,
|
||||
validateApiFrontendCompatibility,
|
||||
validateExamplesCompatibility,
|
||||
validateWebDeployRequiresWebFrontend,
|
||||
validateWorkersCompatibility,
|
||||
} from "./utils/compatibility-rules";
|
||||
|
||||
export function processAndValidateFlags(
|
||||
options: CLIInput,
|
||||
@@ -126,28 +136,20 @@ export function processAndValidateFlags(
|
||||
const validOptions = options.frontend.filter(
|
||||
(f): f is Frontend => f !== "none",
|
||||
);
|
||||
const webFrontends = validOptions.filter((f) =>
|
||||
WEB_FRAMEWORKS.includes(f),
|
||||
);
|
||||
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);
|
||||
}
|
||||
ensureSingleWebAndNative(validOptions);
|
||||
config.frontend = validOptions;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("api") &&
|
||||
providedFlags.has("frontend") &&
|
||||
config.api &&
|
||||
config.frontend &&
|
||||
config.frontend.length > 0
|
||||
) {
|
||||
validateApiFrontendCompatibility(config.api, config.frontend);
|
||||
}
|
||||
if (options.addons && options.addons.length > 0) {
|
||||
if (options.addons.includes("none")) {
|
||||
if (options.addons.length > 1) {
|
||||
@@ -178,32 +180,26 @@ export function processAndValidateFlags(
|
||||
}
|
||||
}
|
||||
|
||||
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 (config.backend === "convex" || config.backend === "none") {
|
||||
const incompatibleFlags = incompatibleFlagsForBackend(
|
||||
config.backend,
|
||||
providedFlags,
|
||||
options,
|
||||
);
|
||||
if (incompatibleFlags.length > 0) {
|
||||
consola.fatal(
|
||||
`The following flags are incompatible with '--backend convex': ${incompatibleFlags.join(
|
||||
`The following flags are incompatible with '--backend ${config.backend}': ${incompatibleFlags.join(
|
||||
", ",
|
||||
)}. Please remove them.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (providedFlags.has("frontend") && options.frontend) {
|
||||
if (
|
||||
config.backend === "convex" &&
|
||||
providedFlags.has("frontend") &&
|
||||
options.frontend
|
||||
) {
|
||||
const incompatibleFrontends = options.frontend.filter(
|
||||
(f) => f === "solid",
|
||||
);
|
||||
@@ -217,54 +213,15 @@ export function processAndValidateFlags(
|
||||
}
|
||||
}
|
||||
|
||||
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 (providedFlags.has("examples") && options.examples) {
|
||||
const hasNonNoneExamples = options.examples.some((ex) => ex !== "none");
|
||||
if (hasNonNoneExamples) {
|
||||
incompatibleFlags.push("--examples");
|
||||
}
|
||||
}
|
||||
|
||||
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 = [];
|
||||
coerceBackendPresets(config);
|
||||
}
|
||||
|
||||
if (config.orm === "mongoose" && config.database !== "mongodb") {
|
||||
if (
|
||||
providedFlags.has("orm") &&
|
||||
providedFlags.has("database") &&
|
||||
config.orm === "mongoose" &&
|
||||
config.database !== "mongodb"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.",
|
||||
);
|
||||
@@ -272,6 +229,8 @@ export function processAndValidateFlags(
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("database") &&
|
||||
providedFlags.has("orm") &&
|
||||
config.database === "mongodb" &&
|
||||
config.orm &&
|
||||
config.orm !== "mongoose" &&
|
||||
@@ -283,28 +242,50 @@ export function processAndValidateFlags(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.orm === "drizzle" && config.database === "mongodb") {
|
||||
if (
|
||||
providedFlags.has("orm") &&
|
||||
providedFlags.has("database") &&
|
||||
config.orm === "drizzle" &&
|
||||
config.database === "mongodb"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.database && config.database !== "none" && config.orm === "none") {
|
||||
if (
|
||||
providedFlags.has("database") &&
|
||||
providedFlags.has("orm") &&
|
||||
config.database &&
|
||||
config.database !== "none" &&
|
||||
config.orm === "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.orm && config.orm !== "none" && config.database === "none") {
|
||||
if (
|
||||
providedFlags.has("orm") &&
|
||||
providedFlags.has("database") &&
|
||||
config.orm &&
|
||||
config.orm !== "none" &&
|
||||
config.database === "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
"ORM selection requires a database. Please choose a database or set '--orm none'.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.auth && config.database === "none") {
|
||||
if (
|
||||
providedFlags.has("auth") &&
|
||||
providedFlags.has("database") &&
|
||||
config.auth &&
|
||||
config.database === "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Authentication requires a database. Please choose a database or set '--no-auth'.",
|
||||
);
|
||||
@@ -312,6 +293,8 @@ export function processAndValidateFlags(
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
providedFlags.has("database") &&
|
||||
config.dbSetup &&
|
||||
config.dbSetup !== "none" &&
|
||||
config.database === "none"
|
||||
@@ -322,35 +305,60 @@ export function processAndValidateFlags(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.dbSetup === "turso" && config.database !== "sqlite") {
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
(config.database ? providedFlags.has("database") : true) &&
|
||||
config.dbSetup === "turso" &&
|
||||
config.database !== "sqlite"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.dbSetup === "neon" && config.database !== "postgres") {
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
(config.database ? providedFlags.has("database") : true) &&
|
||||
config.dbSetup === "neon" &&
|
||||
config.database !== "postgres"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.dbSetup === "prisma-postgres" && config.database !== "postgres") {
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
(config.database ? providedFlags.has("database") : true) &&
|
||||
config.dbSetup === "prisma-postgres" &&
|
||||
config.database !== "postgres"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.dbSetup === "mongodb-atlas" && config.database !== "mongodb") {
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
(config.database ? providedFlags.has("database") : true) &&
|
||||
config.dbSetup === "mongodb-atlas" &&
|
||||
config.database !== "mongodb"
|
||||
) {
|
||||
consola.fatal(
|
||||
"MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.dbSetup === "supabase" && config.database !== "postgres") {
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
(config.database ? providedFlags.has("database") : true) &&
|
||||
config.dbSetup === "supabase" &&
|
||||
config.database !== "postgres"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
);
|
||||
@@ -358,144 +366,61 @@ export function processAndValidateFlags(
|
||||
}
|
||||
|
||||
if (config.dbSetup === "d1") {
|
||||
if (config.database !== "sqlite") {
|
||||
consola.fatal(
|
||||
"Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
if (
|
||||
(providedFlags.has("dbSetup") && providedFlags.has("database")) ||
|
||||
(providedFlags.has("dbSetup") && !config.database)
|
||||
) {
|
||||
if (config.database !== "sqlite") {
|
||||
consola.fatal(
|
||||
"Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.runtime !== "workers") {
|
||||
consola.fatal(
|
||||
"Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
if (
|
||||
(providedFlags.has("dbSetup") && providedFlags.has("runtime")) ||
|
||||
(providedFlags.has("dbSetup") && !config.runtime)
|
||||
) {
|
||||
if (config.runtime !== "workers") {
|
||||
consola.fatal(
|
||||
"Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.dbSetup === "docker" && config.database === "sqlite") {
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
providedFlags.has("database") &&
|
||||
config.dbSetup === "docker" &&
|
||||
config.database === "sqlite"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.dbSetup === "docker" && config.runtime === "workers") {
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
providedFlags.has("runtime") &&
|
||||
config.dbSetup === "docker" &&
|
||||
config.runtime === "workers"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("runtime") &&
|
||||
options.runtime === "workers" &&
|
||||
config.backend &&
|
||||
config.backend !== "hono"
|
||||
) {
|
||||
consola.fatal(
|
||||
`Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("backend") &&
|
||||
config.backend &&
|
||||
config.backend !== "hono" &&
|
||||
config.runtime === "workers"
|
||||
) {
|
||||
consola.fatal(
|
||||
`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("runtime") &&
|
||||
options.runtime === "workers" &&
|
||||
config.orm &&
|
||||
config.orm !== "drizzle" &&
|
||||
config.orm !== "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
`Cloudflare Workers runtime (--runtime workers) is only supported with Drizzle ORM (--orm drizzle) or no ORM (--orm none). Current ORM: ${config.orm}. Please use '--orm drizzle', '--orm none', or choose a different runtime.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("orm") &&
|
||||
config.orm &&
|
||||
config.orm !== "drizzle" &&
|
||||
config.orm !== "none" &&
|
||||
config.runtime === "workers"
|
||||
) {
|
||||
consola.fatal(
|
||||
`ORM '${config.orm}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Please use '--orm drizzle', '--orm none', or choose a different runtime.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("runtime") &&
|
||||
options.runtime === "workers" &&
|
||||
config.database === "mongodb"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("runtime") &&
|
||||
options.runtime === "workers" &&
|
||||
config.dbSetup === "docker"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Cloudflare Workers runtime (--runtime workers) is not compatible with Docker setup. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("database") &&
|
||||
config.database === "mongodb" &&
|
||||
config.runtime === "workers"
|
||||
) {
|
||||
consola.fatal(
|
||||
"MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("db-setup") &&
|
||||
options.dbSetup === "docker" &&
|
||||
config.runtime === "workers"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Docker setup (--db-setup docker) is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
validateWorkersCompatibility(providedFlags, options, config);
|
||||
|
||||
const hasWebFrontendFlag = (config.frontend ?? []).some((f) =>
|
||||
WEB_FRAMEWORKS.includes(f),
|
||||
isWebFrontend(f),
|
||||
);
|
||||
|
||||
if (
|
||||
config.webDeploy &&
|
||||
config.webDeploy !== "none" &&
|
||||
!hasWebFrontendFlag &&
|
||||
providedFlags.has("frontend")
|
||||
) {
|
||||
consola.fatal(
|
||||
"'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -505,140 +430,20 @@ export function validateConfigCompatibility(config: Partial<ProjectConfig>) {
|
||||
const effectiveBackend = config.backend;
|
||||
const effectiveFrontend = config.frontend;
|
||||
const effectiveApi = config.api;
|
||||
const effectiveRuntime = config.runtime;
|
||||
|
||||
if (effectiveRuntime === "workers" && effectiveBackend !== "hono") {
|
||||
consola.fatal(
|
||||
`Cloudflare Workers runtime is only supported with Hono backend. Current backend: ${effectiveBackend}. Please use a different runtime or change to Hono backend.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const effectiveOrm = config.orm;
|
||||
if (
|
||||
effectiveRuntime === "workers" &&
|
||||
effectiveOrm !== "drizzle" &&
|
||||
effectiveOrm !== "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
`Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Current ORM: ${effectiveOrm}. Please use a different runtime or change to Drizzle ORM or no ORM.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (effectiveRuntime === "workers" && effectiveDatabase === "mongodb") {
|
||||
consola.fatal(
|
||||
"Cloudflare Workers runtime is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (effectiveRuntime === "workers" && config.dbSetup === "docker") {
|
||||
consola.fatal(
|
||||
"Cloudflare Workers runtime is not compatible with Docker setup. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use a different runtime or change to D1 database setup.",
|
||||
);
|
||||
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);
|
||||
}
|
||||
validateApiFrontendCompatibility(effectiveApi, effectiveFrontend);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
validateAddonsAgainstFrontends(config.addons, effectiveFrontend);
|
||||
config.addons = [...new Set(config.addons)];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
validateExamplesCompatibility(
|
||||
config.examples ?? [],
|
||||
effectiveBackend,
|
||||
effectiveDatabase,
|
||||
effectiveFrontend ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
export function getProvidedFlags(options: CLIInput): Set<string> {
|
||||
|
||||
Reference in New Issue
Block a user