From 1dc922233fc11630a5c9ae5cf807b4c9db2ffda6 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Wed, 11 Jun 2025 18:54:07 +0530 Subject: [PATCH] use zod for project name validation --- .changeset/easy-olives-accept.md | 5 ++ apps/cli/src/index.ts | 30 ++++-------- apps/cli/src/prompts/project-name.ts | 19 ++------ apps/cli/src/types.ts | 23 +++++++++ apps/cli/src/validation.ts | 48 +++++++++++++------ .../auth/server/base/src/lib/auth.ts.hbs | 36 +++++++------- 6 files changed, 93 insertions(+), 68 deletions(-) create mode 100644 .changeset/easy-olives-accept.md diff --git a/.changeset/easy-olives-accept.md b/.changeset/easy-olives-accept.md new file mode 100644 index 0000000..7aa2211 --- /dev/null +++ b/.changeset/easy-olives-accept.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": patch +--- + +use zod for project name validation diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index edf2d85..dcdf577 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -27,6 +27,7 @@ import { FrontendSchema, ORMSchema, PackageManagerSchema, + ProjectNameSchema, RuntimeSchema, } from "./types"; import { trackProjectCreation } from "./utils/analytics"; @@ -36,10 +37,6 @@ import { getLatestCLIVersion } from "./utils/get-latest-cli-version"; import { renderTitle } from "./utils/render-title"; import { getProvidedFlags, processAndValidateFlags } from "./validation"; -const exit = () => process.exit(0); -process.on("SIGINT", exit); -process.on("SIGTERM", exit); - const t = trpcServer.initTRPC.create(); async function handleDirectoryConflict(currentPathInput: string): Promise<{ @@ -268,32 +265,23 @@ const router = t.router({ }) .input( z.tuple([ - z.string().optional().describe("project-name"), + ProjectNameSchema.optional(), z .object({ yes: z .boolean() .optional() .default(false) - .describe("Use default configuration and skip prompts"), + .describe("Use default configuration"), 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"), + auth: z.boolean().optional(), + frontend: z.array(FrontendSchema).optional(), + addons: z.array(AddonsSchema).optional(), + examples: z.array(ExamplesSchema).optional(), + git: z.boolean().optional(), packageManager: PackageManagerSchema.optional(), - install: z.boolean().optional().describe("Install dependencies"), + install: z.boolean().optional(), dbSetup: DatabaseSetupSchema.optional(), backend: BackendSchema.optional(), runtime: RuntimeSchema.optional(), diff --git a/apps/cli/src/prompts/project-name.ts b/apps/cli/src/prompts/project-name.ts index aefe204..0b5be85 100644 --- a/apps/cli/src/prompts/project-name.ts +++ b/apps/cli/src/prompts/project-name.ts @@ -3,25 +3,14 @@ import { cancel, isCancel, text } from "@clack/prompts"; import fs from "fs-extra"; import pc from "picocolors"; import { DEFAULT_CONFIG } from "../constants"; - -const INVALID_CHARS = ["<", ">", ":", '"', "|", "?", "*"]; -const MAX_LENGTH = 255; +import { ProjectNameSchema } from "../types"; function validateDirectoryName(name: string): string | undefined { if (name === ".") return undefined; - if (!name) return "Project name cannot be empty"; - if (name.length > MAX_LENGTH) { - return `Project name must be less than ${MAX_LENGTH} characters`; - } - if (INVALID_CHARS.some((char) => name.includes(char))) { - return "Project name contains invalid characters"; - } - if (name.startsWith(".") || name.startsWith("-")) { - return "Project name cannot start with a dot or dash"; - } - if (name.toLowerCase() === "node_modules") { - return "Project name is reserved"; + const result = ProjectNameSchema.safeParse(name); + if (!result.success) { + return result.error.issues[0]?.message || "Invalid project name"; } return undefined; } diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index cf30c8f..72d90bf 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -66,6 +66,29 @@ export type DatabaseSetup = z.infer; export const APISchema = z.enum(["trpc", "orpc", "none"]).describe("API type"); export type API = z.infer; +export const ProjectNameSchema = z + .string() + .min(1, "Project name cannot be empty") + .max(255, "Project name must be less than 255 characters") + .refine( + (name) => name === "." || !name.startsWith("."), + "Project name cannot start with a dot (except for '.')", + ) + .refine( + (name) => name === "." || !name.startsWith("-"), + "Project name cannot start with a dash", + ) + .refine((name) => { + const invalidChars = ["<", ">", ":", '"', "|", "?", "*"]; + return !invalidChars.some((char) => name.includes(char)); + }, "Project name contains invalid characters") + .refine( + (name) => name.toLowerCase() !== "node_modules", + "Project name is reserved", + ) + .describe("Project name or path"); +export type ProjectName = z.infer; + export type CreateInput = { projectName?: string; yes?: boolean; diff --git a/apps/cli/src/validation.ts b/apps/cli/src/validation.ts index 1128f0c..094a690 100644 --- a/apps/cli/src/validation.ts +++ b/apps/cli/src/validation.ts @@ -1,18 +1,19 @@ import path from "node:path"; import { consola } from "consola"; -import type { - API, - Addons, - Backend, - CLIInput, - Database, - DatabaseSetup, - Examples, - Frontend, - ORM, - PackageManager, - ProjectConfig, - Runtime, +import { + type API, + type Addons, + type Backend, + type CLIInput, + type Database, + type DatabaseSetup, + type Examples, + type Frontend, + type ORM, + type PackageManager, + type ProjectConfig, + ProjectNameSchema, + type Runtime, } from "./types"; export function processAndValidateFlags( @@ -82,11 +83,30 @@ export function processAndValidateFlags( } if (projectName) { + const result = ProjectNameSchema.safeParse(path.basename(projectName)); + if (!result.success) { + consola.fatal( + `Invalid project name: ${ + result.error.issues[0]?.message || "Invalid project name" + }`, + ); + process.exit(1); + } config.projectName = projectName; } else if (options.projectDirectory) { - config.projectName = path.basename( + const baseName = path.basename( path.resolve(process.cwd(), options.projectDirectory), ); + const result = ProjectNameSchema.safeParse(baseName); + if (!result.success) { + consola.fatal( + `Invalid project name: ${ + result.error.issues[0]?.message || "Invalid project name" + }`, + ); + process.exit(1); + } + config.projectName = baseName; } if (options.frontend && options.frontend.length > 0) { diff --git a/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs b/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs index 6ac7733..21fb256 100644 --- a/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs +++ b/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs @@ -21,10 +21,10 @@ export const auth = betterAuth({ enabled: true, } - {{~#if (includes frontend "native")}} + {{#if (includes frontend "native")}} , plugins: [expo()] - {{/if~}} + {{/if}} }); {{/if}} @@ -52,10 +52,10 @@ export const auth = betterAuth({ enabled: true, } - {{~#if (includes frontend "native")}} + {{#if (includes frontend "native")}} , plugins: [expo()] - {{/if~}} + {{/if}} }); {{/if}} @@ -68,19 +68,19 @@ import { expo } from "@better-auth/expo"; import { client } from "../db"; export const auth = betterAuth({ - database: mongodbAdapter(client), - trustedOrigins: [ - process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}} - "my-better-t-app://",{{/if}} - ], - emailAndPassword: { - enabled: true, - } + database: mongodbAdapter(client), + trustedOrigins: [ + process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}} + "my-better-t-app://",{{/if}} + ], + emailAndPassword: { + enabled: true, + } - {{~#if (includes frontend "native")}} - , - plugins: [expo()] - {{/if~}} + {{#if (includes frontend "native")}} + , + plugins: [expo()] + {{/if}} }); {{/if}} @@ -100,9 +100,9 @@ export const auth = betterAuth({ enabled: true, } - {{~#if (includes frontend "native")}} + {{#if (includes frontend "native")}} , plugins: [expo()] - {{/if~}} + {{/if}} }); {{/if}}