Skip to content

Commit 1dc9222

Browse files
use zod for project name validation
1 parent 82efc2b commit 1dc9222

File tree

6 files changed

+93
-68
lines changed

6 files changed

+93
-68
lines changed

.changeset/easy-olives-accept.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-better-t-stack": patch
3+
---
4+
5+
use zod for project name validation

apps/cli/src/index.ts

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
FrontendSchema,
2828
ORMSchema,
2929
PackageManagerSchema,
30+
ProjectNameSchema,
3031
RuntimeSchema,
3132
} from "./types";
3233
import { trackProjectCreation } from "./utils/analytics";
@@ -36,10 +37,6 @@ import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
3637
import { renderTitle } from "./utils/render-title";
3738
import { getProvidedFlags, processAndValidateFlags } from "./validation";
3839

39-
const exit = () => process.exit(0);
40-
process.on("SIGINT", exit);
41-
process.on("SIGTERM", exit);
42-
4340
const t = trpcServer.initTRPC.create();
4441

4542
async function handleDirectoryConflict(currentPathInput: string): Promise<{
@@ -268,32 +265,23 @@ const router = t.router({
268265
})
269266
.input(
270267
z.tuple([
271-
z.string().optional().describe("project-name"),
268+
ProjectNameSchema.optional(),
272269
z
273270
.object({
274271
yes: z
275272
.boolean()
276273
.optional()
277274
.default(false)
278-
.describe("Use default configuration and skip prompts"),
275+
.describe("Use default configuration"),
279276
database: DatabaseSchema.optional(),
280277
orm: ORMSchema.optional(),
281-
auth: z.boolean().optional().describe("Include authentication"),
282-
frontend: z
283-
.array(FrontendSchema)
284-
.optional()
285-
.describe("Frontend frameworks"),
286-
addons: z
287-
.array(AddonsSchema)
288-
.optional()
289-
.describe("Additional addons"),
290-
examples: z
291-
.array(ExamplesSchema)
292-
.optional()
293-
.describe("Examples to include"),
294-
git: z.boolean().optional().describe("Initialize git repository"),
278+
auth: z.boolean().optional(),
279+
frontend: z.array(FrontendSchema).optional(),
280+
addons: z.array(AddonsSchema).optional(),
281+
examples: z.array(ExamplesSchema).optional(),
282+
git: z.boolean().optional(),
295283
packageManager: PackageManagerSchema.optional(),
296-
install: z.boolean().optional().describe("Install dependencies"),
284+
install: z.boolean().optional(),
297285
dbSetup: DatabaseSetupSchema.optional(),
298286
backend: BackendSchema.optional(),
299287
runtime: RuntimeSchema.optional(),

apps/cli/src/prompts/project-name.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,14 @@ import { cancel, isCancel, text } from "@clack/prompts";
33
import fs from "fs-extra";
44
import pc from "picocolors";
55
import { DEFAULT_CONFIG } from "../constants";
6-
7-
const INVALID_CHARS = ["<", ">", ":", '"', "|", "?", "*"];
8-
const MAX_LENGTH = 255;
6+
import { ProjectNameSchema } from "../types";
97

108
function validateDirectoryName(name: string): string | undefined {
119
if (name === ".") return undefined;
1210

13-
if (!name) return "Project name cannot be empty";
14-
if (name.length > MAX_LENGTH) {
15-
return `Project name must be less than ${MAX_LENGTH} characters`;
16-
}
17-
if (INVALID_CHARS.some((char) => name.includes(char))) {
18-
return "Project name contains invalid characters";
19-
}
20-
if (name.startsWith(".") || name.startsWith("-")) {
21-
return "Project name cannot start with a dot or dash";
22-
}
23-
if (name.toLowerCase() === "node_modules") {
24-
return "Project name is reserved";
11+
const result = ProjectNameSchema.safeParse(name);
12+
if (!result.success) {
13+
return result.error.issues[0]?.message || "Invalid project name";
2514
}
2615
return undefined;
2716
}

apps/cli/src/types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,29 @@ export type DatabaseSetup = z.infer<typeof DatabaseSetupSchema>;
6666
export const APISchema = z.enum(["trpc", "orpc", "none"]).describe("API type");
6767
export type API = z.infer<typeof APISchema>;
6868

69+
export const ProjectNameSchema = z
70+
.string()
71+
.min(1, "Project name cannot be empty")
72+
.max(255, "Project name must be less than 255 characters")
73+
.refine(
74+
(name) => name === "." || !name.startsWith("."),
75+
"Project name cannot start with a dot (except for '.')",
76+
)
77+
.refine(
78+
(name) => name === "." || !name.startsWith("-"),
79+
"Project name cannot start with a dash",
80+
)
81+
.refine((name) => {
82+
const invalidChars = ["<", ">", ":", '"', "|", "?", "*"];
83+
return !invalidChars.some((char) => name.includes(char));
84+
}, "Project name contains invalid characters")
85+
.refine(
86+
(name) => name.toLowerCase() !== "node_modules",
87+
"Project name is reserved",
88+
)
89+
.describe("Project name or path");
90+
export type ProjectName = z.infer<typeof ProjectNameSchema>;
91+
6992
export type CreateInput = {
7093
projectName?: string;
7194
yes?: boolean;

apps/cli/src/validation.ts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import path from "node:path";
22
import { consola } from "consola";
3-
import type {
4-
API,
5-
Addons,
6-
Backend,
7-
CLIInput,
8-
Database,
9-
DatabaseSetup,
10-
Examples,
11-
Frontend,
12-
ORM,
13-
PackageManager,
14-
ProjectConfig,
15-
Runtime,
3+
import {
4+
type API,
5+
type Addons,
6+
type Backend,
7+
type CLIInput,
8+
type Database,
9+
type DatabaseSetup,
10+
type Examples,
11+
type Frontend,
12+
type ORM,
13+
type PackageManager,
14+
type ProjectConfig,
15+
ProjectNameSchema,
16+
type Runtime,
1617
} from "./types";
1718

1819
export function processAndValidateFlags(
@@ -82,11 +83,30 @@ export function processAndValidateFlags(
8283
}
8384

8485
if (projectName) {
86+
const result = ProjectNameSchema.safeParse(path.basename(projectName));
87+
if (!result.success) {
88+
consola.fatal(
89+
`Invalid project name: ${
90+
result.error.issues[0]?.message || "Invalid project name"
91+
}`,
92+
);
93+
process.exit(1);
94+
}
8595
config.projectName = projectName;
8696
} else if (options.projectDirectory) {
87-
config.projectName = path.basename(
97+
const baseName = path.basename(
8898
path.resolve(process.cwd(), options.projectDirectory),
8999
);
100+
const result = ProjectNameSchema.safeParse(baseName);
101+
if (!result.success) {
102+
consola.fatal(
103+
`Invalid project name: ${
104+
result.error.issues[0]?.message || "Invalid project name"
105+
}`,
106+
);
107+
process.exit(1);
108+
}
109+
config.projectName = baseName;
90110
}
91111

92112
if (options.frontend && options.frontend.length > 0) {

apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ export const auth = betterAuth({
2121
enabled: true,
2222
}
2323

24-
{{~#if (includes frontend "native")}}
24+
{{#if (includes frontend "native")}}
2525
,
2626
plugins: [expo()]
27-
{{/if~}}
27+
{{/if}}
2828
});
2929
{{/if}}
3030

@@ -52,10 +52,10 @@ export const auth = betterAuth({
5252
enabled: true,
5353
}
5454

55-
{{~#if (includes frontend "native")}}
55+
{{#if (includes frontend "native")}}
5656
,
5757
plugins: [expo()]
58-
{{/if~}}
58+
{{/if}}
5959
});
6060
{{/if}}
6161

@@ -68,19 +68,19 @@ import { expo } from "@better-auth/expo";
6868
import { client } from "../db";
6969

7070
export const auth = betterAuth({
71-
database: mongodbAdapter(client),
72-
trustedOrigins: [
73-
process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}}
74-
"my-better-t-app://",{{/if}}
75-
],
76-
emailAndPassword: {
77-
enabled: true,
78-
}
71+
database: mongodbAdapter(client),
72+
trustedOrigins: [
73+
process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}}
74+
"my-better-t-app://",{{/if}}
75+
],
76+
emailAndPassword: {
77+
enabled: true,
78+
}
7979

80-
{{~#if (includes frontend "native")}}
81-
,
82-
plugins: [expo()]
83-
{{/if~}}
80+
{{#if (includes frontend "native")}}
81+
,
82+
plugins: [expo()]
83+
{{/if}}
8484
});
8585
{{/if}}
8686

@@ -100,9 +100,9 @@ export const auth = betterAuth({
100100
enabled: true,
101101
}
102102

103-
{{~#if (includes frontend "native")}}
103+
{{#if (includes frontend "native")}}
104104
,
105105
plugins: [expo()]
106-
{{/if~}}
106+
{{/if}}
107107
});
108108
{{/if}}

0 commit comments

Comments
 (0)