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

@@ -0,0 +1,5 @@
---
"create-better-t-stack": patch
---
use zod for project name validation

View File

@@ -27,6 +27,7 @@ import {
FrontendSchema, FrontendSchema,
ORMSchema, ORMSchema,
PackageManagerSchema, PackageManagerSchema,
ProjectNameSchema,
RuntimeSchema, RuntimeSchema,
} from "./types"; } from "./types";
import { trackProjectCreation } from "./utils/analytics"; import { trackProjectCreation } from "./utils/analytics";
@@ -36,10 +37,6 @@ import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
import { renderTitle } from "./utils/render-title"; import { renderTitle } from "./utils/render-title";
import { getProvidedFlags, processAndValidateFlags } from "./validation"; import { getProvidedFlags, processAndValidateFlags } from "./validation";
const exit = () => process.exit(0);
process.on("SIGINT", exit);
process.on("SIGTERM", exit);
const t = trpcServer.initTRPC.create(); const t = trpcServer.initTRPC.create();
async function handleDirectoryConflict(currentPathInput: string): Promise<{ async function handleDirectoryConflict(currentPathInput: string): Promise<{
@@ -268,32 +265,23 @@ const router = t.router({
}) })
.input( .input(
z.tuple([ z.tuple([
z.string().optional().describe("project-name"), ProjectNameSchema.optional(),
z z
.object({ .object({
yes: z yes: z
.boolean() .boolean()
.optional() .optional()
.default(false) .default(false)
.describe("Use default configuration and skip prompts"), .describe("Use default configuration"),
database: DatabaseSchema.optional(), database: DatabaseSchema.optional(),
orm: ORMSchema.optional(), orm: ORMSchema.optional(),
auth: z.boolean().optional().describe("Include authentication"), auth: z.boolean().optional(),
frontend: z frontend: z.array(FrontendSchema).optional(),
.array(FrontendSchema) addons: z.array(AddonsSchema).optional(),
.optional() examples: z.array(ExamplesSchema).optional(),
.describe("Frontend frameworks"), git: z.boolean().optional(),
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(), packageManager: PackageManagerSchema.optional(),
install: z.boolean().optional().describe("Install dependencies"), install: z.boolean().optional(),
dbSetup: DatabaseSetupSchema.optional(), dbSetup: DatabaseSetupSchema.optional(),
backend: BackendSchema.optional(), backend: BackendSchema.optional(),
runtime: RuntimeSchema.optional(), runtime: RuntimeSchema.optional(),

View File

@@ -3,25 +3,14 @@ import { cancel, isCancel, text } from "@clack/prompts";
import fs from "fs-extra"; import fs from "fs-extra";
import pc from "picocolors"; import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants"; import { DEFAULT_CONFIG } from "../constants";
import { ProjectNameSchema } from "../types";
const INVALID_CHARS = ["<", ">", ":", '"', "|", "?", "*"];
const MAX_LENGTH = 255;
function validateDirectoryName(name: string): string | undefined { function validateDirectoryName(name: string): string | undefined {
if (name === ".") return undefined; if (name === ".") return undefined;
if (!name) return "Project name cannot be empty"; const result = ProjectNameSchema.safeParse(name);
if (name.length > MAX_LENGTH) { if (!result.success) {
return `Project name must be less than ${MAX_LENGTH} characters`; return result.error.issues[0]?.message || "Invalid project name";
}
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";
} }
return undefined; 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 const APISchema = z.enum(["trpc", "orpc", "none"]).describe("API type");
export type API = z.infer<typeof APISchema>; 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 = { export type CreateInput = {
projectName?: string; projectName?: string;
yes?: boolean; yes?: boolean;

View File

@@ -1,18 +1,19 @@
import path from "node:path"; import path from "node:path";
import { consola } from "consola"; import { consola } from "consola";
import type { import {
API, type API,
Addons, type Addons,
Backend, type Backend,
CLIInput, type CLIInput,
Database, type Database,
DatabaseSetup, type DatabaseSetup,
Examples, type Examples,
Frontend, type Frontend,
ORM, type ORM,
PackageManager, type PackageManager,
ProjectConfig, type ProjectConfig,
Runtime, ProjectNameSchema,
type Runtime,
} from "./types"; } from "./types";
export function processAndValidateFlags( export function processAndValidateFlags(
@@ -82,11 +83,30 @@ export function processAndValidateFlags(
} }
if (projectName) { 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; config.projectName = projectName;
} else if (options.projectDirectory) { } else if (options.projectDirectory) {
config.projectName = path.basename( const baseName = path.basename(
path.resolve(process.cwd(), options.projectDirectory), 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) { if (options.frontend && options.frontend.length > 0) {

View File

@@ -21,10 +21,10 @@ export const auth = betterAuth({
enabled: true, enabled: true,
} }
{{~#if (includes frontend "native")}} {{#if (includes frontend "native")}}
, ,
plugins: [expo()] plugins: [expo()]
{{/if~}} {{/if}}
}); });
{{/if}} {{/if}}
@@ -52,10 +52,10 @@ export const auth = betterAuth({
enabled: true, enabled: true,
} }
{{~#if (includes frontend "native")}} {{#if (includes frontend "native")}}
, ,
plugins: [expo()] plugins: [expo()]
{{/if~}} {{/if}}
}); });
{{/if}} {{/if}}
@@ -68,19 +68,19 @@ import { expo } from "@better-auth/expo";
import { client } from "../db"; import { client } from "../db";
export const auth = betterAuth({ export const auth = betterAuth({
database: mongodbAdapter(client), database: mongodbAdapter(client),
trustedOrigins: [ trustedOrigins: [
process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}} process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}}
"my-better-t-app://",{{/if}} "my-better-t-app://",{{/if}}
], ],
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
} }
{{~#if (includes frontend "native")}} {{#if (includes frontend "native")}}
, ,
plugins: [expo()] plugins: [expo()]
{{/if~}} {{/if}}
}); });
{{/if}} {{/if}}
@@ -100,9 +100,9 @@ export const auth = betterAuth({
enabled: true, enabled: true,
} }
{{~#if (includes frontend "native")}} {{#if (includes frontend "native")}}
, ,
plugins: [expo()] plugins: [expo()]
{{/if~}} {{/if}}
}); });
{{/if}} {{/if}}