use zod for project name validation

This commit is contained in:
Aman Varshney
2025-06-11 18:54:07 +05:30
parent 82efc2bfc4
commit 1dc922233f
6 changed files with 93 additions and 68 deletions

View File

@@ -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(),

View File

@@ -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;
}

View File

@@ -66,6 +66,29 @@ 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 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<typeof ProjectNameSchema>;
export type CreateInput = {
projectName?: string;
yes?: boolean;

View File

@@ -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) {

View File

@@ -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}}