mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
fix(cli): improve flags handling (#552)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@ import type {
|
|||||||
ProjectConfig,
|
ProjectConfig,
|
||||||
} from "../../types";
|
} from "../../types";
|
||||||
import { trackProjectCreation } from "../../utils/analytics";
|
import { trackProjectCreation } from "../../utils/analytics";
|
||||||
import { coerceBackendPresets } from "../../utils/compatibility-rules";
|
|
||||||
import { displayConfig } from "../../utils/display-config";
|
import { displayConfig } from "../../utils/display-config";
|
||||||
import { exitWithError, handleError } from "../../utils/errors";
|
import { exitWithError, handleError } from "../../utils/errors";
|
||||||
import { generateReproducibleCommand } from "../../utils/generate-reproducible-command";
|
import { generateReproducibleCommand } from "../../utils/generate-reproducible-command";
|
||||||
@@ -148,20 +148,8 @@ export async function createProjectHandler(
|
|||||||
relativePath: finalPathInput,
|
relativePath: finalPathInput,
|
||||||
};
|
};
|
||||||
|
|
||||||
coerceBackendPresets(config);
|
|
||||||
|
|
||||||
validateConfigCompatibility(config, providedFlags, cliInput);
|
validateConfigCompatibility(config, providedFlags, cliInput);
|
||||||
|
|
||||||
if (config.backend === "convex") {
|
|
||||||
log.info(
|
|
||||||
`Due to '--backend convex' flag, the following options have been automatically set: database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo`,
|
|
||||||
);
|
|
||||||
} else if (config.backend === "none") {
|
|
||||||
log.info(
|
|
||||||
"Due to '--backend none', the following options have been automatically set: --auth none, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
|
log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
|
||||||
log.message(displayConfig(config));
|
log.message(displayConfig(config));
|
||||||
log.message("");
|
log.message("");
|
||||||
|
|||||||
@@ -133,72 +133,6 @@ export function validateWorkersCompatibility(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function coerceBackendPresets(config: Partial<ProjectConfig>) {
|
|
||||||
if (config.backend === "convex") {
|
|
||||||
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 = "none" as ProjectConfig["auth"];
|
|
||||||
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 &&
|
|
||||||
options.auth !== "none" &&
|
|
||||||
options.auth !== "clerk"
|
|
||||||
)
|
|
||||||
list.push(`--auth ${options.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 && options.auth !== "none")
|
|
||||||
list.push(`--auth ${options.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(
|
export function validateApiFrontendCompatibility(
|
||||||
api: API | undefined,
|
api: API | undefined,
|
||||||
frontends: Frontend[] = [],
|
frontends: Frontend[] = [],
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import type {
|
|||||||
Runtime,
|
Runtime,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
coerceBackendPresets,
|
|
||||||
ensureSingleWebAndNative,
|
ensureSingleWebAndNative,
|
||||||
incompatibleFlagsForBackend,
|
|
||||||
isWebFrontend,
|
isWebFrontend,
|
||||||
validateAddonsAgainstFrontends,
|
validateAddonsAgainstFrontends,
|
||||||
validateAlchemyCompatibility,
|
validateAlchemyCompatibility,
|
||||||
@@ -226,38 +224,19 @@ export function validateBackendConstraints(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (backend === "convex" || backend === "none") {
|
if (
|
||||||
const incompatibleFlags = incompatibleFlagsForBackend(
|
backend === "convex" &&
|
||||||
backend,
|
providedFlags.has("frontend") &&
|
||||||
providedFlags,
|
options.frontend
|
||||||
options,
|
) {
|
||||||
);
|
const incompatibleFrontends = options.frontend.filter((f) => f === "solid");
|
||||||
if (incompatibleFlags.length > 0) {
|
if (incompatibleFrontends.length > 0) {
|
||||||
exitWithError(
|
exitWithError(
|
||||||
`The following flags are incompatible with '--backend ${backend}': ${incompatibleFlags.join(
|
`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(
|
||||||
", ",
|
", ",
|
||||||
)}. Please remove them.`,
|
)}. Please choose a different frontend or backend.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
backend === "convex" &&
|
|
||||||
providedFlags.has("frontend") &&
|
|
||||||
options.frontend
|
|
||||||
) {
|
|
||||||
const incompatibleFrontends = options.frontend.filter(
|
|
||||||
(f) => f === "solid",
|
|
||||||
);
|
|
||||||
if (incompatibleFrontends.length > 0) {
|
|
||||||
exitWithError(
|
|
||||||
`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(
|
|
||||||
", ",
|
|
||||||
)}. Please choose a different frontend or backend.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coerceBackendPresets(config);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,39 @@ import {
|
|||||||
import { exitWithError } from "./utils/errors";
|
import { exitWithError } from "./utils/errors";
|
||||||
import { extractAndValidateProjectName } from "./utils/project-name-validation";
|
import { extractAndValidateProjectName } from "./utils/project-name-validation";
|
||||||
|
|
||||||
|
const CORE_STACK_FLAGS = new Set([
|
||||||
|
"database",
|
||||||
|
"orm",
|
||||||
|
"backend",
|
||||||
|
"runtime",
|
||||||
|
"frontend",
|
||||||
|
"addons",
|
||||||
|
"examples",
|
||||||
|
"auth",
|
||||||
|
"dbSetup",
|
||||||
|
"api",
|
||||||
|
"webDeploy",
|
||||||
|
"serverDeploy",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function validateYesFlagCombination(
|
||||||
|
options: CLIInput,
|
||||||
|
providedFlags: Set<string>,
|
||||||
|
) {
|
||||||
|
if (!options.yes) return;
|
||||||
|
|
||||||
|
const coreStackFlagsProvided = Array.from(providedFlags).filter((flag) =>
|
||||||
|
CORE_STACK_FLAGS.has(flag),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (coreStackFlagsProvided.length > 0) {
|
||||||
|
exitWithError(
|
||||||
|
`Cannot combine --yes with core stack configuration flags: ${coreStackFlagsProvided.map((f) => `--${f}`).join(", ")}. ` +
|
||||||
|
"The --yes flag uses default configuration. Remove these flags or use --yes without them.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function processAndValidateFlags(
|
export function processAndValidateFlags(
|
||||||
options: CLIInput,
|
options: CLIInput,
|
||||||
providedFlags: Set<string>,
|
providedFlags: Set<string>,
|
||||||
@@ -29,6 +62,8 @@ export function processAndValidateFlags(
|
|||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateYesFlagCombination(options, providedFlags);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
validateArrayOptions(options);
|
validateArrayOptions(options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -55,6 +90,11 @@ export function processProvidedFlagsWithoutValidation(
|
|||||||
options: CLIInput,
|
options: CLIInput,
|
||||||
projectName?: string,
|
projectName?: string,
|
||||||
): Partial<ProjectConfig> {
|
): Partial<ProjectConfig> {
|
||||||
|
if (!options.yolo) {
|
||||||
|
const providedFlags = getProvidedFlags(options);
|
||||||
|
validateYesFlagCombination(options, providedFlags);
|
||||||
|
}
|
||||||
|
|
||||||
const config = processFlags(options, projectName);
|
const config = processFlags(options, projectName);
|
||||||
|
|
||||||
const validatedProjectName = extractAndValidateProjectName(
|
const validatedProjectName = extractAndValidateProjectName(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -83,7 +83,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
describe("Core functionality", () => {
|
describe("Core functionality", () => {
|
||||||
test("creates minimal project successfully", async () => {
|
test("creates minimal project successfully", async () => {
|
||||||
const result = await init("test-app", {
|
const result = await init("test-app", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
|
database: "sqlite",
|
||||||
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
});
|
});
|
||||||
@@ -100,7 +112,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
|
|
||||||
test("returns complete result structure", async () => {
|
test("returns complete result structure", async () => {
|
||||||
const result = await init("result-test", {
|
const result = await init("result-test", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
|
database: "sqlite",
|
||||||
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
});
|
});
|
||||||
@@ -116,7 +140,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
|
|
||||||
test("handles project with custom name", async () => {
|
test("handles project with custom name", async () => {
|
||||||
const result = await init("custom-name", {
|
const result = await init("custom-name", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
|
database: "sqlite",
|
||||||
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
});
|
});
|
||||||
@@ -130,8 +166,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
describe("Configuration options", () => {
|
describe("Configuration options", () => {
|
||||||
test("creates project with Next.js frontend", async () => {
|
test("creates project with Next.js frontend", async () => {
|
||||||
const result = await init("next-app", {
|
const result = await init("next-app", {
|
||||||
yes: true,
|
|
||||||
frontend: ["next"],
|
frontend: ["next"],
|
||||||
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
|
database: "sqlite",
|
||||||
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
});
|
});
|
||||||
@@ -144,8 +191,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
|
|
||||||
test("creates project with Fastify backend", async () => {
|
test("creates project with Fastify backend", async () => {
|
||||||
const result = await init("fastify-app", {
|
const result = await init("fastify-app", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
backend: "fastify",
|
backend: "fastify",
|
||||||
|
runtime: "bun",
|
||||||
|
database: "sqlite",
|
||||||
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
});
|
});
|
||||||
@@ -158,9 +216,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
|
|
||||||
test("creates project with PostgreSQL + Prisma", async () => {
|
test("creates project with PostgreSQL + Prisma", async () => {
|
||||||
const result = await init("pg-app", {
|
const result = await init("pg-app", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
database: "postgres",
|
database: "postgres",
|
||||||
orm: "prisma",
|
orm: "prisma",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
});
|
});
|
||||||
@@ -174,8 +242,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
|
|
||||||
test("creates project with oRPC API", async () => {
|
test("creates project with oRPC API", async () => {
|
||||||
const result = await init("orpc-app", {
|
const result = await init("orpc-app", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
|
database: "sqlite",
|
||||||
|
orm: "drizzle",
|
||||||
api: "orpc",
|
api: "orpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
});
|
});
|
||||||
@@ -188,8 +267,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
|
|
||||||
test("creates project with Node runtime", async () => {
|
test("creates project with Node runtime", async () => {
|
||||||
const result = await init("node-app", {
|
const result = await init("node-app", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
|
backend: "hono",
|
||||||
runtime: "node",
|
runtime: "node",
|
||||||
|
database: "sqlite",
|
||||||
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
});
|
});
|
||||||
@@ -202,8 +292,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
|
|
||||||
test("creates project with Biome addon", async () => {
|
test("creates project with Biome addon", async () => {
|
||||||
const result = await init("biome-app", {
|
const result = await init("biome-app", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
|
database: "sqlite",
|
||||||
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
addons: ["biome"],
|
addons: ["biome"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
});
|
});
|
||||||
@@ -216,7 +317,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
|
|
||||||
test("creates project with analytics disabled", async () => {
|
test("creates project with analytics disabled", async () => {
|
||||||
const result = await init("no-analytics-app", {
|
const result = await init("no-analytics-app", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
|
database: "sqlite",
|
||||||
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
disableAnalytics: true,
|
disableAnalytics: true,
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
@@ -231,7 +344,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
test("handles invalid project name", async () => {
|
test("handles invalid project name", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
init("", {
|
init("", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
|
database: "sqlite",
|
||||||
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
}),
|
}),
|
||||||
@@ -241,7 +366,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
test("handles invalid characters in project name", async () => {
|
test("handles invalid characters in project name", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
init("invalid<name>", {
|
init("invalid<name>", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
|
database: "sqlite",
|
||||||
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
}),
|
}),
|
||||||
@@ -251,9 +388,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
test("handles incompatible database + ORM combination", async () => {
|
test("handles incompatible database + ORM combination", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
init("incompatible", {
|
init("incompatible", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
database: "mongodb",
|
database: "mongodb",
|
||||||
orm: "drizzle",
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
yolo: false,
|
yolo: false,
|
||||||
@@ -264,19 +411,41 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
test("handles auth without database", async () => {
|
test("handles auth without database", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
init("auth-no-db", {
|
init("auth-no-db", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
auth: "better-auth",
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
database: "none",
|
database: "none",
|
||||||
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
yolo: false,
|
yolo: false,
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow(/Authentication requires/);
|
).rejects.toThrow(/ORM selection requires a database/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles directory conflict with error strategy", async () => {
|
test("handles directory conflict with error strategy", async () => {
|
||||||
const result1 = await init("conflict-test", {
|
const result1 = await init("conflict-test", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
|
database: "sqlite",
|
||||||
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
});
|
});
|
||||||
@@ -284,7 +453,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
expect(result1.success).toBe(true);
|
expect(result1.success).toBe(true);
|
||||||
|
|
||||||
const result2 = await init("conflict-test", {
|
const result2 = await init("conflict-test", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
|
database: "sqlite",
|
||||||
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
directoryConflict: "error",
|
directoryConflict: "error",
|
||||||
@@ -298,8 +479,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
describe("Advanced features", () => {
|
describe("Advanced features", () => {
|
||||||
test("creates project with multiple addons", async () => {
|
test("creates project with multiple addons", async () => {
|
||||||
const result = await init("multi-addon", {
|
const result = await init("multi-addon", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
|
database: "sqlite",
|
||||||
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
addons: ["biome", "turborepo"],
|
addons: ["biome", "turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
});
|
});
|
||||||
@@ -312,10 +504,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
|
|
||||||
test("creates project with authentication enabled", async () => {
|
test("creates project with authentication enabled", async () => {
|
||||||
const result = await init("auth-app", {
|
const result = await init("auth-app", {
|
||||||
yes: true,
|
frontend: ["tanstack-router"],
|
||||||
auth: "better-auth",
|
backend: "hono",
|
||||||
|
runtime: "bun",
|
||||||
database: "sqlite",
|
database: "sqlite",
|
||||||
orm: "drizzle",
|
orm: "drizzle",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
});
|
});
|
||||||
@@ -330,11 +531,19 @@ describe("Programmatic API - Fast Tests", () => {
|
|||||||
|
|
||||||
test("validates reproducible command format", async () => {
|
test("validates reproducible command format", async () => {
|
||||||
const result = await init("repro-test", {
|
const result = await init("repro-test", {
|
||||||
yes: true,
|
|
||||||
frontend: ["next"],
|
frontend: ["next"],
|
||||||
backend: "fastify",
|
backend: "fastify",
|
||||||
|
runtime: "bun",
|
||||||
database: "postgres",
|
database: "postgres",
|
||||||
orm: "prisma",
|
orm: "prisma",
|
||||||
|
api: "trpc",
|
||||||
|
auth: "better-auth",
|
||||||
|
dbSetup: "none",
|
||||||
|
webDeploy: "none",
|
||||||
|
serverDeploy: "none",
|
||||||
|
addons: ["turborepo"],
|
||||||
|
examples: ["none"],
|
||||||
|
packageManager: "bun",
|
||||||
install: false,
|
install: false,
|
||||||
git: false,
|
git: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
if (JSON.stringify(nextStack[catKey]) !== JSON.stringify(value)) {
|
if (JSON.stringify(nextStack[catKey]) !== JSON.stringify(value)) {
|
||||||
const displayName = getCategoryDisplayName(catKey);
|
const displayName = getCategoryDisplayName(catKey);
|
||||||
const valueDisplay = Array.isArray(value) ? value.join(", ") : value;
|
const valueDisplay = Array.isArray(value) ? value.join(", ") : value;
|
||||||
const message = `${displayName} set to '${valueDisplay}'`;
|
const message = `${displayName} set to '${valueDisplay}' (Convex backend requires this configuration)`;
|
||||||
|
|
||||||
notes[catKey].notes.push(
|
notes[catKey].notes.push(
|
||||||
`Convex backend selected: ${displayName} will be set to '${valueDisplay}'.`,
|
`Convex backend selected: ${displayName} will be set to '${valueDisplay}'.`,
|
||||||
@@ -263,7 +263,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
notes.backend.hasIssue = true;
|
notes.backend.hasIssue = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "convex",
|
category: "convex",
|
||||||
message: "Removed incompatible web frontends (Solid)",
|
message: "Removed Solid frontend (not compatible with Convex backend)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (nextStack.nativeFrontend[0] === "none") {
|
if (nextStack.nativeFrontend[0] === "none") {
|
||||||
@@ -285,7 +285,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
if (JSON.stringify(nextStack[catKey]) !== JSON.stringify(value)) {
|
if (JSON.stringify(nextStack[catKey]) !== JSON.stringify(value)) {
|
||||||
const displayName = getCategoryDisplayName(catKey);
|
const displayName = getCategoryDisplayName(catKey);
|
||||||
const valueDisplay = Array.isArray(value) ? "none" : value;
|
const valueDisplay = Array.isArray(value) ? "none" : value;
|
||||||
const message = `${displayName} set to '${valueDisplay}'`;
|
const message = `${displayName} set to '${valueDisplay}' (no backend selected)`;
|
||||||
|
|
||||||
notes[catKey].notes.push(
|
notes[catKey].notes.push(
|
||||||
`No backend selected: ${displayName} will be set to '${valueDisplay}'.`,
|
`No backend selected: ${displayName} will be set to '${valueDisplay}'.`,
|
||||||
@@ -312,7 +312,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "runtime",
|
category: "runtime",
|
||||||
message: "Runtime set to 'Bun' (None is only for Convex)",
|
message:
|
||||||
|
"Runtime set to 'Bun' (runtime 'None' is only available with Convex backend)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (nextStack.api === "none" && (isConvex || isBackendNone)) {
|
if (nextStack.api === "none" && (isConvex || isBackendNone)) {
|
||||||
@@ -328,7 +329,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "api",
|
category: "api",
|
||||||
message: "Examples removed (API 'None' does not support examples)",
|
message:
|
||||||
|
"Examples removed (examples require an API layer but 'None' was selected)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,7 +349,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "database",
|
category: "database",
|
||||||
message: "ORM set to 'None' (requires a database)",
|
message:
|
||||||
|
"ORM set to 'None' (ORM requires a database but 'None' was selected)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (nextStack.auth !== "none" && nextStack.backend !== "convex") {
|
if (nextStack.auth !== "none" && nextStack.backend !== "convex") {
|
||||||
@@ -363,7 +366,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "database",
|
category: "database",
|
||||||
message: "Authentication set to 'None' (requires a database)",
|
message:
|
||||||
|
"Authentication set to 'None' (auth requires a database but 'None' was selected)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (nextStack.dbSetup !== "none") {
|
if (nextStack.dbSetup !== "none") {
|
||||||
@@ -379,7 +383,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "database",
|
category: "database",
|
||||||
message: "DB Setup set to 'None' (requires a database)",
|
message:
|
||||||
|
"DB Setup set to 'None' (database setup requires a database but 'None' was selected)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (nextStack.database === "mongodb") {
|
} else if (nextStack.database === "mongodb") {
|
||||||
@@ -396,7 +401,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "database",
|
category: "database",
|
||||||
message: "ORM set to 'Prisma' (MongoDB requires Prisma or Mongoose)",
|
message:
|
||||||
|
"ORM set to 'Prisma' (MongoDB database only works with Prisma or Mongoose ORM)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -413,7 +419,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "database",
|
category: "database",
|
||||||
message: "ORM set to 'Drizzle' (Mongoose only works with MongoDB)",
|
message:
|
||||||
|
"ORM set to 'Drizzle' (Mongoose ORM only works with MongoDB database)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (nextStack.dbSetup === "turso") {
|
if (nextStack.dbSetup === "turso") {
|
||||||
@@ -430,7 +437,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "dbSetup",
|
category: "dbSetup",
|
||||||
message: "Database set to 'SQLite' (required by Turso)",
|
message:
|
||||||
|
"Database set to 'SQLite' (Turso hosting requires SQLite database)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (nextStack.orm !== "drizzle") {
|
if (nextStack.orm !== "drizzle") {
|
||||||
@@ -446,7 +454,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "dbSetup",
|
category: "dbSetup",
|
||||||
message: "ORM set to 'Drizzle' (required by Turso)",
|
message:
|
||||||
|
"ORM set to 'Drizzle' (Turso hosting requires Drizzle ORM)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (nextStack.dbSetup === "prisma-postgres") {
|
} else if (nextStack.dbSetup === "prisma-postgres") {
|
||||||
@@ -495,7 +504,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changes.push({
|
changes.push({
|
||||||
category: "dbSetup",
|
category: "dbSetup",
|
||||||
message:
|
message:
|
||||||
"ORM set to 'Prisma' (MongoDB Atlas requires Prisma or Mongoose)",
|
"ORM set to 'Prisma' (MongoDB Atlas with current setup requires Prisma ORM)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (nextStack.dbSetup === "neon") {
|
} else if (nextStack.dbSetup === "neon") {
|
||||||
@@ -512,7 +521,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "dbSetup",
|
category: "dbSetup",
|
||||||
message: "Database set to 'PostgreSQL' (required by Neon)",
|
message:
|
||||||
|
"Database set to 'PostgreSQL' (Neon hosting requires PostgreSQL database)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (nextStack.dbSetup === "supabase") {
|
} else if (nextStack.dbSetup === "supabase") {
|
||||||
@@ -530,7 +540,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changes.push({
|
changes.push({
|
||||||
category: "dbSetup",
|
category: "dbSetup",
|
||||||
message:
|
message:
|
||||||
"Database set to 'PostgreSQL' (required by Supabase setup)",
|
"Database set to 'PostgreSQL' (Supabase hosting requires PostgreSQL database)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (nextStack.dbSetup === "d1") {
|
} else if (nextStack.dbSetup === "d1") {
|
||||||
@@ -647,21 +657,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "runtime",
|
category: "runtime",
|
||||||
message: "Backend set to 'Hono' (required by Cloudflare Workers)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextStack.serverDeploy === "none") {
|
|
||||||
notes.serverDeploy.notes.push(
|
|
||||||
"Cloudflare Workers runtime requires a server deployment. Wrangler will be selected.",
|
|
||||||
);
|
|
||||||
notes.serverDeploy.hasIssue = true;
|
|
||||||
nextStack.serverDeploy = "wrangler";
|
|
||||||
changed = true;
|
|
||||||
changes.push({
|
|
||||||
category: "serverDeploy",
|
|
||||||
message:
|
message:
|
||||||
"Server deployment set to 'Wrangler' (required by Cloudflare Workers)",
|
"Backend set to 'Hono' (Cloudflare Workers runtime only works with Hono backend)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,7 +675,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "runtime",
|
category: "runtime",
|
||||||
message: "ORM set to 'Drizzle' (required by Cloudflare Workers)",
|
message:
|
||||||
|
"ORM set to 'Drizzle' (Cloudflare Workers runtime only supports Drizzle or no ORM)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,7 +694,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changes.push({
|
changes.push({
|
||||||
category: "runtime",
|
category: "runtime",
|
||||||
message:
|
message:
|
||||||
"Database set to 'SQLite' (MongoDB not compatible with Workers)",
|
"Database set to 'SQLite' (MongoDB not compatible with Cloudflare Workers runtime)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -714,9 +712,48 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changes.push({
|
changes.push({
|
||||||
category: "runtime",
|
category: "runtime",
|
||||||
message:
|
message:
|
||||||
"DB Setup set to 'D1' (Docker not compatible with Workers)",
|
"DB Setup set to 'D1' (Docker setup not compatible with Cloudflare Workers runtime)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (nextStack.serverDeploy === "wrangler") {
|
||||||
|
notes.runtime.notes.push(
|
||||||
|
"Wrangler deployment requires Cloudflare Workers runtime. Server deployment disabled.",
|
||||||
|
);
|
||||||
|
notes.serverDeploy.notes.push(
|
||||||
|
"Selected runtime is not compatible with Wrangler deployment. Server deployment disabled.",
|
||||||
|
);
|
||||||
|
notes.runtime.hasIssue = true;
|
||||||
|
notes.serverDeploy.hasIssue = true;
|
||||||
|
nextStack.serverDeploy = "none";
|
||||||
|
changed = true;
|
||||||
|
changes.push({
|
||||||
|
category: "runtime",
|
||||||
|
message:
|
||||||
|
"Server deployment set to 'None' (Wrangler requires Cloudflare Workers runtime)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
nextStack.backend !== "hono" &&
|
||||||
|
nextStack.serverDeploy === "wrangler"
|
||||||
|
) {
|
||||||
|
notes.backend.notes.push(
|
||||||
|
"Wrangler deployment requires Hono backend (via Workers runtime). Server deployment disabled.",
|
||||||
|
);
|
||||||
|
notes.serverDeploy.notes.push(
|
||||||
|
"Selected backend is not compatible with Wrangler deployment. Server deployment disabled.",
|
||||||
|
);
|
||||||
|
notes.backend.hasIssue = true;
|
||||||
|
notes.serverDeploy.hasIssue = true;
|
||||||
|
nextStack.serverDeploy = "none";
|
||||||
|
changed = true;
|
||||||
|
changes.push({
|
||||||
|
category: "backend",
|
||||||
|
message:
|
||||||
|
"Server deployment set to 'None' (Wrangler requires Hono backend via Workers runtime)",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isNuxt = nextStack.webFrontend.includes("nuxt");
|
const isNuxt = nextStack.webFrontend.includes("nuxt");
|
||||||
@@ -754,7 +791,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "auth",
|
category: "auth",
|
||||||
message: "Auth set to 'None' (Clerk only available with Convex)",
|
message:
|
||||||
|
"Auth set to 'None' (Clerk authentication only works with Convex backend)",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const hasClerkCompatibleFrontend =
|
const hasClerkCompatibleFrontend =
|
||||||
@@ -784,7 +822,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changes.push({
|
changes.push({
|
||||||
category: "auth",
|
category: "auth",
|
||||||
message:
|
message:
|
||||||
"Auth set to 'None' (Clerk not compatible with selected frontends)",
|
"Auth set to 'None' (Clerk not compatible with Svelte, Nuxt, or Solid frontends)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -804,7 +842,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changes.push({
|
changes.push({
|
||||||
category: "auth",
|
category: "auth",
|
||||||
message:
|
message:
|
||||||
"Auth set to 'None' (Better-Auth not compatible with Convex)",
|
"Auth set to 'None' (Better-Auth not compatible with Convex backend - use Clerk instead)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -824,7 +862,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
notes.addons.hasIssue = true;
|
notes.addons.hasIssue = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "addons",
|
category: "addons",
|
||||||
message: "PWA addon removed (requires compatible web frontend)",
|
message:
|
||||||
|
"PWA addon removed (only works with TanStack Router, React Router, Solid, or Next.js)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!isTauriCompat && nextStack.addons.includes("tauri")) {
|
if (!isTauriCompat && nextStack.addons.includes("tauri")) {
|
||||||
@@ -839,7 +878,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
notes.addons.hasIssue = true;
|
notes.addons.hasIssue = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "addons",
|
category: "addons",
|
||||||
message: "Tauri addon removed (requires compatible web frontend)",
|
message:
|
||||||
|
"Tauri addon removed (only works with TanStack Router, React Router, Nuxt, Svelte, Solid, or Next.js)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -872,7 +912,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "addons",
|
category: "addons",
|
||||||
message: "Biome addon removed (included in Ultracite)",
|
message:
|
||||||
|
"Biome addon removed (Ultracite already includes Biome configuration)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -895,21 +936,22 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
incompatibleExamples.push("todo");
|
incompatibleExamples.push("todo");
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "examples",
|
category: "examples",
|
||||||
message: "Todo example removed (requires a database)",
|
message:
|
||||||
|
"Todo example removed (requires a database but 'None' was selected)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (nextStack.backend === "elysia" && nextStack.examples.includes("ai")) {
|
if (nextStack.backend === "elysia" && nextStack.examples.includes("ai")) {
|
||||||
incompatibleExamples.push("ai");
|
incompatibleExamples.push("ai");
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "examples",
|
category: "examples",
|
||||||
message: "AI example removed (not compatible with Elysia)",
|
message: "AI example removed (not compatible with Elysia backend)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isSolid && nextStack.examples.includes("ai")) {
|
if (isSolid && nextStack.examples.includes("ai")) {
|
||||||
incompatibleExamples.push("ai");
|
incompatibleExamples.push("ai");
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "examples",
|
category: "examples",
|
||||||
message: "AI example removed (not compatible with Solid)",
|
message: "AI example removed (not compatible with Solid frontend)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -976,7 +1018,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "webDeploy",
|
category: "webDeploy",
|
||||||
message: "Web deployment set to 'none' (requires web frontend)",
|
message:
|
||||||
|
"Web deployment set to 'None' (requires a web frontend but only native frontend selected)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -996,91 +1039,41 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "serverDeploy",
|
category: "serverDeploy",
|
||||||
message: "Server deployment set to 'none' (requires backend)",
|
message:
|
||||||
|
"Server deployment set to 'None' (requires a backend but 'None' or 'Convex' was selected)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextStack.serverDeploy !== "none" && nextStack.runtime !== "workers") {
|
if (
|
||||||
|
nextStack.serverDeploy === "wrangler" &&
|
||||||
|
(nextStack.runtime !== "workers" || nextStack.backend !== "hono")
|
||||||
|
) {
|
||||||
notes.serverDeploy.notes.push(
|
notes.serverDeploy.notes.push(
|
||||||
"Selected server deployment targets Cloudflare Workers. Runtime will be set to 'Cloudflare Workers'.",
|
"Wrangler deployment requires Cloudflare Workers runtime and Hono backend. Server deployment disabled.",
|
||||||
);
|
);
|
||||||
notes.runtime.notes.push(
|
notes.serverDeploy.notes.push(
|
||||||
"Server deployment requires Cloudflare Workers runtime. It will be selected.",
|
"To use Wrangler: Set Runtime to 'Cloudflare Workers' and Backend to 'Hono', then re-enable Wrangler deployment.",
|
||||||
);
|
);
|
||||||
|
if (nextStack.runtime !== "workers") {
|
||||||
|
notes.runtime.notes.push(
|
||||||
|
"Selected runtime is not compatible with Wrangler deployment. Switch to 'Cloudflare Workers' to use Wrangler.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (nextStack.backend !== "hono") {
|
||||||
|
notes.backend.notes.push(
|
||||||
|
"Selected backend is not compatible with Wrangler deployment. Switch to 'Hono' to use Wrangler.",
|
||||||
|
);
|
||||||
|
}
|
||||||
notes.serverDeploy.hasIssue = true;
|
notes.serverDeploy.hasIssue = true;
|
||||||
notes.runtime.hasIssue = true;
|
notes.runtime.hasIssue = true;
|
||||||
nextStack.runtime = "workers";
|
notes.backend.hasIssue = true;
|
||||||
|
nextStack.serverDeploy = "none";
|
||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "serverDeploy",
|
category: "serverDeploy",
|
||||||
message:
|
message:
|
||||||
"Runtime set to 'Cloudflare Workers' (required by server deployment)",
|
"Server deployment disabled (Tip: Use Cloudflare Workers runtime + Hono backend to enable Wrangler)",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (nextStack.backend !== "hono") {
|
|
||||||
notes.runtime.notes.push(
|
|
||||||
"Cloudflare Workers runtime requires Hono backend. Hono will be selected.",
|
|
||||||
);
|
|
||||||
notes.backend.notes.push(
|
|
||||||
"Cloudflare Workers runtime requires Hono backend. It will be selected.",
|
|
||||||
);
|
|
||||||
notes.runtime.hasIssue = true;
|
|
||||||
notes.backend.hasIssue = true;
|
|
||||||
nextStack.backend = "hono";
|
|
||||||
changes.push({
|
|
||||||
category: "runtime",
|
|
||||||
message: "Backend set to 'Hono' (required by Cloudflare Workers)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextStack.orm !== "drizzle" && nextStack.orm !== "none") {
|
|
||||||
notes.runtime.notes.push(
|
|
||||||
"Cloudflare Workers runtime requires Drizzle ORM or no ORM. Drizzle will be selected.",
|
|
||||||
);
|
|
||||||
notes.orm.notes.push(
|
|
||||||
"Cloudflare Workers runtime requires Drizzle ORM or no ORM. Drizzle will be selected.",
|
|
||||||
);
|
|
||||||
notes.runtime.hasIssue = true;
|
|
||||||
notes.orm.hasIssue = true;
|
|
||||||
nextStack.orm = "drizzle";
|
|
||||||
changes.push({
|
|
||||||
category: "runtime",
|
|
||||||
message: "ORM set to 'Drizzle' (required by Cloudflare Workers)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextStack.database === "mongodb") {
|
|
||||||
notes.runtime.notes.push(
|
|
||||||
"Cloudflare Workers runtime is not compatible with MongoDB. SQLite will be selected.",
|
|
||||||
);
|
|
||||||
notes.database.notes.push(
|
|
||||||
"MongoDB is not compatible with Cloudflare Workers runtime. SQLite will be selected.",
|
|
||||||
);
|
|
||||||
notes.runtime.hasIssue = true;
|
|
||||||
notes.database.hasIssue = true;
|
|
||||||
nextStack.database = "sqlite";
|
|
||||||
changes.push({
|
|
||||||
category: "runtime",
|
|
||||||
message:
|
|
||||||
"Database set to 'SQLite' (MongoDB not compatible with Workers)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextStack.dbSetup === "docker") {
|
|
||||||
notes.runtime.notes.push(
|
|
||||||
"Cloudflare Workers runtime does not support Docker setup. D1 will be selected.",
|
|
||||||
);
|
|
||||||
notes.dbSetup.notes.push(
|
|
||||||
"Docker setup is not compatible with Cloudflare Workers runtime. D1 will be selected.",
|
|
||||||
);
|
|
||||||
notes.runtime.hasIssue = true;
|
|
||||||
notes.dbSetup.hasIssue = true;
|
|
||||||
nextStack.dbSetup = "d1";
|
|
||||||
changes.push({
|
|
||||||
category: "runtime",
|
|
||||||
message: "DB Setup set to 'D1' (Docker not compatible with Workers)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAlchemyWebDeploy = nextStack.webDeploy === "alchemy";
|
const isAlchemyWebDeploy = nextStack.webDeploy === "alchemy";
|
||||||
@@ -1121,7 +1114,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
category: "alchemy",
|
category: "alchemy",
|
||||||
message: `Removed ${incompatibleFrontends.join(" and ")} (not compatible with Alchemy ${deployType})`,
|
message: `Removed ${incompatibleFrontends.join(" and ")} frontend (temporarily not compatible with Alchemy ${deployType} - support coming soon)`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1172,21 +1165,17 @@ const generateCommand = (stackState: StackState): string => {
|
|||||||
|
|
||||||
flags.push(`--backend ${stackState.backend}`);
|
flags.push(`--backend ${stackState.backend}`);
|
||||||
|
|
||||||
if (stackState.backend !== "convex") {
|
flags.push(`--runtime ${stackState.runtime}`);
|
||||||
flags.push(`--runtime ${stackState.runtime}`);
|
|
||||||
|
|
||||||
flags.push(`--api ${stackState.api}`);
|
flags.push(`--api ${stackState.api}`);
|
||||||
|
|
||||||
flags.push(`--database ${stackState.database}`);
|
flags.push(`--auth ${stackState.auth}`);
|
||||||
|
|
||||||
flags.push(`--orm ${stackState.orm}`);
|
flags.push(`--database ${stackState.database}`);
|
||||||
|
|
||||||
flags.push(`--auth ${stackState.auth}`);
|
flags.push(`--orm ${stackState.orm}`);
|
||||||
|
|
||||||
flags.push(`--db-setup ${stackState.dbSetup}`);
|
flags.push(`--db-setup ${stackState.dbSetup}`);
|
||||||
} else {
|
|
||||||
flags.push(`--auth ${stackState.auth}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
flags.push(`--package-manager ${stackState.packageManager}`);
|
flags.push(`--package-manager ${stackState.packageManager}`);
|
||||||
|
|
||||||
@@ -1276,27 +1265,26 @@ const StackBuilder = () => {
|
|||||||
if (
|
if (
|
||||||
["webFrontend", "nativeFrontend", "addons", "examples"].includes(catKey)
|
["webFrontend", "nativeFrontend", "addons", "examples"].includes(catKey)
|
||||||
) {
|
) {
|
||||||
const currentValues: string[] = [];
|
|
||||||
randomStack[
|
|
||||||
catKey as "webFrontend" | "nativeFrontend" | "addons" | "examples"
|
|
||||||
] = currentValues;
|
|
||||||
|
|
||||||
if (catKey === "webFrontend" || catKey === "nativeFrontend") {
|
if (catKey === "webFrontend" || catKey === "nativeFrontend") {
|
||||||
const randomIndex = Math.floor(Math.random() * options.length);
|
const randomIndex = Math.floor(Math.random() * options.length);
|
||||||
const selectedOption = options[randomIndex].id;
|
const selectedOption = options[randomIndex].id;
|
||||||
currentValues.push(selectedOption);
|
randomStack[catKey as "webFrontend" | "nativeFrontend"] = [
|
||||||
if (selectedOption === "none" && currentValues.length > 1) {
|
selectedOption,
|
||||||
randomStack[catKey] = ["none"];
|
];
|
||||||
} else if (selectedOption !== "none") {
|
|
||||||
randomStack[catKey] = currentValues.filter((id) => id !== "none");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const numToPick = Math.floor(
|
const numToPick = Math.floor(
|
||||||
Math.random() * Math.min(options.length + 1, 4),
|
Math.random() * Math.min(options.length, 4),
|
||||||
);
|
);
|
||||||
const shuffledOptions = [...options].sort(() => 0.5 - Math.random());
|
if (numToPick === 0) {
|
||||||
for (let i = 0; i < numToPick; i++) {
|
randomStack[catKey as "addons" | "examples"] = ["none"];
|
||||||
currentValues.push(shuffledOptions[i].id);
|
} else {
|
||||||
|
const shuffledOptions = [...options]
|
||||||
|
.filter((opt) => opt.id !== "none")
|
||||||
|
.sort(() => 0.5 - Math.random())
|
||||||
|
.slice(0, numToPick);
|
||||||
|
randomStack[catKey as "addons" | "examples"] = shuffledOptions.map(
|
||||||
|
(opt) => opt.id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1475,18 +1463,26 @@ const StackBuilder = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setLastChanges(compatibilityAnalysis.changes);
|
setLastChanges(compatibilityAnalysis.changes);
|
||||||
setStack(compatibilityAnalysis.adjustedStack);
|
|
||||||
|
const isStackDifferent =
|
||||||
|
JSON.stringify(stack) !==
|
||||||
|
JSON.stringify(compatibilityAnalysis.adjustedStack);
|
||||||
|
if (isStackDifferent) {
|
||||||
|
setStack(compatibilityAnalysis.adjustedStack);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
compatibilityAnalysis.adjustedStack,
|
compatibilityAnalysis.adjustedStack,
|
||||||
setStack,
|
setStack,
|
||||||
compatibilityAnalysis.changes,
|
compatibilityAnalysis.changes,
|
||||||
|
stack,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cmd = generateCommand(stack);
|
const stackToUse = compatibilityAnalysis.adjustedStack || stack;
|
||||||
|
const cmd = generateCommand(stackToUse);
|
||||||
setCommand(cmd);
|
setCommand(cmd);
|
||||||
}, [stack]);
|
}, [stack, compatibilityAnalysis.adjustedStack]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProjectNameError(validateProjectName(stack.projectName || ""));
|
setProjectNameError(validateProjectName(stack.projectName || ""));
|
||||||
|
|||||||
Reference in New Issue
Block a user