mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): add alchemy and improve cli tooling and structure (#520)
This commit is contained in:
@@ -5,13 +5,19 @@ import { isTelemetryEnabled } from "./telemetry";
|
||||
const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY || "";
|
||||
const POSTHOG_HOST = process.env.POSTHOG_HOST;
|
||||
|
||||
function generateSessionId() {
|
||||
const rand = Math.random().toString(36).slice(2);
|
||||
const now = Date.now().toString(36);
|
||||
return `cli_${now}${rand}`;
|
||||
}
|
||||
|
||||
export async function trackProjectCreation(
|
||||
config: ProjectConfig,
|
||||
disableAnalytics = false,
|
||||
) {
|
||||
if (!isTelemetryEnabled() || disableAnalytics) return;
|
||||
|
||||
const sessionId = `cli_${crypto.randomUUID().replace(/-/g, "")}`;
|
||||
const sessionId = generateSessionId();
|
||||
// biome-ignore lint/correctness/noUnusedVariables: `projectName`, `projectDir`, and `relativePath` are not used in the event properties
|
||||
const { projectName, projectDir, relativePath, ...safeConfig } = config;
|
||||
|
||||
@@ -21,8 +27,8 @@ export async function trackProjectCreation(
|
||||
properties: {
|
||||
...safeConfig,
|
||||
cli_version: getLatestCLIVersion(),
|
||||
node_version: process.version,
|
||||
platform: process.platform,
|
||||
node_version: typeof process !== "undefined" ? process.version : "",
|
||||
platform: typeof process !== "undefined" ? process.platform : "",
|
||||
$ip: null,
|
||||
},
|
||||
distinct_id: sessionId,
|
||||
|
||||
@@ -22,6 +22,7 @@ export async function writeBtsConfig(projectConfig: ProjectConfig) {
|
||||
dbSetup: projectConfig.dbSetup,
|
||||
api: projectConfig.api,
|
||||
webDeploy: projectConfig.webDeploy,
|
||||
serverDeploy: projectConfig.serverDeploy,
|
||||
};
|
||||
|
||||
const baseContent = {
|
||||
@@ -40,6 +41,7 @@ export async function writeBtsConfig(projectConfig: ProjectConfig) {
|
||||
dbSetup: btsConfig.dbSetup,
|
||||
api: btsConfig.api,
|
||||
webDeploy: btsConfig.webDeploy,
|
||||
serverDeploy: btsConfig.serverDeploy,
|
||||
};
|
||||
|
||||
let configContent = JSON.stringify(baseContent);
|
||||
@@ -91,7 +93,9 @@ export async function readBtsConfig(
|
||||
|
||||
export async function updateBtsConfig(
|
||||
projectDir: string,
|
||||
updates: Partial<Pick<BetterTStackConfig, "addons" | "webDeploy">>,
|
||||
updates: Partial<
|
||||
Pick<BetterTStackConfig, "addons" | "webDeploy" | "serverDeploy">
|
||||
>,
|
||||
) {
|
||||
try {
|
||||
const configPath = path.join(projectDir, BTS_CONFIG_FILE);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type {
|
||||
Addons,
|
||||
API,
|
||||
Backend,
|
||||
CLIInput,
|
||||
Frontend,
|
||||
ProjectConfig,
|
||||
ServerDeploy,
|
||||
WebDeploy,
|
||||
} from "../types";
|
||||
import { validateAddonCompatibility } from "./addon-compatibility";
|
||||
@@ -252,6 +254,21 @@ export function validateWebDeployRequiresWebFrontend(
|
||||
}
|
||||
}
|
||||
|
||||
export function validateServerDeployRequiresBackend(
|
||||
serverDeploy: ServerDeploy | undefined,
|
||||
backend: Backend | undefined,
|
||||
) {
|
||||
if (
|
||||
serverDeploy &&
|
||||
serverDeploy !== "none" &&
|
||||
(!backend || backend === "none")
|
||||
) {
|
||||
exitWithError(
|
||||
"'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateAddonsAgainstFrontends(
|
||||
addons: Addons[] = [],
|
||||
frontends: Frontend[] = [],
|
||||
@@ -297,3 +314,31 @@ export function validateExamplesCompatibility(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateAlchemyCompatibility(
|
||||
webDeploy: WebDeploy | undefined,
|
||||
serverDeploy: ServerDeploy | undefined,
|
||||
frontends: Frontend[] = [],
|
||||
) {
|
||||
const isAlchemyWebDeploy = webDeploy === "alchemy";
|
||||
const isAlchemyServerDeploy = serverDeploy === "alchemy";
|
||||
|
||||
if (isAlchemyWebDeploy || isAlchemyServerDeploy) {
|
||||
const incompatibleFrontends = frontends.filter(
|
||||
(f) => f === "next" || f === "react-router",
|
||||
);
|
||||
|
||||
if (incompatibleFrontends.length > 0) {
|
||||
const deployType =
|
||||
isAlchemyWebDeploy && isAlchemyServerDeploy
|
||||
? "web and server deployment"
|
||||
: isAlchemyWebDeploy
|
||||
? "web deployment"
|
||||
: "server deployment";
|
||||
|
||||
exitWithError(
|
||||
`Alchemy ${deployType} is temporarily not compatible with ${incompatibleFrontends.join(" and ")} frontend(s). Please choose a different frontend or deployment option.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
134
apps/cli/src/utils/config-processing.ts
Normal file
134
apps/cli/src/utils/config-processing.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import path from "node:path";
|
||||
import type {
|
||||
API,
|
||||
Backend,
|
||||
CLIInput,
|
||||
Database,
|
||||
DatabaseSetup,
|
||||
ORM,
|
||||
PackageManager,
|
||||
ProjectConfig,
|
||||
Runtime,
|
||||
ServerDeploy,
|
||||
WebDeploy,
|
||||
} from "../types";
|
||||
|
||||
export 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");
|
||||
}
|
||||
|
||||
export function deriveProjectName(
|
||||
projectName?: string,
|
||||
projectDirectory?: string,
|
||||
): string {
|
||||
if (projectName) {
|
||||
return projectName;
|
||||
}
|
||||
if (projectDirectory) {
|
||||
return path.basename(path.resolve(process.cwd(), projectDirectory));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function processFlags(
|
||||
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;
|
||||
}
|
||||
|
||||
if (options.serverDeploy) {
|
||||
config.serverDeploy = options.serverDeploy as ServerDeploy;
|
||||
}
|
||||
|
||||
const derivedName = deriveProjectName(projectName, options.projectDirectory);
|
||||
if (derivedName) {
|
||||
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(
|
||||
(key) => options[key as keyof CLIInput] !== undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function validateNoneExclusivity<T>(
|
||||
options: (T | "none")[] | undefined,
|
||||
optionName: string,
|
||||
): void {
|
||||
if (!options || options.length === 0) return;
|
||||
|
||||
if (options.includes("none" as T | "none") && options.length > 1) {
|
||||
throw new Error(`Cannot combine 'none' with other ${optionName}.`);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateArrayOptions(options: CLIInput): void {
|
||||
validateNoneExclusivity(options.frontend, "frontend options");
|
||||
validateNoneExclusivity(options.addons, "addons");
|
||||
validateNoneExclusivity(options.examples, "examples");
|
||||
}
|
||||
333
apps/cli/src/utils/config-validation.ts
Normal file
333
apps/cli/src/utils/config-validation.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
import type {
|
||||
CLIInput,
|
||||
Database,
|
||||
DatabaseSetup,
|
||||
ProjectConfig,
|
||||
Runtime,
|
||||
} from "../types";
|
||||
import {
|
||||
coerceBackendPresets,
|
||||
ensureSingleWebAndNative,
|
||||
incompatibleFlagsForBackend,
|
||||
isWebFrontend,
|
||||
validateAddonsAgainstFrontends,
|
||||
validateApiFrontendCompatibility,
|
||||
validateExamplesCompatibility,
|
||||
validateServerDeployRequiresBackend,
|
||||
validateWebDeployRequiresWebFrontend,
|
||||
validateWorkersCompatibility,
|
||||
validateAlchemyCompatibility,
|
||||
} from "./compatibility-rules";
|
||||
import { exitWithError } from "./errors";
|
||||
|
||||
export function validateDatabaseOrmAuth(
|
||||
cfg: Partial<ProjectConfig>,
|
||||
flags?: Set<string>,
|
||||
): void {
|
||||
const db = cfg.database;
|
||||
const orm = cfg.orm;
|
||||
const has = (k: string) => (flags ? flags.has(k) : true);
|
||||
|
||||
if (has("orm") && has("database") && orm === "mongoose" && db !== "mongodb") {
|
||||
exitWithError(
|
||||
"Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.",
|
||||
);
|
||||
}
|
||||
|
||||
if (has("orm") && has("database") && orm === "drizzle" && db === "mongodb") {
|
||||
exitWithError(
|
||||
"Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
has("database") &&
|
||||
has("orm") &&
|
||||
db === "mongodb" &&
|
||||
orm &&
|
||||
orm !== "mongoose" &&
|
||||
orm !== "prisma" &&
|
||||
orm !== "none"
|
||||
) {
|
||||
exitWithError(
|
||||
"MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.",
|
||||
);
|
||||
}
|
||||
|
||||
if (has("database") && has("orm") && db && db !== "none" && orm === "none") {
|
||||
exitWithError(
|
||||
"Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (has("orm") && has("database") && orm && orm !== "none" && db === "none") {
|
||||
exitWithError(
|
||||
"ORM selection requires a database. Please choose a database or set '--orm none'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (has("auth") && has("database") && cfg.auth && db === "none") {
|
||||
exitWithError(
|
||||
"Authentication requires a database. Please choose a database or set '--no-auth'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (cfg.auth && db === "none") {
|
||||
exitWithError(
|
||||
"Authentication requires a database. Please choose a database or set '--no-auth'.",
|
||||
);
|
||||
}
|
||||
|
||||
if (orm && orm !== "none" && db === "none") {
|
||||
exitWithError(
|
||||
"ORM selection requires a database. Please choose a database or set '--orm none'.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateDatabaseSetup(
|
||||
config: Partial<ProjectConfig>,
|
||||
providedFlags: Set<string>,
|
||||
): void {
|
||||
const { dbSetup, database, runtime } = config;
|
||||
|
||||
if (
|
||||
providedFlags.has("dbSetup") &&
|
||||
providedFlags.has("database") &&
|
||||
dbSetup &&
|
||||
dbSetup !== "none" &&
|
||||
database === "none"
|
||||
) {
|
||||
exitWithError(
|
||||
"Database setup requires a database. Please choose a database or set '--db-setup none'.",
|
||||
);
|
||||
}
|
||||
|
||||
const setupValidations: Record<
|
||||
DatabaseSetup,
|
||||
{ database?: Database; runtime?: Runtime; errorMessage: string }
|
||||
> = {
|
||||
turso: {
|
||||
database: "sqlite",
|
||||
errorMessage:
|
||||
"Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.",
|
||||
},
|
||||
neon: {
|
||||
database: "postgres",
|
||||
errorMessage:
|
||||
"Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
},
|
||||
"prisma-postgres": {
|
||||
database: "postgres",
|
||||
errorMessage:
|
||||
"Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
},
|
||||
"mongodb-atlas": {
|
||||
database: "mongodb",
|
||||
errorMessage:
|
||||
"MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.",
|
||||
},
|
||||
supabase: {
|
||||
database: "postgres",
|
||||
errorMessage:
|
||||
"Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.",
|
||||
},
|
||||
d1: {
|
||||
database: "sqlite",
|
||||
runtime: "workers",
|
||||
errorMessage:
|
||||
"Cloudflare D1 setup requires SQLite database and Cloudflare Workers runtime.",
|
||||
},
|
||||
docker: {
|
||||
errorMessage:
|
||||
"Docker setup is not compatible with SQLite database or Cloudflare Workers runtime.",
|
||||
},
|
||||
none: { errorMessage: "" },
|
||||
};
|
||||
|
||||
if (dbSetup && dbSetup !== "none") {
|
||||
const validation = setupValidations[dbSetup];
|
||||
|
||||
if (validation.database && database !== validation.database) {
|
||||
exitWithError(validation.errorMessage);
|
||||
}
|
||||
|
||||
if (validation.runtime && runtime !== validation.runtime) {
|
||||
exitWithError(validation.errorMessage);
|
||||
}
|
||||
|
||||
if (dbSetup === "docker") {
|
||||
if (database === "sqlite") {
|
||||
exitWithError(
|
||||
"Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.",
|
||||
);
|
||||
}
|
||||
if (runtime === "workers") {
|
||||
exitWithError(
|
||||
"Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validateBackendConstraints(
|
||||
config: Partial<ProjectConfig>,
|
||||
providedFlags: Set<string>,
|
||||
options: CLIInput,
|
||||
): void {
|
||||
const { backend } = config;
|
||||
|
||||
if (
|
||||
providedFlags.has("backend") &&
|
||||
backend &&
|
||||
backend !== "convex" &&
|
||||
backend !== "none"
|
||||
) {
|
||||
if (providedFlags.has("runtime") && options.runtime === "none") {
|
||||
exitWithError(
|
||||
"'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (backend === "convex" || backend === "none") {
|
||||
const incompatibleFlags = incompatibleFlagsForBackend(
|
||||
backend,
|
||||
providedFlags,
|
||||
options,
|
||||
);
|
||||
if (incompatibleFlags.length > 0) {
|
||||
exitWithError(
|
||||
`The following flags are incompatible with '--backend ${backend}': ${incompatibleFlags.join(
|
||||
", ",
|
||||
)}. Please remove them.`,
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateFrontendConstraints(
|
||||
config: Partial<ProjectConfig>,
|
||||
providedFlags: Set<string>,
|
||||
): void {
|
||||
const { frontend } = config;
|
||||
|
||||
if (frontend && frontend.length > 0) {
|
||||
ensureSingleWebAndNative(frontend);
|
||||
|
||||
if (
|
||||
providedFlags.has("api") &&
|
||||
providedFlags.has("frontend") &&
|
||||
config.api
|
||||
) {
|
||||
validateApiFrontendCompatibility(config.api, frontend);
|
||||
}
|
||||
}
|
||||
|
||||
const hasWebFrontendFlag = (frontend ?? []).some((f) => isWebFrontend(f));
|
||||
validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
|
||||
}
|
||||
|
||||
export function validateApiConstraints(
|
||||
config: Partial<ProjectConfig>,
|
||||
options: CLIInput,
|
||||
): void {
|
||||
if (config.api === "none") {
|
||||
if (
|
||||
options.examples &&
|
||||
!(options.examples.length === 1 && options.examples[0] === "none") &&
|
||||
options.backend !== "convex"
|
||||
) {
|
||||
exitWithError(
|
||||
"Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validateFullConfig(
|
||||
config: Partial<ProjectConfig>,
|
||||
providedFlags: Set<string>,
|
||||
options: CLIInput,
|
||||
): void {
|
||||
validateDatabaseOrmAuth(config, providedFlags);
|
||||
validateDatabaseSetup(config, providedFlags);
|
||||
|
||||
validateBackendConstraints(config, providedFlags, options);
|
||||
|
||||
validateFrontendConstraints(config, providedFlags);
|
||||
|
||||
validateApiConstraints(config, options);
|
||||
|
||||
validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
|
||||
|
||||
validateWorkersCompatibility(providedFlags, options, config);
|
||||
|
||||
if (config.addons && config.addons.length > 0) {
|
||||
validateAddonsAgainstFrontends(config.addons, config.frontend);
|
||||
config.addons = [...new Set(config.addons)];
|
||||
}
|
||||
|
||||
validateExamplesCompatibility(
|
||||
config.examples ?? [],
|
||||
config.backend,
|
||||
config.database,
|
||||
config.frontend ?? [],
|
||||
);
|
||||
|
||||
validateAlchemyCompatibility(
|
||||
config.webDeploy,
|
||||
config.serverDeploy,
|
||||
config.frontend ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
export function validateConfigForProgrammaticUse(
|
||||
config: Partial<ProjectConfig>,
|
||||
): void {
|
||||
try {
|
||||
validateDatabaseOrmAuth(config);
|
||||
|
||||
if (config.frontend && config.frontend.length > 0) {
|
||||
ensureSingleWebAndNative(config.frontend);
|
||||
}
|
||||
|
||||
validateApiFrontendCompatibility(config.api, config.frontend);
|
||||
|
||||
if (config.addons && config.addons.length > 0) {
|
||||
validateAddonsAgainstFrontends(config.addons, config.frontend);
|
||||
}
|
||||
|
||||
validateExamplesCompatibility(
|
||||
config.examples ?? [],
|
||||
config.backend,
|
||||
config.database,
|
||||
config.frontend ?? [],
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(String(error));
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,12 @@ export function displayConfig(config: Partial<ProjectConfig>) {
|
||||
);
|
||||
}
|
||||
|
||||
if (config.serverDeploy !== undefined) {
|
||||
configDisplay.push(
|
||||
`${pc.blue("Server Deployment:")} ${String(config.serverDeploy)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (configDisplay.length === 0) {
|
||||
return pc.yellow("No configuration selected.");
|
||||
}
|
||||
|
||||
61
apps/cli/src/utils/format-with-biome.ts
Normal file
61
apps/cli/src/utils/format-with-biome.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import path from "node:path";
|
||||
import { Biome } from "@biomejs/js-api/nodejs";
|
||||
import fs from "fs-extra";
|
||||
import { glob } from "tinyglobby";
|
||||
|
||||
export async function formatProjectWithBiome(projectDir: string) {
|
||||
const biome = new Biome();
|
||||
const { projectKey } = biome.openProject(projectDir);
|
||||
|
||||
biome.applyConfiguration(projectKey, {
|
||||
formatter: {
|
||||
enabled: true,
|
||||
indentStyle: "tab",
|
||||
},
|
||||
javascript: {
|
||||
formatter: {
|
||||
quoteStyle: "double",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const files = await glob("**/*", {
|
||||
cwd: projectDir,
|
||||
dot: true,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
});
|
||||
|
||||
for (const filePath of files) {
|
||||
try {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const supported = new Set([
|
||||
".ts",
|
||||
".tsx",
|
||||
".js",
|
||||
".jsx",
|
||||
".cjs",
|
||||
".mjs",
|
||||
".cts",
|
||||
".mts",
|
||||
".json",
|
||||
".jsonc",
|
||||
".md",
|
||||
".mdx",
|
||||
".css",
|
||||
".scss",
|
||||
".html",
|
||||
]);
|
||||
if (!supported.has(ext)) continue;
|
||||
|
||||
const original = await fs.readFile(filePath, "utf8");
|
||||
const result = biome.formatContent(projectKey, original, { filePath });
|
||||
const content = result?.content;
|
||||
if (typeof content !== "string") continue;
|
||||
if (content.length === 0 && original.length > 0) continue;
|
||||
if (content !== original) {
|
||||
await fs.writeFile(filePath, content);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
|
||||
|
||||
flags.push(`--db-setup ${config.dbSetup}`);
|
||||
flags.push(`--web-deploy ${config.webDeploy}`);
|
||||
flags.push(`--server-deploy ${config.serverDeploy}`);
|
||||
flags.push(config.git ? "--git" : "--no-git");
|
||||
flags.push(`--package-manager ${config.packageManager}`);
|
||||
flags.push(config.install ? "--install" : "--no-install");
|
||||
|
||||
@@ -14,8 +14,9 @@ export async function handleDirectoryConflict(
|
||||
}> {
|
||||
while (true) {
|
||||
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
|
||||
const dirExists = fs.pathExistsSync(resolvedPath);
|
||||
const dirIsNotEmpty = dirExists && fs.readdirSync(resolvedPath).length > 0;
|
||||
const dirExists = await fs.pathExists(resolvedPath);
|
||||
const dirIsNotEmpty =
|
||||
dirExists && (await fs.readdir(resolvedPath)).length > 0;
|
||||
|
||||
if (!dirIsNotEmpty) {
|
||||
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
|
||||
|
||||
47
apps/cli/src/utils/project-name-validation.ts
Normal file
47
apps/cli/src/utils/project-name-validation.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import path from "node:path";
|
||||
import { ProjectNameSchema } from "../types";
|
||||
import { exitWithError } from "./errors";
|
||||
|
||||
export 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 validateProjectNameThrow(name: string): void {
|
||||
const result = ProjectNameSchema.safeParse(name);
|
||||
if (!result.success) {
|
||||
throw new Error(`Invalid project name: ${result.error.issues[0]?.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function extractAndValidateProjectName(
|
||||
projectName?: string,
|
||||
projectDirectory?: string,
|
||||
throwOnError = false,
|
||||
): string {
|
||||
const derivedName =
|
||||
projectName ||
|
||||
(projectDirectory
|
||||
? path.basename(path.resolve(process.cwd(), projectDirectory))
|
||||
: "");
|
||||
|
||||
if (!derivedName) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const nameToValidate = projectName ? path.basename(projectName) : derivedName;
|
||||
|
||||
if (throwOnError) {
|
||||
validateProjectNameThrow(nameToValidate);
|
||||
} else {
|
||||
validateProjectName(nameToValidate);
|
||||
}
|
||||
|
||||
return projectName || derivedName;
|
||||
}
|
||||
Reference in New Issue
Block a user