Skip to content

Commit 81d801e

Browse files
feat(cli): Add predefined templates (--template) (#700)
* feat(cli): Add predefined templates (--template) * remove test
1 parent 25de52b commit 81d801e

File tree

16 files changed

+466
-186
lines changed

16 files changed

+466
-186
lines changed

apps/cli/src/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ export function getDefaultConfig() {
4040
export const DEFAULT_CONFIG = getDefaultConfig();
4141

4242
export const dependencyVersionMap = {
43-
"better-auth": "^1.3.28",
44-
"@better-auth/expo": "^1.3.28",
43+
"better-auth": "^1.4.0",
44+
"@better-auth/expo": "^1.4.0",
4545

4646
"@clerk/nextjs": "^6.31.5",
4747
"@clerk/clerk-react": "^5.45.0",

apps/cli/src/helpers/core/command-handlers.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ import {
2525
setupProjectDirectory,
2626
} from "../../utils/project-directory";
2727
import { renderTitle } from "../../utils/render-title";
28+
import {
29+
getTemplateConfig,
30+
getTemplateDescription,
31+
} from "../../utils/templates";
2832
import {
2933
getProvidedFlags,
3034
processAndValidateFlags,
@@ -126,15 +130,40 @@ export async function createProjectHandler(
126130
shouldClearDirectory,
127131
);
128132

129-
const cliInput = {
133+
const originalInput = {
130134
...input,
131135
projectDirectory: input.projectName,
132136
};
133137

134-
const providedFlags = getProvidedFlags(cliInput);
138+
const providedFlags = getProvidedFlags(originalInput);
139+
140+
let cliInput = originalInput;
141+
142+
if (input.template && input.template !== "none") {
143+
const templateConfig = getTemplateConfig(input.template);
144+
if (templateConfig) {
145+
log.info(
146+
pc.cyan(
147+
`Using template: ${input.template} - ${getTemplateDescription(input.template)}`,
148+
),
149+
);
150+
const userOverrides: Record<string, unknown> = {};
151+
for (const [key, value] of Object.entries(originalInput)) {
152+
if (value !== undefined) {
153+
userOverrides[key] = value;
154+
}
155+
}
156+
cliInput = {
157+
...templateConfig,
158+
...userOverrides,
159+
template: input.template,
160+
projectDirectory: originalInput.projectDirectory,
161+
};
162+
}
163+
}
135164

136165
let config: ProjectConfig;
137-
if (input.yes) {
166+
if (cliInput.yes) {
138167
const flagConfig = processProvidedFlagsWithoutValidation(
139168
cliInput,
140169
finalBaseName,
@@ -175,7 +204,7 @@ export async function createProjectHandler(
175204
);
176205
}
177206

178-
await createProject(config, { manualDb: input.manualDb });
207+
await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb });
179208

180209
const reproducibleCommand = generateReproducibleCommand(config);
181210
log.success(

apps/cli/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import {
4040
RuntimeSchema,
4141
type ServerDeploy,
4242
ServerDeploySchema,
43+
type Template,
44+
TemplateSchema,
4345
type WebDeploy,
4446
WebDeploySchema,
4547
} from "./types";
@@ -60,6 +62,9 @@ export const router = os.router({
6062
z.tuple([
6163
ProjectNameSchema.optional(),
6264
z.object({
65+
template: TemplateSchema.optional().describe(
66+
"Use a predefined template"
67+
),
6368
yes: z
6469
.boolean()
6570
.optional()
@@ -263,6 +268,7 @@ export type {
263268
WebDeploy,
264269
ServerDeploy,
265270
DirectoryConflict,
271+
Template,
266272
CreateInput,
267273
AddInput,
268274
ProjectConfig,

apps/cli/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,14 @@ export const DirectoryConflictSchema = z
130130
.describe("How to handle existing directory conflicts");
131131
export type DirectoryConflict = z.infer<typeof DirectoryConflictSchema>;
132132

133+
export const TemplateSchema = z
134+
.enum(["mern", "pern", "t3", "none"])
135+
.describe("Predefined project template");
136+
export type Template = z.infer<typeof TemplateSchema>;
137+
133138
export type CreateInput = {
134139
projectName?: string;
140+
template?: Template;
135141
yes?: boolean;
136142
yolo?: boolean;
137143
verbose?: boolean;

apps/cli/src/utils/setup-catalogs.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ type PackageInfo = {
1010
};
1111

1212
type CatalogEntry = {
13-
version: string;
13+
versions: Set<string>;
1414
packages: string[];
1515
};
1616

@@ -25,7 +25,7 @@ export async function setupCatalogs(
2525
const packagePaths = [
2626
"apps/server",
2727
"apps/web",
28-
// "apps/native", // having issues
28+
"apps/native",
2929
"apps/fumadocs",
3030
"apps/docs",
3131
"packages/api",
@@ -89,10 +89,11 @@ function findDuplicateDependencies(
8989

9090
const existing = depCount.get(depName);
9191
if (existing) {
92+
existing.versions.add(version);
9293
existing.packages.push(pkg.path);
9394
} else {
9495
depCount.set(depName, {
95-
version,
96+
versions: new Set([version]),
9697
packages: [pkg.path],
9798
});
9899
}
@@ -101,8 +102,8 @@ function findDuplicateDependencies(
101102

102103
const catalog: Record<string, string> = {};
103104
for (const [depName, info] of depCount.entries()) {
104-
if (info.packages.length > 1) {
105-
catalog[depName] = info.version;
105+
if (info.packages.length > 1 && info.versions.size === 1) {
106+
catalog[depName] = Array.from(info.versions)[0];
106107
}
107108
}
108109

apps/cli/src/utils/templates.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import type { CreateInput, Template } from "../types";
2+
3+
export const TEMPLATE_PRESETS: Record<Template, CreateInput | null> = {
4+
mern: {
5+
database: "mongodb",
6+
orm: "mongoose",
7+
backend: "express",
8+
runtime: "node",
9+
frontend: ["react-router"],
10+
api: "orpc",
11+
auth: "better-auth",
12+
payments: "none",
13+
addons: ["turborepo"],
14+
examples: ["todo"],
15+
dbSetup: "mongodb-atlas",
16+
webDeploy: "none",
17+
serverDeploy: "none",
18+
},
19+
pern: {
20+
database: "postgres",
21+
orm: "drizzle",
22+
backend: "express",
23+
runtime: "node",
24+
frontend: ["tanstack-router"],
25+
api: "trpc",
26+
auth: "better-auth",
27+
payments: "none",
28+
addons: ["turborepo"],
29+
examples: ["todo"],
30+
dbSetup: "none",
31+
webDeploy: "none",
32+
serverDeploy: "none",
33+
},
34+
t3: {
35+
database: "postgres",
36+
orm: "prisma",
37+
backend: "self",
38+
runtime: "none",
39+
frontend: ["next"],
40+
api: "trpc",
41+
auth: "better-auth",
42+
payments: "none",
43+
addons: ["biome", "turborepo"],
44+
examples: ["none"],
45+
dbSetup: "none",
46+
webDeploy: "none",
47+
serverDeploy: "none",
48+
},
49+
none: null,
50+
};
51+
52+
export function getTemplateConfig(template: Template) {
53+
if (template === "none" || !template) {
54+
return null;
55+
}
56+
57+
const config = TEMPLATE_PRESETS[template];
58+
if (!config) {
59+
throw new Error(`Unknown template: ${template}`);
60+
}
61+
62+
return config;
63+
}
64+
65+
export function getTemplateDescription(template: Template) {
66+
const descriptions: Record<Template, string> = {
67+
mern: "MongoDB + Express + React + Node.js - Classic MERN stack",
68+
pern: "PostgreSQL + Express + React + Node.js - Popular PERN stack",
69+
t3: "T3 Stack - Next.js + tRPC + Prisma + PostgreSQL + Better Auth",
70+
none: "No template - Full customization",
71+
};
72+
73+
return descriptions[template] || "";
74+
}
75+
76+
export function listAvailableTemplates() {
77+
return Object.keys(TEMPLATE_PRESETS).filter(
78+
(t) => t !== "none",
79+
) as Template[];
80+
}

apps/cli/src/validation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ function validateYesFlagCombination(
3333
) {
3434
if (!options.yes) return;
3535

36+
if (options.template && options.template !== "none") {
37+
return;
38+
}
39+
3640
const coreStackFlagsProvided = Array.from(providedFlags).filter((flag) =>
3741
CORE_STACK_FLAGS.has(flag),
3842
);
Lines changed: 83 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,100 @@
1+
import { relations } from "drizzle-orm";
12
import {
23
mysqlTable,
34
varchar,
45
text,
56
timestamp,
67
boolean,
8+
index,
79
} from "drizzle-orm/mysql-core";
810

911
export const user = mysqlTable("user", {
1012
id: varchar("id", { length: 36 }).primaryKey(),
11-
name: text("name").notNull(),
13+
name: varchar("name", { length: 255 }).notNull(),
1214
email: varchar("email", { length: 255 }).notNull().unique(),
13-
emailVerified: boolean("email_verified").notNull(),
15+
emailVerified: boolean("email_verified").default(false).notNull(),
1416
image: text("image"),
15-
createdAt: timestamp("created_at").notNull(),
16-
updatedAt: timestamp("updated_at").notNull(),
17+
createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(),
18+
updatedAt: timestamp("updated_at", { fsp: 3 })
19+
.defaultNow()
20+
.$onUpdate(() => /* @__PURE__ */ new Date())
21+
.notNull(),
1722
});
1823

19-
export const session = mysqlTable("session", {
20-
id: varchar("id", { length: 36 }).primaryKey(),
21-
expiresAt: timestamp("expires_at").notNull(),
22-
token: varchar("token", { length: 255 }).notNull().unique(),
23-
createdAt: timestamp("created_at").notNull(),
24-
updatedAt: timestamp("updated_at").notNull(),
25-
ipAddress: text("ip_address"),
26-
userAgent: text("user_agent"),
27-
userId: varchar("user_id", { length: 36 })
28-
.notNull()
29-
.references(() => user.id, { onDelete: "cascade" }),
30-
});
24+
export const session = mysqlTable(
25+
"session",
26+
{
27+
id: varchar("id", { length: 36 }).primaryKey(),
28+
expiresAt: timestamp("expires_at", { fsp: 3 }).notNull(),
29+
token: varchar("token", { length: 255 }).notNull().unique(),
30+
createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(),
31+
updatedAt: timestamp("updated_at", { fsp: 3 })
32+
.$onUpdate(() => /* @__PURE__ */ new Date())
33+
.notNull(),
34+
ipAddress: text("ip_address"),
35+
userAgent: text("user_agent"),
36+
userId: varchar("user_id", { length: 36 })
37+
.notNull()
38+
.references(() => user.id, { onDelete: "cascade" }),
39+
},
40+
(table) => [index("session_userId_idx").on(table.userId)],
41+
);
3142

32-
export const account = mysqlTable("account", {
33-
id: varchar("id", { length: 36 }).primaryKey(),
34-
accountId: text("account_id").notNull(),
35-
providerId: text("provider_id").notNull(),
36-
userId: varchar("user_id", { length: 36 })
37-
.notNull()
38-
.references(() => user.id, { onDelete: "cascade" }),
39-
accessToken: text("access_token"),
40-
refreshToken: text("refresh_token"),
41-
idToken: text("id_token"),
43+
export const account = mysqlTable(
44+
"account",
45+
{
46+
id: varchar("id", { length: 36 }).primaryKey(),
47+
accountId: text("account_id").notNull(),
48+
providerId: text("provider_id").notNull(),
49+
userId: varchar("user_id", { length: 36 })
50+
.notNull()
51+
.references(() => user.id, { onDelete: "cascade" }),
52+
accessToken: text("access_token"),
53+
refreshToken: text("refresh_token"),
54+
idToken: text("id_token"),
55+
accessTokenExpiresAt: timestamp("access_token_expires_at", { fsp: 3 }),
56+
refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { fsp: 3 }),
57+
scope: text("scope"),
58+
password: text("password"),
59+
createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(),
60+
updatedAt: timestamp("updated_at", { fsp: 3 })
61+
.$onUpdate(() => /* @__PURE__ */ new Date())
62+
.notNull(),
63+
},
64+
(table) => [index("account_userId_idx").on(table.userId)],
65+
);
4266

43-
accessTokenExpiresAt: timestamp("access_token_expires_at"),
44-
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
45-
scope: text("scope"),
46-
password: text("password"),
47-
createdAt: timestamp("created_at").notNull(),
48-
updatedAt: timestamp("updated_at").notNull(),
49-
});
67+
export const verification = mysqlTable(
68+
"verification",
69+
{
70+
id: varchar("id", { length: 36 }).primaryKey(),
71+
identifier: varchar("identifier", { length: 255 }).notNull(),
72+
value: text("value").notNull(),
73+
expiresAt: timestamp("expires_at", { fsp: 3 }).notNull(),
74+
createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(),
75+
updatedAt: timestamp("updated_at", { fsp: 3 })
76+
.defaultNow()
77+
.$onUpdate(() => /* @__PURE__ */ new Date())
78+
.notNull(),
79+
},
80+
(table) => [index("verification_identifier_idx").on(table.identifier)],
81+
);
5082

51-
export const verification = mysqlTable("verification", {
52-
id: varchar("id", { length: 36 }).primaryKey(),
53-
identifier: text("identifier").notNull(),
54-
value: text("value").notNull(),
55-
expiresAt: timestamp("expires_at").notNull(),
56-
createdAt: timestamp("created_at"),
57-
updatedAt: timestamp("updated_at"),
58-
});
83+
export const userRelations = relations(user, ({ many }) => ({
84+
sessions: many(session),
85+
accounts: many(account),
86+
}));
87+
88+
export const sessionRelations = relations(session, ({ one }) => ({
89+
user: one(user, {
90+
fields: [session.userId],
91+
references: [user.id],
92+
}),
93+
}));
94+
95+
export const accountRelations = relations(account, ({ one }) => ({
96+
user: one(user, {
97+
fields: [account.userId],
98+
references: [user.id],
99+
}),
100+
}));

0 commit comments

Comments
 (0)