Files
create-better-t-stack/apps/cli/src/utils/compatibility-rules.ts
2025-08-29 10:24:00 +05:30

290 lines
8.7 KiB
TypeScript

import type {
Addons,
API,
Backend,
CLIInput,
Frontend,
ProjectConfig,
ServerDeploy,
WebDeploy,
} from "../types";
import { validateAddonCompatibility } from "./addon-compatibility";
import { WEB_FRAMEWORKS } from "./compatibility";
import { exitWithError } from "./errors";
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) {
exitWithError(
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid",
);
}
if (native.length > 1) {
exitWithError(
"Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles",
);
}
}
export function validateWorkersCompatibility(
providedFlags: Set<string>,
options: CLIInput,
config: Partial<ProjectConfig>,
) {
if (
providedFlags.has("runtime") &&
options.runtime === "workers" &&
config.backend &&
config.backend !== "hono"
) {
exitWithError(
`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.`,
);
}
if (
providedFlags.has("backend") &&
config.backend &&
config.backend !== "hono" &&
config.runtime === "workers"
) {
exitWithError(
`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.`,
);
}
if (
providedFlags.has("runtime") &&
options.runtime === "workers" &&
config.orm &&
config.orm !== "drizzle" &&
config.orm !== "none"
) {
exitWithError(
`Cloudflare Workers runtime (--runtime workers) is only supported with Drizzle ORM (--orm drizzle) or no ORM (--orm none). Current ORM: ${config.orm}. Please use '--orm drizzle', '--orm none', or choose a different runtime.`,
);
}
if (
providedFlags.has("orm") &&
config.orm &&
config.orm !== "drizzle" &&
config.orm !== "none" &&
config.runtime === "workers"
) {
exitWithError(
`ORM '${config.orm}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Please use '--orm drizzle', '--orm none', or choose a different runtime.`,
);
}
if (
providedFlags.has("runtime") &&
options.runtime === "workers" &&
config.database === "mongodb"
) {
exitWithError(
"Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.",
);
}
if (
providedFlags.has("runtime") &&
options.runtime === "workers" &&
config.dbSetup === "docker"
) {
exitWithError(
"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.",
);
}
if (
providedFlags.has("database") &&
config.database === "mongodb" &&
config.runtime === "workers"
) {
exitWithError(
"MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.",
);
}
if (
providedFlags.has("dbSetup") &&
options.dbSetup === "docker" &&
config.runtime === "workers"
) {
exitWithError(
"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.",
);
}
}
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") {
exitWithError(
`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.`,
);
}
}
export function isFrontendAllowedWithBackend(
frontend: Frontend,
backend?: ProjectConfig["backend"],
auth?: string,
) {
if (backend === "convex" && frontend === "solid") return false;
if (auth === "clerk" && backend === "convex") {
const incompatibleFrontends = ["nuxt", "svelte", "solid"];
if (incompatibleFrontends.includes(frontend)) 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) {
exitWithError(
"'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.",
);
}
}
export function validateServerDeployRequiresBackend(
serverDeploy: ServerDeploy | undefined,
backend: Backend | undefined,
) {
if (
serverDeploy &&
serverDeploy !== "none" &&
(!backend || backend === "none")
) {
exitWithError(
"'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.",
);
}
}
export function validateAddonsAgainstFrontends(
addons: Addons[] = [],
frontends: Frontend[] = [],
) {
for (const addon of addons) {
if (addon === "none") continue;
const { isCompatible, reason } = validateAddonCompatibility(
addon,
frontends,
);
if (!isCompatible) {
exitWithError(`Incompatible addon/frontend combination: ${reason}`);
}
}
}
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"
) {
exitWithError(
"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.",
);
}
if (examplesArr.includes("ai") && backend === "elysia") {
exitWithError(
"The 'ai' example is not compatible with the Elysia backend.",
);
}
if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) {
exitWithError(
"The 'ai' example is not compatible with the Solid frontend.",
);
}
}
export function validateAlchemyCompatibility(
webDeploy: WebDeploy | undefined,
serverDeploy: ServerDeploy | undefined,
frontends: Frontend[] = [],
) {
const isAlchemyWebDeploy = webDeploy === "alchemy";
const isAlchemyServerDeploy = serverDeploy === "alchemy";
if (isAlchemyWebDeploy || isAlchemyServerDeploy) {
const incompatibleFrontends = frontends.filter((f) => f === "next");
if (incompatibleFrontends.length > 0) {
const deployType =
isAlchemyWebDeploy && isAlchemyServerDeploy
? "web and server deployment"
: isAlchemyWebDeploy
? "web deployment"
: "server deployment";
exitWithError(
`Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")} frontend(s). Please choose a different frontend or deployment option.`,
);
}
}
}