mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): add programmatic api (#494)
This commit is contained in:
3
apps/cli/src/cli.ts
Normal file
3
apps/cli/src/cli.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createBtsCli } from "./index";
|
||||
|
||||
createBtsCli().run();
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { intro, log, outro } from "@clack/prompts";
|
||||
import consola from "consola";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../../constants";
|
||||
@@ -7,7 +8,13 @@ import { getAddonsToAdd } from "../../prompts/addons";
|
||||
import { gatherConfig } from "../../prompts/config-prompts";
|
||||
import { getProjectName } from "../../prompts/project-name";
|
||||
import { getDeploymentToAdd } from "../../prompts/web-deploy";
|
||||
import type { AddInput, CreateInput, ProjectConfig } from "../../types";
|
||||
import type {
|
||||
AddInput,
|
||||
CreateInput,
|
||||
DirectoryConflict,
|
||||
InitResult,
|
||||
ProjectConfig,
|
||||
} from "../../types";
|
||||
import { trackProjectCreation } from "../../utils/analytics";
|
||||
import { displayConfig } from "../../utils/display-config";
|
||||
import { exitWithError, handleError } from "../../utils/errors";
|
||||
@@ -26,113 +33,221 @@ import { installDependencies } from "./install-dependencies";
|
||||
|
||||
export async function createProjectHandler(
|
||||
input: CreateInput & { projectName?: string },
|
||||
) {
|
||||
): Promise<InitResult> {
|
||||
const startTime = Date.now();
|
||||
const timeScaffolded = new Date().toISOString();
|
||||
|
||||
if (input.renderTitle !== false) {
|
||||
renderTitle();
|
||||
}
|
||||
intro(pc.magenta("Creating a new Better-T Stack project"));
|
||||
|
||||
if (input.yolo) {
|
||||
consola.fatal("YOLO mode enabled - skipping checks. Things may break!");
|
||||
}
|
||||
|
||||
let currentPathInput: string;
|
||||
if (input.yes && input.projectName) {
|
||||
currentPathInput = input.projectName;
|
||||
} else if (input.yes) {
|
||||
let defaultName = DEFAULT_CONFIG.relativePath;
|
||||
let counter = 1;
|
||||
while (
|
||||
fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) &&
|
||||
fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0
|
||||
) {
|
||||
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
||||
counter++;
|
||||
}
|
||||
currentPathInput = defaultName;
|
||||
} else {
|
||||
currentPathInput = await getProjectName(input.projectName);
|
||||
}
|
||||
|
||||
let finalPathInput: string;
|
||||
let shouldClearDirectory: boolean;
|
||||
|
||||
try {
|
||||
renderTitle();
|
||||
intro(pc.magenta("Creating a new Better-T Stack project"));
|
||||
|
||||
let currentPathInput: string;
|
||||
if (input.yes && input.projectName) {
|
||||
currentPathInput = input.projectName;
|
||||
} else if (input.yes) {
|
||||
let defaultName = DEFAULT_CONFIG.relativePath;
|
||||
let counter = 1;
|
||||
while (
|
||||
fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) &&
|
||||
fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0
|
||||
) {
|
||||
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
||||
counter++;
|
||||
}
|
||||
currentPathInput = defaultName;
|
||||
if (input.directoryConflict) {
|
||||
const result = await handleDirectoryConflictProgrammatically(
|
||||
currentPathInput,
|
||||
input.directoryConflict,
|
||||
);
|
||||
finalPathInput = result.finalPathInput;
|
||||
shouldClearDirectory = result.shouldClearDirectory;
|
||||
} else {
|
||||
currentPathInput = await getProjectName(input.projectName);
|
||||
const result = await handleDirectoryConflict(currentPathInput);
|
||||
finalPathInput = result.finalPathInput;
|
||||
shouldClearDirectory = result.shouldClearDirectory;
|
||||
}
|
||||
} catch (error) {
|
||||
const elapsedTimeMs = Date.now() - startTime;
|
||||
return {
|
||||
success: false,
|
||||
projectConfig: {
|
||||
projectName: "",
|
||||
projectDir: "",
|
||||
relativePath: "",
|
||||
database: "none",
|
||||
orm: "none",
|
||||
backend: "none",
|
||||
runtime: "none",
|
||||
frontend: [],
|
||||
addons: [],
|
||||
examples: [],
|
||||
auth: false,
|
||||
git: false,
|
||||
packageManager: "npm",
|
||||
install: false,
|
||||
dbSetup: "none",
|
||||
api: "none",
|
||||
webDeploy: "none",
|
||||
} satisfies ProjectConfig,
|
||||
reproducibleCommand: "",
|
||||
timeScaffolded,
|
||||
elapsedTimeMs,
|
||||
projectDirectory: "",
|
||||
relativePath: "",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
|
||||
const { finalPathInput, shouldClearDirectory } =
|
||||
await handleDirectoryConflict(currentPathInput);
|
||||
const { finalResolvedPath, finalBaseName } = await setupProjectDirectory(
|
||||
finalPathInput,
|
||||
shouldClearDirectory,
|
||||
);
|
||||
|
||||
const { finalResolvedPath, finalBaseName } = await setupProjectDirectory(
|
||||
finalPathInput,
|
||||
shouldClearDirectory,
|
||||
);
|
||||
const cliInput = {
|
||||
...input,
|
||||
projectDirectory: input.projectName,
|
||||
};
|
||||
|
||||
const cliInput = {
|
||||
...input,
|
||||
projectDirectory: input.projectName,
|
||||
const providedFlags = getProvidedFlags(cliInput);
|
||||
|
||||
const flagConfig = processAndValidateFlags(
|
||||
cliInput,
|
||||
providedFlags,
|
||||
finalBaseName,
|
||||
);
|
||||
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
|
||||
|
||||
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 (input.yes) {
|
||||
config = {
|
||||
...DEFAULT_CONFIG,
|
||||
...flagConfig,
|
||||
projectName: finalBaseName,
|
||||
projectDir: finalResolvedPath,
|
||||
relativePath: finalPathInput,
|
||||
};
|
||||
|
||||
const providedFlags = getProvidedFlags(cliInput);
|
||||
const flagConfig = processAndValidateFlags(
|
||||
cliInput,
|
||||
providedFlags,
|
||||
finalBaseName,
|
||||
);
|
||||
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
|
||||
|
||||
if (!input.yes && Object.keys(otherFlags).length > 0) {
|
||||
log.info(pc.yellow("Using these pre-selected options:"));
|
||||
log.message(displayConfig(otherFlags));
|
||||
log.message("");
|
||||
if (config.backend === "convex") {
|
||||
log.info(
|
||||
"Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo",
|
||||
);
|
||||
} else if (config.backend === "none") {
|
||||
log.info(
|
||||
"Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
|
||||
);
|
||||
}
|
||||
|
||||
let config: ProjectConfig;
|
||||
if (input.yes) {
|
||||
config = {
|
||||
...DEFAULT_CONFIG,
|
||||
...flagConfig,
|
||||
projectName: finalBaseName,
|
||||
projectDir: finalResolvedPath,
|
||||
relativePath: finalPathInput,
|
||||
};
|
||||
log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
|
||||
log.message(displayConfig(config));
|
||||
log.message("");
|
||||
} else {
|
||||
config = await gatherConfig(
|
||||
flagConfig,
|
||||
finalBaseName,
|
||||
finalResolvedPath,
|
||||
finalPathInput,
|
||||
);
|
||||
}
|
||||
|
||||
if (config.backend === "convex") {
|
||||
log.info(
|
||||
"Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo",
|
||||
);
|
||||
} else if (config.backend === "none") {
|
||||
log.info(
|
||||
"Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
|
||||
);
|
||||
await createProject(config);
|
||||
|
||||
const reproducibleCommand = generateReproducibleCommand(config);
|
||||
log.success(
|
||||
pc.blue(
|
||||
`You can reproduce this setup with the following command:\n${reproducibleCommand}`,
|
||||
),
|
||||
);
|
||||
|
||||
await trackProjectCreation(config);
|
||||
|
||||
const elapsedTimeMs = Date.now() - startTime;
|
||||
const elapsedTimeInSeconds = (elapsedTimeMs / 1000).toFixed(2);
|
||||
outro(
|
||||
pc.magenta(
|
||||
`Project created successfully in ${pc.bold(
|
||||
elapsedTimeInSeconds,
|
||||
)} seconds!`,
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
projectConfig: config,
|
||||
reproducibleCommand,
|
||||
timeScaffolded,
|
||||
elapsedTimeMs,
|
||||
projectDirectory: config.projectDir,
|
||||
relativePath: config.relativePath,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleDirectoryConflictProgrammatically(
|
||||
currentPathInput: string,
|
||||
strategy: DirectoryConflict,
|
||||
): Promise<{ finalPathInput: string; shouldClearDirectory: boolean }> {
|
||||
const currentPath = path.resolve(process.cwd(), currentPathInput);
|
||||
|
||||
if (!fs.pathExistsSync(currentPath)) {
|
||||
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
|
||||
}
|
||||
|
||||
const dirContents = fs.readdirSync(currentPath);
|
||||
const isNotEmpty = dirContents.length > 0;
|
||||
|
||||
if (!isNotEmpty) {
|
||||
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
|
||||
}
|
||||
|
||||
switch (strategy) {
|
||||
case "overwrite":
|
||||
return { finalPathInput: currentPathInput, shouldClearDirectory: true };
|
||||
|
||||
case "merge":
|
||||
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
|
||||
|
||||
case "increment": {
|
||||
let counter = 1;
|
||||
const baseName = currentPathInput;
|
||||
let finalPathInput = `${baseName}-${counter}`;
|
||||
|
||||
while (
|
||||
fs.pathExistsSync(path.resolve(process.cwd(), finalPathInput)) &&
|
||||
fs.readdirSync(path.resolve(process.cwd(), finalPathInput)).length > 0
|
||||
) {
|
||||
counter++;
|
||||
finalPathInput = `${baseName}-${counter}`;
|
||||
}
|
||||
|
||||
log.info(
|
||||
pc.yellow("Using default/flag options (config prompts skipped):"),
|
||||
);
|
||||
log.message(displayConfig(config));
|
||||
log.message("");
|
||||
} else {
|
||||
config = await gatherConfig(
|
||||
flagConfig,
|
||||
finalBaseName,
|
||||
finalResolvedPath,
|
||||
finalPathInput,
|
||||
);
|
||||
return { finalPathInput, shouldClearDirectory: false };
|
||||
}
|
||||
|
||||
await createProject(config);
|
||||
case "error":
|
||||
throw new Error(
|
||||
`Directory "${currentPathInput}" already exists and is not empty. Use directoryConflict: "overwrite", "merge", or "increment" to handle this.`,
|
||||
);
|
||||
|
||||
const reproducibleCommand = generateReproducibleCommand(config);
|
||||
log.success(
|
||||
pc.blue(
|
||||
`You can reproduce this setup with the following command:\n${reproducibleCommand}`,
|
||||
),
|
||||
);
|
||||
|
||||
await trackProjectCreation(config);
|
||||
|
||||
const elapsedTimeInSeconds = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||
outro(
|
||||
pc.magenta(
|
||||
`Project created successfully in ${pc.bold(
|
||||
elapsedTimeInSeconds,
|
||||
)} seconds!`,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to create project");
|
||||
default:
|
||||
throw new Error(`Unknown directory conflict strategy: ${strategy}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,17 +7,35 @@ import {
|
||||
createProjectHandler,
|
||||
} from "./helpers/project-generation/command-handlers";
|
||||
import {
|
||||
type AddInput,
|
||||
type Addons,
|
||||
AddonsSchema,
|
||||
type API,
|
||||
APISchema,
|
||||
type Backend,
|
||||
BackendSchema,
|
||||
type BetterTStackConfig,
|
||||
type CreateInput,
|
||||
type Database,
|
||||
DatabaseSchema,
|
||||
type DatabaseSetup,
|
||||
DatabaseSetupSchema,
|
||||
type DirectoryConflict,
|
||||
DirectoryConflictSchema,
|
||||
type Examples,
|
||||
ExamplesSchema,
|
||||
type Frontend,
|
||||
FrontendSchema,
|
||||
type InitResult,
|
||||
type ORM,
|
||||
ORMSchema,
|
||||
type PackageManager,
|
||||
PackageManagerSchema,
|
||||
type ProjectConfig,
|
||||
ProjectNameSchema,
|
||||
type Runtime,
|
||||
RuntimeSchema,
|
||||
type WebDeploy,
|
||||
WebDeploySchema,
|
||||
} from "./types";
|
||||
import { handleError } from "./utils/errors";
|
||||
@@ -28,7 +46,7 @@ import { displaySponsors, fetchSponsors } from "./utils/sponsors";
|
||||
|
||||
const t = trpcServer.initTRPC.create();
|
||||
|
||||
const router = t.router({
|
||||
export const router = t.router({
|
||||
init: t.procedure
|
||||
.meta({
|
||||
description: "Create a new Better-T Stack project",
|
||||
@@ -44,6 +62,18 @@ const router = t.router({
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe("Use default configuration"),
|
||||
yolo: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(
|
||||
"(WARNING - NOT RECOMMENDED) Bypass validations and compatibility checks",
|
||||
),
|
||||
verbose: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe("Show detailed result information"),
|
||||
database: DatabaseSchema.optional(),
|
||||
orm: ORMSchema.optional(),
|
||||
auth: z.boolean().optional(),
|
||||
@@ -58,6 +88,8 @@ const router = t.router({
|
||||
runtime: RuntimeSchema.optional(),
|
||||
api: APISchema.optional(),
|
||||
webDeploy: WebDeploySchema.optional(),
|
||||
directoryConflict: DirectoryConflictSchema.optional(),
|
||||
renderTitle: z.boolean().optional(),
|
||||
}),
|
||||
]),
|
||||
)
|
||||
@@ -67,7 +99,11 @@ const router = t.router({
|
||||
projectName,
|
||||
...options,
|
||||
};
|
||||
await createProjectHandler(combinedInput);
|
||||
const result = await createProjectHandler(combinedInput);
|
||||
|
||||
if (options.verbose) {
|
||||
return result;
|
||||
}
|
||||
}),
|
||||
add: t.procedure
|
||||
.meta({
|
||||
@@ -129,8 +165,90 @@ const router = t.router({
|
||||
}),
|
||||
});
|
||||
|
||||
createCli({
|
||||
router,
|
||||
name: "create-better-t-stack",
|
||||
version: getLatestCLIVersion(),
|
||||
}).run();
|
||||
const caller = t.createCallerFactory(router)({});
|
||||
|
||||
export function createBtsCli() {
|
||||
return createCli({
|
||||
router,
|
||||
name: "create-better-t-stack",
|
||||
version: getLatestCLIVersion(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new Better-T Stack project
|
||||
*
|
||||
* @example CLI usage:
|
||||
* ```bash
|
||||
* npx create-better-t-stack my-app --yes
|
||||
* ```
|
||||
*
|
||||
* @example Programmatic usage (always returns structured data):
|
||||
* ```typescript
|
||||
* import { init } from "create-better-t-stack";
|
||||
*
|
||||
* const result = await init("my-app", {
|
||||
* yes: true,
|
||||
* frontend: ["tanstack-router"],
|
||||
* backend: "hono",
|
||||
* database: "sqlite",
|
||||
* orm: "drizzle",
|
||||
* auth: true,
|
||||
* addons: ["biome", "turborepo"],
|
||||
* packageManager: "bun",
|
||||
* install: false,
|
||||
* directoryConflict: "increment", // auto-handle conflicts
|
||||
* });
|
||||
*
|
||||
* if (result.success) {
|
||||
* console.log(`Project created at: ${result.projectDirectory}`);
|
||||
* console.log(`Reproducible command: ${result.reproducibleCommand}`);
|
||||
* console.log(`Time taken: ${result.elapsedTimeMs}ms`);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export async function init(
|
||||
projectName?: string,
|
||||
options?: CreateInput,
|
||||
): Promise<InitResult> {
|
||||
const opts = (options ?? {}) as CreateInput;
|
||||
const programmaticOpts = { ...opts, verbose: true };
|
||||
const prev = process.env.BTS_PROGRAMMATIC;
|
||||
process.env.BTS_PROGRAMMATIC = "1";
|
||||
const result = await caller.init([projectName, programmaticOpts]);
|
||||
if (prev === undefined) delete process.env.BTS_PROGRAMMATIC;
|
||||
else process.env.BTS_PROGRAMMATIC = prev;
|
||||
return result as InitResult;
|
||||
}
|
||||
|
||||
export async function sponsors() {
|
||||
return caller.sponsors();
|
||||
}
|
||||
|
||||
export async function docs() {
|
||||
return caller.docs();
|
||||
}
|
||||
|
||||
export async function builder() {
|
||||
return caller.builder();
|
||||
}
|
||||
|
||||
export type {
|
||||
Database,
|
||||
ORM,
|
||||
Backend,
|
||||
Runtime,
|
||||
Frontend,
|
||||
Addons,
|
||||
Examples,
|
||||
PackageManager,
|
||||
DatabaseSetup,
|
||||
API,
|
||||
WebDeploy,
|
||||
DirectoryConflict,
|
||||
CreateInput,
|
||||
AddInput,
|
||||
ProjectConfig,
|
||||
BetterTStackConfig,
|
||||
InitResult,
|
||||
};
|
||||
|
||||
@@ -108,9 +108,16 @@ export const WebDeploySchema = z
|
||||
.describe("Web deployment");
|
||||
export type WebDeploy = z.infer<typeof WebDeploySchema>;
|
||||
|
||||
export const DirectoryConflictSchema = z
|
||||
.enum(["merge", "overwrite", "increment", "error"])
|
||||
.describe("How to handle existing directory conflicts");
|
||||
export type DirectoryConflict = z.infer<typeof DirectoryConflictSchema>;
|
||||
|
||||
export type CreateInput = {
|
||||
projectName?: string;
|
||||
yes?: boolean;
|
||||
yolo?: boolean;
|
||||
verbose?: boolean;
|
||||
database?: Database;
|
||||
orm?: ORM;
|
||||
auth?: boolean;
|
||||
@@ -125,6 +132,8 @@ export type CreateInput = {
|
||||
runtime?: Runtime;
|
||||
api?: API;
|
||||
webDeploy?: WebDeploy;
|
||||
directoryConflict?: DirectoryConflict;
|
||||
renderTitle?: boolean;
|
||||
};
|
||||
|
||||
export type AddInput = {
|
||||
@@ -175,3 +184,14 @@ export interface BetterTStackConfig {
|
||||
api: API;
|
||||
webDeploy: WebDeploy;
|
||||
}
|
||||
|
||||
export interface InitResult {
|
||||
success: boolean;
|
||||
projectConfig: ProjectConfig;
|
||||
reproducibleCommand: string;
|
||||
timeScaffolded: string;
|
||||
elapsedTimeMs: number;
|
||||
projectDirectory: string;
|
||||
relativePath: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,23 @@ import { cancel } from "@clack/prompts";
|
||||
import consola from "consola";
|
||||
import pc from "picocolors";
|
||||
|
||||
function isProgrammatic(): boolean {
|
||||
return process.env.BTS_PROGRAMMATIC === "1";
|
||||
}
|
||||
|
||||
export function exitWithError(message: string): never {
|
||||
consola.error(pc.red(message));
|
||||
if (isProgrammatic()) {
|
||||
throw new Error(message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export function exitCancelled(message = "Operation cancelled"): never {
|
||||
cancel(pc.red(message));
|
||||
if (isProgrammatic()) {
|
||||
throw new Error(message);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -16,5 +26,8 @@ export function handleError(error: unknown, fallbackMessage?: string): never {
|
||||
const message =
|
||||
error instanceof Error ? error.message : fallbackMessage || String(error);
|
||||
consola.error(pc.red(message));
|
||||
if (isProgrammatic()) {
|
||||
throw new Error(message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { exitCancelled, handleError } from "./errors";
|
||||
|
||||
export async function handleDirectoryConflict(
|
||||
currentPathInput: string,
|
||||
silent = false,
|
||||
): Promise<{
|
||||
finalPathInput: string;
|
||||
shouldClearDirectory: boolean;
|
||||
@@ -20,6 +21,12 @@ export async function handleDirectoryConflict(
|
||||
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
|
||||
}
|
||||
|
||||
if (silent) {
|
||||
throw new Error(
|
||||
`Directory "${currentPathInput}" already exists and is not empty. In silent mode, please provide a different project name or clear the directory manually.`,
|
||||
);
|
||||
}
|
||||
|
||||
log.warn(
|
||||
`Directory "${pc.yellow(
|
||||
currentPathInput,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import path from "node:path";
|
||||
import {
|
||||
type Addons,
|
||||
type API,
|
||||
type Backend,
|
||||
type CLIInput,
|
||||
type Database,
|
||||
type DatabaseSetup,
|
||||
type Examples,
|
||||
type Frontend,
|
||||
type ORM,
|
||||
type PackageManager,
|
||||
type ProjectConfig,
|
||||
@@ -28,6 +25,36 @@ import {
|
||||
} from "./utils/compatibility-rules";
|
||||
import { exitWithError } from "./utils/errors";
|
||||
|
||||
function processArrayOption<T>(options: (T | "none")[] | undefined): T[] {
|
||||
if (!options || options.length === 0) return [];
|
||||
if (options.includes("none" as T | "none")) return [];
|
||||
return options.filter((item): item is T => item !== "none");
|
||||
}
|
||||
|
||||
function deriveProjectName(
|
||||
projectName?: string,
|
||||
projectDirectory?: string,
|
||||
): string {
|
||||
if (projectName) {
|
||||
return projectName;
|
||||
}
|
||||
if (projectDirectory) {
|
||||
return path.basename(path.resolve(process.cwd(), projectDirectory));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function validateProjectName(name: string): void {
|
||||
const result = ProjectNameSchema.safeParse(name);
|
||||
if (!result.success) {
|
||||
exitWithError(
|
||||
`Invalid project name: ${
|
||||
result.error.issues[0]?.message || "Invalid project name"
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function processAndValidateFlags(
|
||||
options: CLIInput,
|
||||
providedFlags: Set<string>,
|
||||
@@ -96,29 +123,13 @@ export function processAndValidateFlags(
|
||||
config.webDeploy = options.webDeploy as WebDeploy;
|
||||
}
|
||||
|
||||
if (projectName) {
|
||||
const result = ProjectNameSchema.safeParse(path.basename(projectName));
|
||||
if (!result.success) {
|
||||
exitWithError(
|
||||
`Invalid project name: ${
|
||||
result.error.issues[0]?.message || "Invalid project name"
|
||||
}`,
|
||||
);
|
||||
}
|
||||
config.projectName = projectName;
|
||||
} else if (options.projectDirectory) {
|
||||
const baseName = path.basename(
|
||||
path.resolve(process.cwd(), options.projectDirectory),
|
||||
);
|
||||
const result = ProjectNameSchema.safeParse(baseName);
|
||||
if (!result.success) {
|
||||
exitWithError(
|
||||
`Invalid project name: ${
|
||||
result.error.issues[0]?.message || "Invalid project name"
|
||||
}`,
|
||||
);
|
||||
}
|
||||
config.projectName = baseName;
|
||||
const derivedName = deriveProjectName(projectName, options.projectDirectory);
|
||||
if (derivedName) {
|
||||
const nameToValidate = projectName
|
||||
? path.basename(projectName)
|
||||
: derivedName;
|
||||
validateProjectName(nameToValidate);
|
||||
config.projectName = projectName || derivedName;
|
||||
}
|
||||
|
||||
if (options.frontend && options.frontend.length > 0) {
|
||||
@@ -128,9 +139,7 @@ export function processAndValidateFlags(
|
||||
}
|
||||
config.frontend = [];
|
||||
} else {
|
||||
const validOptions = options.frontend.filter(
|
||||
(f): f is Frontend => f !== "none",
|
||||
);
|
||||
const validOptions = processArrayOption(options.frontend);
|
||||
ensureSingleWebAndNative(validOptions);
|
||||
config.frontend = validOptions;
|
||||
}
|
||||
@@ -152,9 +161,7 @@ export function processAndValidateFlags(
|
||||
}
|
||||
config.addons = [];
|
||||
} else {
|
||||
config.addons = options.addons.filter(
|
||||
(addon): addon is Addons => addon !== "none",
|
||||
);
|
||||
config.addons = processArrayOption(options.addons);
|
||||
}
|
||||
}
|
||||
if (options.examples && options.examples.length > 0) {
|
||||
@@ -164,9 +171,7 @@ export function processAndValidateFlags(
|
||||
}
|
||||
config.examples = [];
|
||||
} else {
|
||||
config.examples = options.examples.filter(
|
||||
(ex): ex is Examples => ex !== "none",
|
||||
);
|
||||
config.examples = processArrayOption(options.examples);
|
||||
if (options.examples.includes("none") && config.backend !== "convex") {
|
||||
config.examples = [];
|
||||
}
|
||||
@@ -421,6 +426,85 @@ export function validateConfigCompatibility(config: Partial<ProjectConfig>) {
|
||||
);
|
||||
}
|
||||
|
||||
export function processProvidedFlagsWithoutValidation(
|
||||
options: CLIInput,
|
||||
projectName?: string,
|
||||
): Partial<ProjectConfig> {
|
||||
const config: Partial<ProjectConfig> = {};
|
||||
|
||||
if (options.api) {
|
||||
config.api = options.api as API;
|
||||
}
|
||||
|
||||
if (options.backend) {
|
||||
config.backend = options.backend as Backend;
|
||||
}
|
||||
|
||||
if (options.database) {
|
||||
config.database = options.database as Database;
|
||||
}
|
||||
|
||||
if (options.orm) {
|
||||
config.orm = options.orm as ORM;
|
||||
}
|
||||
|
||||
if (options.auth !== undefined) {
|
||||
config.auth = options.auth;
|
||||
}
|
||||
|
||||
if (options.git !== undefined) {
|
||||
config.git = options.git;
|
||||
}
|
||||
|
||||
if (options.install !== undefined) {
|
||||
config.install = options.install;
|
||||
}
|
||||
|
||||
if (options.runtime) {
|
||||
config.runtime = options.runtime as Runtime;
|
||||
}
|
||||
|
||||
if (options.dbSetup) {
|
||||
config.dbSetup = options.dbSetup as DatabaseSetup;
|
||||
}
|
||||
|
||||
if (options.packageManager) {
|
||||
config.packageManager = options.packageManager as PackageManager;
|
||||
}
|
||||
|
||||
if (options.webDeploy) {
|
||||
config.webDeploy = options.webDeploy as WebDeploy;
|
||||
}
|
||||
|
||||
const derivedName = deriveProjectName(projectName, options.projectDirectory);
|
||||
if (derivedName) {
|
||||
const nameToValidate = projectName
|
||||
? path.basename(projectName)
|
||||
: derivedName;
|
||||
const result = ProjectNameSchema.safeParse(nameToValidate);
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
`Invalid project name: ${result.error.issues[0]?.message}`,
|
||||
);
|
||||
}
|
||||
config.projectName = projectName || derivedName;
|
||||
}
|
||||
|
||||
if (options.frontend && options.frontend.length > 0) {
|
||||
config.frontend = processArrayOption(options.frontend);
|
||||
}
|
||||
|
||||
if (options.addons && options.addons.length > 0) {
|
||||
config.addons = processArrayOption(options.addons);
|
||||
}
|
||||
|
||||
if (options.examples && options.examples.length > 0) {
|
||||
config.examples = processArrayOption(options.examples);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
export function getProvidedFlags(options: CLIInput): Set<string> {
|
||||
return new Set(
|
||||
Object.keys(options).filter(
|
||||
|
||||
Reference in New Issue
Block a user