mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
migrate to trpc-cli (#313)
This commit is contained in:
@@ -1,134 +0,0 @@
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
import type { YargsArgv } from "./types";
|
||||
import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
|
||||
|
||||
export async function parseCliArguments(): Promise<YargsArgv> {
|
||||
const argv = await yargs(hideBin(process.argv))
|
||||
.scriptName("create-better-t-stack")
|
||||
.usage(
|
||||
"$0 [project-directory] [options]",
|
||||
"Create a new Better-T Stack project",
|
||||
)
|
||||
.positional("project-directory", {
|
||||
describe: "Project name/directory",
|
||||
type: "string",
|
||||
})
|
||||
.option("yes", {
|
||||
alias: "y",
|
||||
type: "boolean",
|
||||
describe: "Use default configuration and skip prompts",
|
||||
default: false,
|
||||
})
|
||||
.option("database", {
|
||||
type: "string",
|
||||
describe: "Database type",
|
||||
choices: ["none", "sqlite", "postgres", "mysql", "mongodb"],
|
||||
})
|
||||
.option("orm", {
|
||||
type: "string",
|
||||
describe: "ORM type",
|
||||
choices: ["drizzle", "prisma", "mongoose", "none"],
|
||||
})
|
||||
.option("auth", {
|
||||
type: "boolean",
|
||||
describe: "Include authentication (use --no-auth to exclude)",
|
||||
})
|
||||
.option("frontend", {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Frontend types",
|
||||
choices: [
|
||||
"tanstack-router",
|
||||
"react-router",
|
||||
"tanstack-start",
|
||||
"next",
|
||||
"nuxt",
|
||||
"native-nativewind",
|
||||
"native-unistyles",
|
||||
"svelte",
|
||||
"solid",
|
||||
"none",
|
||||
],
|
||||
})
|
||||
.option("addons", {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Additional addons",
|
||||
choices: [
|
||||
"pwa",
|
||||
"tauri",
|
||||
"starlight",
|
||||
"biome",
|
||||
"husky",
|
||||
"turborepo",
|
||||
"none",
|
||||
],
|
||||
})
|
||||
.option("examples", {
|
||||
type: "array",
|
||||
string: true,
|
||||
describe: "Examples to include",
|
||||
choices: ["todo", "ai", "none"],
|
||||
})
|
||||
.option("git", {
|
||||
type: "boolean",
|
||||
describe: "Initialize git repository (use --no-git to skip)",
|
||||
})
|
||||
.option("package-manager", {
|
||||
alias: "pm",
|
||||
type: "string",
|
||||
describe: "Package manager",
|
||||
choices: ["npm", "pnpm", "bun"],
|
||||
})
|
||||
.option("install", {
|
||||
type: "boolean",
|
||||
describe: "Install dependencies (use --no-install to skip)",
|
||||
})
|
||||
.option("db-setup", {
|
||||
type: "string",
|
||||
describe: "Database setup",
|
||||
choices: [
|
||||
"turso",
|
||||
"neon",
|
||||
"prisma-postgres",
|
||||
"mongodb-atlas",
|
||||
"supabase",
|
||||
"none",
|
||||
],
|
||||
})
|
||||
.option("backend", {
|
||||
type: "string",
|
||||
describe: "Backend framework",
|
||||
choices: [
|
||||
"hono",
|
||||
"express",
|
||||
"fastify",
|
||||
"next",
|
||||
"elysia",
|
||||
"convex",
|
||||
"none",
|
||||
],
|
||||
})
|
||||
.option("runtime", {
|
||||
type: "string",
|
||||
describe: "Runtime",
|
||||
choices: ["bun", "node", "none"],
|
||||
})
|
||||
.option("api", {
|
||||
type: "string",
|
||||
describe: "API type",
|
||||
choices: ["trpc", "orpc", "none"],
|
||||
})
|
||||
.completion()
|
||||
.recommendCommands()
|
||||
.version(getLatestCLIVersion())
|
||||
.alias("version", "v")
|
||||
.help()
|
||||
.alias("help", "h")
|
||||
.strict()
|
||||
.wrap(null)
|
||||
.parse();
|
||||
|
||||
return argv as YargsArgv;
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export const dependencyVersionMap = {
|
||||
fastify: "^5.3.3",
|
||||
"@fastify/cors": "^11.0.1",
|
||||
|
||||
turbo: "^2.4.2",
|
||||
turbo: "^2.5.4",
|
||||
|
||||
ai: "^4.3.16",
|
||||
"@ai-sdk/google": "^1.2.3",
|
||||
|
||||
@@ -11,41 +11,155 @@ import {
|
||||
import { consola } from "consola";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import { parseCliArguments } from "./cli";
|
||||
import { createCli, trpcServer, zod as z } from "trpc-cli";
|
||||
import { DEFAULT_CONFIG } from "./constants";
|
||||
import { createProject } from "./helpers/project-generation/create-project";
|
||||
import { gatherConfig } from "./prompts/config-prompts";
|
||||
import { getProjectName } from "./prompts/project-name";
|
||||
import type { ProjectConfig } from "./types";
|
||||
import type { CreateInput, ProjectConfig } from "./types";
|
||||
import {
|
||||
APISchema,
|
||||
AddonsSchema,
|
||||
BackendSchema,
|
||||
DatabaseSchema,
|
||||
DatabaseSetupSchema,
|
||||
ExamplesSchema,
|
||||
FrontendSchema,
|
||||
ORMSchema,
|
||||
PackageManagerSchema,
|
||||
RuntimeSchema,
|
||||
} from "./types";
|
||||
import { trackProjectCreation } from "./utils/analytics";
|
||||
import { displayConfig } from "./utils/display-config";
|
||||
import { generateReproducibleCommand } from "./utils/generate-reproducible-command";
|
||||
import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
|
||||
import { renderTitle } from "./utils/render-title";
|
||||
import { processAndValidateFlags } from "./validation";
|
||||
import { getProvidedFlags, processAndValidateFlags } from "./validation";
|
||||
|
||||
const exit = () => process.exit(0);
|
||||
process.on("SIGINT", exit);
|
||||
process.on("SIGTERM", exit);
|
||||
|
||||
async function main() {
|
||||
const t = trpcServer.initTRPC.create();
|
||||
|
||||
async function handleDirectoryConflict(currentPathInput: string): Promise<{
|
||||
finalPathInput: string;
|
||||
shouldClearDirectory: boolean;
|
||||
}> {
|
||||
while (true) {
|
||||
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
|
||||
const dirExists = fs.pathExistsSync(resolvedPath);
|
||||
const dirIsNotEmpty = dirExists && fs.readdirSync(resolvedPath).length > 0;
|
||||
|
||||
if (!dirIsNotEmpty) {
|
||||
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
|
||||
}
|
||||
|
||||
log.warn(
|
||||
`Directory "${pc.yellow(
|
||||
currentPathInput,
|
||||
)}" already exists and is not empty.`,
|
||||
);
|
||||
|
||||
const action = await select<"overwrite" | "merge" | "rename" | "cancel">({
|
||||
message: "What would you like to do?",
|
||||
options: [
|
||||
{
|
||||
value: "overwrite",
|
||||
label: "Overwrite",
|
||||
hint: "Empty the directory and create the project",
|
||||
},
|
||||
{
|
||||
value: "merge",
|
||||
label: "Merge",
|
||||
hint: "Create project files inside, potentially overwriting conflicts",
|
||||
},
|
||||
{
|
||||
value: "rename",
|
||||
label: "Choose a different name/path",
|
||||
hint: "Keep the existing directory and create a new one",
|
||||
},
|
||||
{ value: "cancel", label: "Cancel", hint: "Abort the process" },
|
||||
],
|
||||
initialValue: "rename",
|
||||
});
|
||||
|
||||
if (isCancel(action)) {
|
||||
cancel(pc.red("Operation cancelled."));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "overwrite":
|
||||
return { finalPathInput: currentPathInput, shouldClearDirectory: true };
|
||||
case "merge":
|
||||
log.info(
|
||||
`Proceeding into existing directory "${pc.yellow(
|
||||
currentPathInput,
|
||||
)}". Files may be overwritten.`,
|
||||
);
|
||||
return {
|
||||
finalPathInput: currentPathInput,
|
||||
shouldClearDirectory: false,
|
||||
};
|
||||
case "rename": {
|
||||
log.info("Please choose a different project name or path.");
|
||||
const newPathInput = await getProjectName(undefined);
|
||||
return await handleDirectoryConflict(newPathInput);
|
||||
}
|
||||
case "cancel":
|
||||
cancel(pc.red("Operation cancelled."));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setupProjectDirectory(
|
||||
finalPathInput: string,
|
||||
shouldClearDirectory: boolean,
|
||||
): Promise<{ finalResolvedPath: string; finalBaseName: string }> {
|
||||
let finalResolvedPath: string;
|
||||
let finalBaseName: string;
|
||||
|
||||
if (finalPathInput === ".") {
|
||||
finalResolvedPath = process.cwd();
|
||||
finalBaseName = path.basename(finalResolvedPath);
|
||||
} else {
|
||||
finalResolvedPath = path.resolve(process.cwd(), finalPathInput);
|
||||
finalBaseName = path.basename(finalResolvedPath);
|
||||
}
|
||||
|
||||
if (shouldClearDirectory) {
|
||||
const s = spinner();
|
||||
s.start(`Clearing directory "${finalResolvedPath}"...`);
|
||||
try {
|
||||
await fs.emptyDir(finalResolvedPath);
|
||||
s.stop(`Directory "${finalResolvedPath}" cleared.`);
|
||||
} catch (error) {
|
||||
s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
|
||||
consola.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
await fs.ensureDir(finalResolvedPath);
|
||||
}
|
||||
|
||||
return { finalResolvedPath, finalBaseName };
|
||||
}
|
||||
|
||||
async function createProjectHandler(
|
||||
input: CreateInput & { projectName?: string },
|
||||
) {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const options = await parseCliArguments();
|
||||
const cliProjectNameArg = options.projectDirectory;
|
||||
|
||||
renderTitle();
|
||||
intro(pc.magenta("Creating a new Better-T-Stack project"));
|
||||
intro(pc.magenta("Creating a new Better-T Stack project"));
|
||||
|
||||
let currentPathInput: string;
|
||||
let finalPathInput: string;
|
||||
let finalResolvedPath: string;
|
||||
let finalBaseName: string;
|
||||
let shouldClearDirectory = false;
|
||||
|
||||
if (options.yes && cliProjectNameArg) {
|
||||
currentPathInput = cliProjectNameArg;
|
||||
} else if (options.yes) {
|
||||
if (input.yes && input.projectName) {
|
||||
currentPathInput = input.projectName;
|
||||
} else if (input.yes) {
|
||||
let defaultName = DEFAULT_CONFIG.relativePath;
|
||||
let counter = 1;
|
||||
while (
|
||||
@@ -57,113 +171,38 @@ async function main() {
|
||||
}
|
||||
currentPathInput = defaultName;
|
||||
} else {
|
||||
currentPathInput = await getProjectName(cliProjectNameArg);
|
||||
currentPathInput = await getProjectName(input.projectName);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
|
||||
const dirExists = fs.pathExistsSync(resolvedPath);
|
||||
const dirIsNotEmpty =
|
||||
dirExists && fs.readdirSync(resolvedPath).length > 0;
|
||||
const { finalPathInput, shouldClearDirectory } =
|
||||
await handleDirectoryConflict(currentPathInput);
|
||||
|
||||
if (!dirIsNotEmpty) {
|
||||
finalPathInput = currentPathInput;
|
||||
shouldClearDirectory = false;
|
||||
break;
|
||||
}
|
||||
const { finalResolvedPath, finalBaseName } = await setupProjectDirectory(
|
||||
finalPathInput,
|
||||
shouldClearDirectory,
|
||||
);
|
||||
|
||||
log.warn(
|
||||
`Directory "${pc.yellow(
|
||||
currentPathInput,
|
||||
)}" already exists and is not empty.`,
|
||||
);
|
||||
const cliInput = {
|
||||
...input,
|
||||
projectDirectory: input.projectName,
|
||||
};
|
||||
|
||||
const action = await select<"overwrite" | "merge" | "rename" | "cancel">({
|
||||
message: "What would you like to do?",
|
||||
options: [
|
||||
{
|
||||
value: "overwrite",
|
||||
label: "Overwrite",
|
||||
hint: "Empty the directory and create the project",
|
||||
},
|
||||
{
|
||||
value: "merge",
|
||||
label: "Merge",
|
||||
hint: "Create project files inside, potentially overwriting conflicts",
|
||||
},
|
||||
{
|
||||
value: "rename",
|
||||
label: "Choose a different name/path",
|
||||
hint: "Keep the existing directory and create a new one",
|
||||
},
|
||||
{ value: "cancel", label: "Cancel", hint: "Abort the process" },
|
||||
],
|
||||
initialValue: "rename",
|
||||
});
|
||||
|
||||
if (isCancel(action)) {
|
||||
cancel(pc.red("Operation cancelled."));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (action === "overwrite") {
|
||||
finalPathInput = currentPathInput;
|
||||
shouldClearDirectory = true;
|
||||
break;
|
||||
}
|
||||
if (action === "merge") {
|
||||
finalPathInput = currentPathInput;
|
||||
shouldClearDirectory = false;
|
||||
log.info(
|
||||
`Proceeding into existing directory "${pc.yellow(
|
||||
currentPathInput,
|
||||
)}". Files may be overwritten.`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (action === "rename") {
|
||||
log.info("Please choose a different project name or path.");
|
||||
currentPathInput = await getProjectName(undefined);
|
||||
} else if (action === "cancel") {
|
||||
cancel(pc.red("Operation cancelled."));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (finalPathInput === ".") {
|
||||
finalResolvedPath = process.cwd();
|
||||
finalBaseName = path.basename(finalResolvedPath);
|
||||
} else {
|
||||
finalResolvedPath = path.resolve(process.cwd(), finalPathInput);
|
||||
finalBaseName = path.basename(finalResolvedPath);
|
||||
}
|
||||
|
||||
if (shouldClearDirectory) {
|
||||
const s = spinner();
|
||||
s.start(`Clearing directory "${finalResolvedPath}"...`);
|
||||
try {
|
||||
await fs.emptyDir(finalResolvedPath);
|
||||
s.stop(`Directory "${finalResolvedPath}" cleared.`);
|
||||
} catch (error) {
|
||||
s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
|
||||
consola.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
await fs.ensureDir(finalResolvedPath);
|
||||
}
|
||||
|
||||
const flagConfig = processAndValidateFlags(options, finalBaseName);
|
||||
const providedFlags = getProvidedFlags(cliInput);
|
||||
const flagConfig = processAndValidateFlags(
|
||||
cliInput,
|
||||
providedFlags,
|
||||
finalBaseName,
|
||||
);
|
||||
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
|
||||
|
||||
if (!options.yes && Object.keys(otherFlags).length > 0) {
|
||||
if (!input.yes && Object.keys(otherFlags).length > 0) {
|
||||
log.info(pc.yellow("Using these pre-selected options:"));
|
||||
log.message(displayConfig(otherFlags));
|
||||
log.message("");
|
||||
}
|
||||
|
||||
let config: ProjectConfig;
|
||||
if (options.yes) {
|
||||
if (input.yes) {
|
||||
config = {
|
||||
...DEFAULT_CONFIG,
|
||||
...flagConfig,
|
||||
@@ -199,7 +238,6 @@ async function main() {
|
||||
await createProject(config);
|
||||
|
||||
const reproducibleCommand = generateReproducibleCommand(config);
|
||||
|
||||
log.success(
|
||||
pc.blue(
|
||||
`You can reproduce this setup with the following command:\n${reproducibleCommand}`,
|
||||
@@ -217,41 +255,66 @@ async function main() {
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.name === "YError") {
|
||||
cancel(pc.red(`Invalid arguments: ${error.message}`));
|
||||
} else {
|
||||
consola.error(`An unexpected error occurred: ${error.message}`);
|
||||
if (!error.message.includes("is only supported with")) {
|
||||
consola.error(error.stack);
|
||||
}
|
||||
}
|
||||
process.exit(1);
|
||||
} else {
|
||||
consola.error("An unexpected error occurred.");
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
consola.error("Aborting installation due to unexpected error...");
|
||||
if (err instanceof Error) {
|
||||
if (
|
||||
!err.message.includes("is only supported with") &&
|
||||
!err.message.includes("incompatible with") &&
|
||||
!err.message.includes("requires") &&
|
||||
!err.message.includes("Cannot use") &&
|
||||
!err.message.includes("Cannot select multiple") &&
|
||||
!err.message.includes("Cannot combine") &&
|
||||
!err.message.includes("not supported")
|
||||
) {
|
||||
consola.error(err.message);
|
||||
consola.error(err.stack);
|
||||
}
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
process.exit(1);
|
||||
const router = t.router({
|
||||
init: t.procedure
|
||||
.meta({
|
||||
description: "Create a new Better-T Stack project",
|
||||
default: true,
|
||||
})
|
||||
.input(
|
||||
z.tuple([
|
||||
z.string().optional().describe("project-name"),
|
||||
z
|
||||
.object({
|
||||
yes: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe("Use default configuration and skip prompts"),
|
||||
database: DatabaseSchema.optional(),
|
||||
orm: ORMSchema.optional(),
|
||||
auth: z.boolean().optional().describe("Include authentication"),
|
||||
frontend: z
|
||||
.array(FrontendSchema)
|
||||
.optional()
|
||||
.describe("Frontend frameworks"),
|
||||
addons: z
|
||||
.array(AddonsSchema)
|
||||
.optional()
|
||||
.describe("Additional addons"),
|
||||
examples: z
|
||||
.array(ExamplesSchema)
|
||||
.optional()
|
||||
.describe("Examples to include"),
|
||||
git: z.boolean().optional().describe("Initialize git repository"),
|
||||
packageManager: PackageManagerSchema.optional(),
|
||||
install: z.boolean().optional().describe("Install dependencies"),
|
||||
dbSetup: DatabaseSetupSchema.optional(),
|
||||
backend: BackendSchema.optional(),
|
||||
runtime: RuntimeSchema.optional(),
|
||||
api: APISchema.optional(),
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
]),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const [projectName, options] = input;
|
||||
const combinedInput = {
|
||||
projectName: projectName || undefined,
|
||||
...options,
|
||||
};
|
||||
return await createProjectHandler(combinedInput);
|
||||
}),
|
||||
});
|
||||
|
||||
createCli({
|
||||
router,
|
||||
name: "create-better-t-stack",
|
||||
version: getLatestCLIVersion(),
|
||||
}).run();
|
||||
|
||||
@@ -1,66 +1,73 @@
|
||||
export type Database = "sqlite" | "postgres" | "mongodb" | "mysql" | "none";
|
||||
export type ORM = "drizzle" | "prisma" | "mongoose" | "none";
|
||||
export type PackageManager = "npm" | "pnpm" | "bun";
|
||||
export type Addons =
|
||||
| "pwa"
|
||||
| "biome"
|
||||
| "tauri"
|
||||
| "husky"
|
||||
| "starlight"
|
||||
| "turborepo"
|
||||
| "none";
|
||||
export type Backend =
|
||||
| "hono"
|
||||
| "express"
|
||||
| "fastify"
|
||||
| "next"
|
||||
| "elysia"
|
||||
| "convex"
|
||||
| "none";
|
||||
export type Runtime = "node" | "bun" | "none";
|
||||
export type Examples = "todo" | "ai" | "none";
|
||||
export type Frontend =
|
||||
| "react-router"
|
||||
| "tanstack-router"
|
||||
| "tanstack-start"
|
||||
| "next"
|
||||
| "nuxt"
|
||||
| "native-nativewind"
|
||||
| "native-unistyles"
|
||||
| "svelte"
|
||||
| "solid"
|
||||
| "none";
|
||||
export type DatabaseSetup =
|
||||
| "turso"
|
||||
| "prisma-postgres"
|
||||
| "mongodb-atlas"
|
||||
| "neon"
|
||||
| "supabase"
|
||||
| "none";
|
||||
export type API = "trpc" | "orpc" | "none";
|
||||
import { z } from "zod";
|
||||
|
||||
export interface ProjectConfig {
|
||||
projectName: string;
|
||||
projectDir: string;
|
||||
relativePath: string;
|
||||
backend: Backend;
|
||||
runtime: Runtime;
|
||||
database: Database;
|
||||
orm: ORM;
|
||||
auth: boolean;
|
||||
addons: Addons[];
|
||||
examples: Examples[];
|
||||
git: boolean;
|
||||
packageManager: PackageManager;
|
||||
install: boolean;
|
||||
dbSetup: DatabaseSetup;
|
||||
frontend: Frontend[];
|
||||
api: API;
|
||||
}
|
||||
export const DatabaseSchema = z
|
||||
.enum(["none", "sqlite", "postgres", "mysql", "mongodb"])
|
||||
.describe("Database type");
|
||||
export type Database = z.infer<typeof DatabaseSchema>;
|
||||
|
||||
export type YargsArgv = {
|
||||
projectDirectory?: string;
|
||||
export const ORMSchema = z
|
||||
.enum(["drizzle", "prisma", "mongoose", "none"])
|
||||
.describe("ORM type");
|
||||
export type ORM = z.infer<typeof ORMSchema>;
|
||||
|
||||
export const BackendSchema = z
|
||||
.enum(["hono", "express", "fastify", "next", "elysia", "convex", "none"])
|
||||
.describe("Backend framework");
|
||||
export type Backend = z.infer<typeof BackendSchema>;
|
||||
|
||||
export const RuntimeSchema = z
|
||||
.enum(["bun", "node", "none"])
|
||||
.describe("Runtime environment");
|
||||
export type Runtime = z.infer<typeof RuntimeSchema>;
|
||||
|
||||
export const FrontendSchema = z
|
||||
.enum([
|
||||
"tanstack-router",
|
||||
"react-router",
|
||||
"tanstack-start",
|
||||
"next",
|
||||
"nuxt",
|
||||
"native-nativewind",
|
||||
"native-unistyles",
|
||||
"svelte",
|
||||
"solid",
|
||||
"none",
|
||||
])
|
||||
.describe("Frontend framework");
|
||||
export type Frontend = z.infer<typeof FrontendSchema>;
|
||||
|
||||
export const AddonsSchema = z
|
||||
.enum(["pwa", "tauri", "starlight", "biome", "husky", "turborepo", "none"])
|
||||
.describe("Additional addons");
|
||||
export type Addons = z.infer<typeof AddonsSchema>;
|
||||
|
||||
export const ExamplesSchema = z
|
||||
.enum(["todo", "ai", "none"])
|
||||
.describe("Example templates to include");
|
||||
export type Examples = z.infer<typeof ExamplesSchema>;
|
||||
|
||||
export const PackageManagerSchema = z
|
||||
.enum(["npm", "pnpm", "bun"])
|
||||
.describe("Package manager");
|
||||
export type PackageManager = z.infer<typeof PackageManagerSchema>;
|
||||
|
||||
export const DatabaseSetupSchema = z
|
||||
.enum([
|
||||
"turso",
|
||||
"neon",
|
||||
"prisma-postgres",
|
||||
"mongodb-atlas",
|
||||
"supabase",
|
||||
"none",
|
||||
])
|
||||
.describe("Database hosting setup");
|
||||
export type DatabaseSetup = z.infer<typeof DatabaseSetupSchema>;
|
||||
|
||||
export const APISchema = z.enum(["trpc", "orpc", "none"]).describe("API type");
|
||||
export type API = z.infer<typeof APISchema>;
|
||||
|
||||
export type CreateInput = {
|
||||
projectName?: string;
|
||||
yes?: boolean;
|
||||
database?: Database;
|
||||
orm?: ORM;
|
||||
@@ -75,7 +82,29 @@ export type YargsArgv = {
|
||||
backend?: Backend;
|
||||
runtime?: Runtime;
|
||||
api?: API;
|
||||
|
||||
_: (string | number)[];
|
||||
$0: string;
|
||||
};
|
||||
|
||||
export type CLIInput = CreateInput & {
|
||||
projectDirectory?: string;
|
||||
};
|
||||
|
||||
export interface ProjectConfig {
|
||||
projectName: string;
|
||||
projectDir: string;
|
||||
relativePath: string;
|
||||
database: Database;
|
||||
orm: ORM;
|
||||
backend: Backend;
|
||||
runtime: Runtime;
|
||||
frontend: Frontend[];
|
||||
addons: Addons[];
|
||||
examples: Examples[];
|
||||
auth: boolean;
|
||||
git: boolean;
|
||||
packageManager: PackageManager;
|
||||
install: boolean;
|
||||
dbSetup: DatabaseSetup;
|
||||
api: API;
|
||||
}
|
||||
|
||||
export type AvailablePackageManagers = "npm" | "pnpm" | "bun";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import path from "node:path";
|
||||
import { log } from "@clack/prompts";
|
||||
import { consola } from "consola";
|
||||
import type {
|
||||
API,
|
||||
Addons,
|
||||
Backend,
|
||||
CLIInput,
|
||||
Database,
|
||||
DatabaseSetup,
|
||||
Examples,
|
||||
@@ -13,17 +13,14 @@ import type {
|
||||
PackageManager,
|
||||
ProjectConfig,
|
||||
Runtime,
|
||||
YargsArgv,
|
||||
} from "./types";
|
||||
|
||||
export function processAndValidateFlags(
|
||||
options: YargsArgv,
|
||||
options: CLIInput,
|
||||
providedFlags: Set<string>,
|
||||
projectName?: string,
|
||||
): Partial<ProjectConfig> {
|
||||
const config: Partial<ProjectConfig> = {};
|
||||
const providedFlags: Set<string> = new Set(
|
||||
Object.keys(options).filter((key) => key !== "_" && key !== "$0"),
|
||||
);
|
||||
|
||||
if (options.api) {
|
||||
config.api = options.api as API;
|
||||
@@ -223,6 +220,12 @@ export function processAndValidateFlags(
|
||||
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(
|
||||
@@ -240,148 +243,99 @@ export function processAndValidateFlags(
|
||||
config.runtime = "none";
|
||||
config.dbSetup = "none";
|
||||
config.examples = [];
|
||||
} else {
|
||||
if (config.database === "none") {
|
||||
if (providedFlags.has("orm") && options.orm !== "none") {
|
||||
consola.fatal(
|
||||
`'--orm ${options.orm}' is incompatible with '--database none'. Please use '--orm none' or choose a database.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (providedFlags.has("auth") && options.auth === true) {
|
||||
consola.fatal(
|
||||
`'--auth' requires a database. Cannot use '--auth' with '--database none'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") {
|
||||
consola.fatal(
|
||||
`'--db-setup ${options.dbSetup}' requires a database. Cannot use with '--database none'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
config.orm = "none";
|
||||
config.auth = false;
|
||||
config.dbSetup = "none";
|
||||
if (config.orm === "mongoose" && config.database !== "mongodb") {
|
||||
consola.fatal(
|
||||
"Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log.info(
|
||||
"Due to '--database none', '--orm' has been automatically set to 'none'.",
|
||||
);
|
||||
log.info(
|
||||
"Due to '--database none', '--auth' has been automatically set to 'false'.",
|
||||
);
|
||||
log.info(
|
||||
"Due to '--database none', '--db-setup' has been automatically set to 'none'.",
|
||||
);
|
||||
}
|
||||
if (
|
||||
config.database === "mongodb" &&
|
||||
config.orm &&
|
||||
config.orm !== "mongoose" &&
|
||||
config.orm !== "prisma"
|
||||
) {
|
||||
consola.fatal(
|
||||
"MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.orm === "mongoose") {
|
||||
if (!providedFlags.has("database")) {
|
||||
config.database = "mongodb";
|
||||
log.info(
|
||||
"Due to '--orm mongoose', '--database' has been automatically set to 'mongodb'.",
|
||||
);
|
||||
} else if (config.database !== "mongodb") {
|
||||
consola.fatal(
|
||||
`'--orm mongoose' requires '--database mongodb'. Cannot use '--orm mongoose' with '--database ${config.database}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
if (config.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.dbSetup) {
|
||||
if (config.dbSetup === "turso") {
|
||||
if (!providedFlags.has("database")) {
|
||||
config.database = "sqlite";
|
||||
log.info(
|
||||
"Due to '--db-setup turso', '--database' has been automatically set to 'sqlite'.",
|
||||
);
|
||||
} else if (config.database !== "sqlite") {
|
||||
consola.fatal(
|
||||
`'--db-setup turso' requires '--database sqlite'. Cannot use with '--database ${config.database}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!providedFlags.has("orm")) {
|
||||
config.orm = "drizzle";
|
||||
log.info(
|
||||
"Due to '--db-setup turso', '--orm' has been automatically set to 'drizzle'.",
|
||||
);
|
||||
} else if (config.orm !== "drizzle") {
|
||||
consola.fatal(
|
||||
`'--db-setup turso' requires '--orm drizzle'. Cannot use with '--orm ${config.orm}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (config.dbSetup === "prisma-postgres") {
|
||||
if (!providedFlags.has("database")) {
|
||||
config.database = "postgres";
|
||||
log.info(
|
||||
"Due to '--db-setup prisma-postgres', '--database' has been automatically set to 'postgres'.",
|
||||
);
|
||||
} else if (config.database !== "postgres") {
|
||||
consola.fatal(
|
||||
`'--db-setup prisma-postgres' requires '--database postgres'. Cannot use with '--database ${config.database}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!providedFlags.has("orm")) {
|
||||
config.orm = "prisma";
|
||||
log.info(
|
||||
"Due to '--db-setup prisma-postgres', '--orm' has been automatically set to 'prisma'.",
|
||||
);
|
||||
} else if (config.orm !== "prisma") {
|
||||
consola.fatal(
|
||||
`'--db-setup prisma-postgres' requires '--orm prisma'. Cannot use with '--orm ${config.orm}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (config.dbSetup === "supabase") {
|
||||
if (!providedFlags.has("database")) {
|
||||
config.database = "postgres";
|
||||
log.info(
|
||||
"Due to '--db-setup supabase', '--database' has been automatically set to 'postgres'.",
|
||||
);
|
||||
} else if (config.database !== "postgres") {
|
||||
consola.fatal(
|
||||
`'--db-setup supabase' requires '--database postgres'. Cannot use with '--database ${config.database}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (config.dbSetup === "neon") {
|
||||
if (!providedFlags.has("database")) {
|
||||
config.database = "postgres";
|
||||
log.info(
|
||||
"Due to '--db-setup neon', '--database' has been automatically set to 'postgres'.",
|
||||
);
|
||||
} else if (config.database !== "postgres") {
|
||||
consola.fatal(
|
||||
`'--db-setup neon' requires '--database postgres'. Cannot use with '--database ${config.database}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (config.dbSetup === "mongodb-atlas") {
|
||||
if (!providedFlags.has("database")) {
|
||||
config.database = "mongodb";
|
||||
log.info(
|
||||
"Due to '--db-setup mongodb-atlas', '--database' has been automatically set to 'mongodb'.",
|
||||
);
|
||||
} else if (config.database !== "mongodb") {
|
||||
consola.fatal(
|
||||
`'--db-setup mongodb-atlas' requires '--database mongodb'. Cannot use with '--database ${config.database}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (config.database && 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.database === "mongodb" && config.orm === "drizzle") {
|
||||
consola.fatal(
|
||||
`'--database mongodb' is incompatible with '--orm drizzle'. Use '--orm mongoose' or '--orm prisma' with MongoDB.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (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") {
|
||||
consola.fatal(
|
||||
"Authentication requires a database. Please choose a database or set '--no-auth'.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
config.dbSetup &&
|
||||
config.dbSetup !== "none" &&
|
||||
config.database === "none"
|
||||
) {
|
||||
consola.fatal(
|
||||
"Database setup requires a database. Please choose a database or set '--db-setup none'.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (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") {
|
||||
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") {
|
||||
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") {
|
||||
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") {
|
||||
consola.fatal(
|
||||
"Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return config;
|
||||
@@ -500,3 +454,11 @@ export function validateConfigCompatibility(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getProvidedFlags(options: CLIInput): Set<string> {
|
||||
return new Set(
|
||||
Object.keys(options).filter(
|
||||
(key) => options[key as keyof CLIInput] !== undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user