@yoms/create-monorepo 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +204 -0
- package/dist/index.js +649 -0
- package/package.json +74 -0
- package/templates/backend-hono/base/.env.example +7 -0
- package/templates/backend-hono/base/Dockerfile +55 -0
- package/templates/backend-hono/base/package.json +30 -0
- package/templates/backend-hono/base/src/config/env.ts +12 -0
- package/templates/backend-hono/base/src/config/logger.ts +52 -0
- package/templates/backend-hono/base/src/index.ts +49 -0
- package/templates/backend-hono/base/src/lib/__tests__/response.test.ts +66 -0
- package/templates/backend-hono/base/src/lib/errors.ts +87 -0
- package/templates/backend-hono/base/src/lib/response.ts +72 -0
- package/templates/backend-hono/base/src/middleware/__tests__/error.test.ts +86 -0
- package/templates/backend-hono/base/src/middleware/cors.middleware.ts +9 -0
- package/templates/backend-hono/base/src/middleware/error.middleware.ts +43 -0
- package/templates/backend-hono/base/src/middleware/logger.middleware.ts +14 -0
- package/templates/backend-hono/base/src/middleware/rate-limit.middleware.ts +135 -0
- package/templates/backend-hono/base/src/routes/__tests__/health.test.ts +36 -0
- package/templates/backend-hono/base/src/routes/health.route.ts +14 -0
- package/templates/backend-hono/base/src/types/app.types.ts +26 -0
- package/templates/backend-hono/base/tsconfig.json +10 -0
- package/templates/backend-hono/base/vitest.config.ts +19 -0
- package/templates/backend-hono/features/mongodb-prisma/env-additions.txt +2 -0
- package/templates/backend-hono/features/mongodb-prisma/package-additions.json +14 -0
- package/templates/backend-hono/features/mongodb-prisma/prisma/schema.prisma +18 -0
- package/templates/backend-hono/features/mongodb-prisma/src/config/database.ts +43 -0
- package/templates/backend-hono/features/mongodb-prisma/src/routes/users.route.ts +82 -0
- package/templates/backend-hono/features/mongodb-prisma/src/services/user.service.ts +121 -0
- package/templates/backend-hono/features/postgres-prisma/env-additions.txt +2 -0
- package/templates/backend-hono/features/postgres-prisma/package-additions.json +15 -0
- package/templates/backend-hono/features/postgres-prisma/prisma/schema.prisma +18 -0
- package/templates/backend-hono/features/postgres-prisma/src/config/database.ts +43 -0
- package/templates/backend-hono/features/postgres-prisma/src/routes/users.route.ts +82 -0
- package/templates/backend-hono/features/postgres-prisma/src/services/user.service.ts +121 -0
- package/templates/backend-hono/features/redis/env-additions.txt +2 -0
- package/templates/backend-hono/features/redis/package-additions.json +8 -0
- package/templates/backend-hono/features/redis/src/config/redis.ts +32 -0
- package/templates/backend-hono/features/redis/src/services/cache.service.ts +107 -0
- package/templates/backend-hono/features/smtp/env-additions.txt +7 -0
- package/templates/backend-hono/features/smtp/package-additions.json +8 -0
- package/templates/backend-hono/features/smtp/src/config/mail.ts +38 -0
- package/templates/backend-hono/features/smtp/src/services/email.service.ts +78 -0
- package/templates/backend-hono/features/swagger/package-additions.json +6 -0
- package/templates/backend-hono/features/swagger/src/lib/openapi.ts +19 -0
- package/templates/backend-hono/features/swagger/src/routes/docs.route.ts +18 -0
- package/templates/frontend-nextjs/base/.env.example +2 -0
- package/templates/frontend-nextjs/base/app/globals.css +59 -0
- package/templates/frontend-nextjs/base/app/layout.tsx +22 -0
- package/templates/frontend-nextjs/base/app/page.tsx +49 -0
- package/templates/frontend-nextjs/base/components.json +18 -0
- package/templates/frontend-nextjs/base/lib/api-client.ts +67 -0
- package/templates/frontend-nextjs/base/lib/utils.ts +6 -0
- package/templates/frontend-nextjs/base/next.config.ts +8 -0
- package/templates/frontend-nextjs/base/package.json +33 -0
- package/templates/frontend-nextjs/base/postcss.config.mjs +9 -0
- package/templates/frontend-nextjs/base/public/.gitkeep +1 -0
- package/templates/frontend-nextjs/base/tailwind.config.ts +58 -0
- package/templates/frontend-nextjs/base/tsconfig.json +19 -0
- package/templates/shared/base/package.json +19 -0
- package/templates/shared/base/src/index.ts +5 -0
- package/templates/shared/base/src/schemas/user.schema.ts +34 -0
- package/templates/shared/base/src/types.ts +46 -0
- package/templates/shared/base/tsconfig.json +9 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { cac } from "cac";
|
|
5
|
+
|
|
6
|
+
// src/commands/create.ts
|
|
7
|
+
import path7 from "path";
|
|
8
|
+
import ora from "ora";
|
|
9
|
+
import { execa as execa2 } from "execa";
|
|
10
|
+
|
|
11
|
+
// src/utils/file-ops.ts
|
|
12
|
+
import fs from "fs-extra";
|
|
13
|
+
import path from "path";
|
|
14
|
+
async function isDirEmpty(dirPath) {
|
|
15
|
+
if (!await fs.pathExists(dirPath)) return true;
|
|
16
|
+
const files = await fs.readdir(dirPath);
|
|
17
|
+
return files.length === 0;
|
|
18
|
+
}
|
|
19
|
+
async function ensureDir(dirPath) {
|
|
20
|
+
await fs.ensureDir(dirPath);
|
|
21
|
+
}
|
|
22
|
+
async function writeFile(filePath, content) {
|
|
23
|
+
await fs.outputFile(filePath, content);
|
|
24
|
+
}
|
|
25
|
+
async function readFile(filePath) {
|
|
26
|
+
return fs.readFile(filePath, "utf-8");
|
|
27
|
+
}
|
|
28
|
+
function replaceTokens(content, tokens) {
|
|
29
|
+
let result = content;
|
|
30
|
+
for (const [token, value] of Object.entries(tokens)) {
|
|
31
|
+
result = result.replaceAll(token, value);
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
async function copyDirRecursive(src, dest, tokens) {
|
|
36
|
+
await ensureDir(dest);
|
|
37
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const srcPath = path.join(src, entry.name);
|
|
40
|
+
const destPath = path.join(dest, entry.name);
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
await copyDirRecursive(srcPath, destPath, tokens);
|
|
43
|
+
} else {
|
|
44
|
+
let content = await readFile(srcPath);
|
|
45
|
+
if (tokens) {
|
|
46
|
+
content = replaceTokens(content, tokens);
|
|
47
|
+
}
|
|
48
|
+
await writeFile(destPath, content);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/utils/logger.ts
|
|
54
|
+
import picocolors from "picocolors";
|
|
55
|
+
var logger = {
|
|
56
|
+
info: (message) => {
|
|
57
|
+
console.log(picocolors.blue("\u2139"), message);
|
|
58
|
+
},
|
|
59
|
+
success: (message) => {
|
|
60
|
+
console.log(picocolors.green("\u2714"), message);
|
|
61
|
+
},
|
|
62
|
+
error: (message) => {
|
|
63
|
+
console.log(picocolors.red("\u2716"), message);
|
|
64
|
+
},
|
|
65
|
+
warn: (message) => {
|
|
66
|
+
console.log(picocolors.yellow("\u26A0"), message);
|
|
67
|
+
},
|
|
68
|
+
debug: (...args) => {
|
|
69
|
+
if (process.env.DEBUG) {
|
|
70
|
+
console.log(picocolors.gray("[debug]"), ...args);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/utils/package-manager.ts
|
|
76
|
+
import { execa } from "execa";
|
|
77
|
+
async function installDependencies(cwd, pm) {
|
|
78
|
+
await execa(pm, ["install"], { cwd, stdio: "inherit" });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/prompts/project.prompts.ts
|
|
82
|
+
import enquirer from "enquirer";
|
|
83
|
+
async function promptProjectName(defaultName = "my-monorepo") {
|
|
84
|
+
const { projectName } = await enquirer.prompt({
|
|
85
|
+
type: "input",
|
|
86
|
+
name: "projectName",
|
|
87
|
+
message: "Project name:",
|
|
88
|
+
initial: defaultName,
|
|
89
|
+
validate: (input) => {
|
|
90
|
+
if (!input.trim()) return "Project name is required";
|
|
91
|
+
if (!/^[a-z0-9-]+$/.test(input)) {
|
|
92
|
+
return "Project name must be lowercase alphanumeric with hyphens only";
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return projectName;
|
|
98
|
+
}
|
|
99
|
+
async function promptPackageManager() {
|
|
100
|
+
const { packageManager } = await enquirer.prompt({
|
|
101
|
+
type: "select",
|
|
102
|
+
name: "packageManager",
|
|
103
|
+
message: "Select package manager:",
|
|
104
|
+
choices: ["pnpm", "npm", "yarn", "bun"],
|
|
105
|
+
initial: 0
|
|
106
|
+
// Default to pnpm
|
|
107
|
+
});
|
|
108
|
+
return packageManager;
|
|
109
|
+
}
|
|
110
|
+
async function promptIncludeBackend() {
|
|
111
|
+
const { includeBackend } = await enquirer.prompt({
|
|
112
|
+
type: "confirm",
|
|
113
|
+
name: "includeBackend",
|
|
114
|
+
message: "Include backend package?",
|
|
115
|
+
initial: true
|
|
116
|
+
});
|
|
117
|
+
return includeBackend;
|
|
118
|
+
}
|
|
119
|
+
async function promptIncludeFrontend() {
|
|
120
|
+
const { includeFrontend } = await enquirer.prompt({
|
|
121
|
+
type: "confirm",
|
|
122
|
+
name: "includeFrontend",
|
|
123
|
+
message: "Include frontend package?",
|
|
124
|
+
initial: true
|
|
125
|
+
});
|
|
126
|
+
return includeFrontend;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/prompts/backend.prompts.ts
|
|
130
|
+
import enquirer2 from "enquirer";
|
|
131
|
+
async function promptBackendConfig() {
|
|
132
|
+
const { backendType } = await enquirer2.prompt({
|
|
133
|
+
type: "select",
|
|
134
|
+
name: "backendType",
|
|
135
|
+
message: "Backend type:",
|
|
136
|
+
choices: [
|
|
137
|
+
{ name: "web", message: "Web API (REST/HTTP)" },
|
|
138
|
+
{ name: "worker", message: "Background Worker" },
|
|
139
|
+
{ name: "cli", message: "CLI Tool" }
|
|
140
|
+
],
|
|
141
|
+
initial: 0
|
|
142
|
+
});
|
|
143
|
+
let framework = "hono";
|
|
144
|
+
if (backendType === "web") {
|
|
145
|
+
const { selectedFramework } = await enquirer2.prompt({
|
|
146
|
+
type: "select",
|
|
147
|
+
name: "selectedFramework",
|
|
148
|
+
message: "Backend framework:",
|
|
149
|
+
choices: [
|
|
150
|
+
{ name: "hono", message: "Hono (Recommended - Best TypeScript inference)" },
|
|
151
|
+
{ name: "tsoa", message: "TSOA (OpenAPI-first with code generation)" }
|
|
152
|
+
],
|
|
153
|
+
initial: 0
|
|
154
|
+
});
|
|
155
|
+
framework = selectedFramework;
|
|
156
|
+
}
|
|
157
|
+
const { database } = await enquirer2.prompt({
|
|
158
|
+
type: "select",
|
|
159
|
+
name: "database",
|
|
160
|
+
message: "Database:",
|
|
161
|
+
choices: [
|
|
162
|
+
{ name: "postgres", message: "PostgreSQL with Prisma" },
|
|
163
|
+
{ name: "mongodb", message: "MongoDB with Prisma" },
|
|
164
|
+
{ name: "none", message: "None" }
|
|
165
|
+
],
|
|
166
|
+
initial: 0
|
|
167
|
+
});
|
|
168
|
+
const { includeRedis } = await enquirer2.prompt({
|
|
169
|
+
type: "confirm",
|
|
170
|
+
name: "includeRedis",
|
|
171
|
+
message: "Include Redis caching?",
|
|
172
|
+
initial: false
|
|
173
|
+
});
|
|
174
|
+
const { includeSmtp } = await enquirer2.prompt({
|
|
175
|
+
type: "confirm",
|
|
176
|
+
name: "includeSmtp",
|
|
177
|
+
message: "Include SMTP email service?",
|
|
178
|
+
initial: false
|
|
179
|
+
});
|
|
180
|
+
let includeSwagger = false;
|
|
181
|
+
if (backendType === "web" && framework === "hono") {
|
|
182
|
+
const { swagger } = await enquirer2.prompt({
|
|
183
|
+
type: "confirm",
|
|
184
|
+
name: "swagger",
|
|
185
|
+
message: "Include Swagger/OpenAPI documentation?",
|
|
186
|
+
initial: false
|
|
187
|
+
});
|
|
188
|
+
includeSwagger = swagger;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
type: backendType,
|
|
192
|
+
framework,
|
|
193
|
+
database: database === "none" ? void 0 : database,
|
|
194
|
+
includeRedis,
|
|
195
|
+
includeSmtp,
|
|
196
|
+
includeSwagger
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/prompts/frontend.prompts.ts
|
|
201
|
+
import enquirer3 from "enquirer";
|
|
202
|
+
async function promptFrontendConfig() {
|
|
203
|
+
const { includeShadcn } = await enquirer3.prompt({
|
|
204
|
+
type: "confirm",
|
|
205
|
+
name: "includeShadcn",
|
|
206
|
+
message: "Include shadcn/ui components?",
|
|
207
|
+
initial: true
|
|
208
|
+
});
|
|
209
|
+
const { apiUrl } = await enquirer3.prompt({
|
|
210
|
+
type: "input",
|
|
211
|
+
name: "apiUrl",
|
|
212
|
+
message: "API URL for development:",
|
|
213
|
+
initial: "http://localhost:3001",
|
|
214
|
+
validate: (input) => {
|
|
215
|
+
if (!input.trim()) return "API URL is required";
|
|
216
|
+
try {
|
|
217
|
+
new URL(input);
|
|
218
|
+
return true;
|
|
219
|
+
} catch {
|
|
220
|
+
return "Must be a valid URL";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return {
|
|
225
|
+
framework: "nextjs",
|
|
226
|
+
includeShadcn,
|
|
227
|
+
apiUrl
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/generators/monorepo.generator.ts
|
|
232
|
+
import path2 from "path";
|
|
233
|
+
|
|
234
|
+
// src/generators/base.generator.ts
|
|
235
|
+
var BaseGenerator = class {
|
|
236
|
+
options;
|
|
237
|
+
constructor(options) {
|
|
238
|
+
this.options = options;
|
|
239
|
+
}
|
|
240
|
+
getTokens(additionalTokens) {
|
|
241
|
+
const defaultTokens = {
|
|
242
|
+
__PROJECT_NAME__: this.options.projectName,
|
|
243
|
+
__PACKAGE_SCOPE__: `@${this.options.projectName}`,
|
|
244
|
+
__DATABASE_PROVIDER__: "none",
|
|
245
|
+
__HAS_REDIS__: "false",
|
|
246
|
+
__HAS_SMTP__: "false",
|
|
247
|
+
__API_PORT__: "3001",
|
|
248
|
+
__WEB_PORT__: "3000",
|
|
249
|
+
__BACKEND_FRAMEWORK__: "hono",
|
|
250
|
+
__API_URL__: "http://localhost:3001"
|
|
251
|
+
};
|
|
252
|
+
return { ...defaultTokens, ...additionalTokens };
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// src/generators/monorepo.generator.ts
|
|
257
|
+
var MonorepoGenerator = class extends BaseGenerator {
|
|
258
|
+
async generate(config) {
|
|
259
|
+
await ensureDir(this.options.projectDir);
|
|
260
|
+
await this.createRootFiles(config);
|
|
261
|
+
await this.createPackagesDirectory();
|
|
262
|
+
}
|
|
263
|
+
async createRootFiles(config) {
|
|
264
|
+
const workspaceContent = `packages:
|
|
265
|
+
- 'packages/*'
|
|
266
|
+
`;
|
|
267
|
+
await writeFile(path2.join(this.options.projectDir, "pnpm-workspace.yaml"), workspaceContent);
|
|
268
|
+
const packageJson = {
|
|
269
|
+
name: this.options.projectName,
|
|
270
|
+
version: "0.1.0",
|
|
271
|
+
private: true,
|
|
272
|
+
scripts: {
|
|
273
|
+
dev: `${this.options.packageManager} --parallel -r dev`,
|
|
274
|
+
build: `${this.options.packageManager} --parallel -r build`,
|
|
275
|
+
typecheck: `${this.options.packageManager} --parallel -r typecheck`,
|
|
276
|
+
lint: `${this.options.packageManager} --parallel -r lint`,
|
|
277
|
+
format: `${this.options.packageManager} --parallel -r format`
|
|
278
|
+
},
|
|
279
|
+
devDependencies: {
|
|
280
|
+
typescript: "^5.7.2",
|
|
281
|
+
prettier: "^3.4.2"
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
await writeFile(
|
|
285
|
+
path2.join(this.options.projectDir, "package.json"),
|
|
286
|
+
JSON.stringify(packageJson, null, 2)
|
|
287
|
+
);
|
|
288
|
+
const gitignoreContent = `# Dependencies
|
|
289
|
+
node_modules/
|
|
290
|
+
pnpm-lock.yaml
|
|
291
|
+
yarn.lock
|
|
292
|
+
package-lock.json
|
|
293
|
+
|
|
294
|
+
# Build outputs
|
|
295
|
+
dist/
|
|
296
|
+
.next/
|
|
297
|
+
*.tsbuildinfo
|
|
298
|
+
|
|
299
|
+
# Environment
|
|
300
|
+
.env
|
|
301
|
+
.env.local
|
|
302
|
+
.env.*.local
|
|
303
|
+
|
|
304
|
+
# IDE
|
|
305
|
+
.vscode/
|
|
306
|
+
.idea/
|
|
307
|
+
*.swp
|
|
308
|
+
*.swo
|
|
309
|
+
*~
|
|
310
|
+
|
|
311
|
+
# OS
|
|
312
|
+
.DS_Store
|
|
313
|
+
Thumbs.db
|
|
314
|
+
|
|
315
|
+
# Logs
|
|
316
|
+
logs/
|
|
317
|
+
*.log
|
|
318
|
+
|
|
319
|
+
# Testing
|
|
320
|
+
coverage/
|
|
321
|
+
|
|
322
|
+
# Misc
|
|
323
|
+
.cache/
|
|
324
|
+
temp/
|
|
325
|
+
tmp/
|
|
326
|
+
`;
|
|
327
|
+
await writeFile(path2.join(this.options.projectDir, ".gitignore"), gitignoreContent);
|
|
328
|
+
const tokens = this.getTokens();
|
|
329
|
+
const readmeContent = `# ${tokens.__PROJECT_NAME__}
|
|
330
|
+
|
|
331
|
+
Generated with create-monorepo.
|
|
332
|
+
|
|
333
|
+
## Getting Started
|
|
334
|
+
|
|
335
|
+
Install dependencies:
|
|
336
|
+
|
|
337
|
+
\`\`\`bash
|
|
338
|
+
${this.options.packageManager} install
|
|
339
|
+
\`\`\`
|
|
340
|
+
|
|
341
|
+
Run in development mode:
|
|
342
|
+
|
|
343
|
+
\`\`\`bash
|
|
344
|
+
${this.options.packageManager} dev
|
|
345
|
+
\`\`\`
|
|
346
|
+
|
|
347
|
+
Build for production:
|
|
348
|
+
|
|
349
|
+
\`\`\`bash
|
|
350
|
+
${this.options.packageManager} build
|
|
351
|
+
\`\`\`
|
|
352
|
+
|
|
353
|
+
## Project Structure
|
|
354
|
+
|
|
355
|
+
\`\`\`
|
|
356
|
+
${tokens.__PROJECT_NAME__}/
|
|
357
|
+
\u251C\u2500\u2500 packages/
|
|
358
|
+
${config.includeBackend ? "\u2502 \u251C\u2500\u2500 api/ # Backend API\n" : ""}${config.includeFrontend ? "\u2502 \u251C\u2500\u2500 web/ # Frontend application\n" : ""}${config.includeBackend && config.includeFrontend ? "\u2502 \u2514\u2500\u2500 shared/ # Shared types and utilities\n" : ""}\u2514\u2500\u2500 ...
|
|
359
|
+
\`\`\`
|
|
360
|
+
|
|
361
|
+
## Available Scripts
|
|
362
|
+
|
|
363
|
+
- \`${this.options.packageManager} dev\` - Start all packages in development mode
|
|
364
|
+
- \`${this.options.packageManager} build\` - Build all packages
|
|
365
|
+
- \`${this.options.packageManager} typecheck\` - Type-check all packages
|
|
366
|
+
- \`${this.options.packageManager} lint\` - Lint all packages
|
|
367
|
+
- \`${this.options.packageManager} format\` - Format all packages with Prettier
|
|
368
|
+
`;
|
|
369
|
+
await writeFile(path2.join(this.options.projectDir, "README.md"), readmeContent);
|
|
370
|
+
const tsconfigBase = {
|
|
371
|
+
compilerOptions: {
|
|
372
|
+
target: "ES2022",
|
|
373
|
+
module: "ESNext",
|
|
374
|
+
lib: ["ES2022"],
|
|
375
|
+
moduleResolution: "bundler",
|
|
376
|
+
resolveJsonModule: true,
|
|
377
|
+
allowJs: true,
|
|
378
|
+
strict: true,
|
|
379
|
+
esModuleInterop: true,
|
|
380
|
+
skipLibCheck: true,
|
|
381
|
+
forceConsistentCasingInFileNames: true,
|
|
382
|
+
noUnusedLocals: true,
|
|
383
|
+
noUnusedParameters: true,
|
|
384
|
+
noImplicitReturns: true,
|
|
385
|
+
noFallthroughCasesInSwitch: true
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
await writeFile(
|
|
389
|
+
path2.join(this.options.projectDir, "tsconfig.base.json"),
|
|
390
|
+
JSON.stringify(tsconfigBase, null, 2)
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
async createPackagesDirectory() {
|
|
394
|
+
await ensureDir(path2.join(this.options.projectDir, "packages"));
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// src/generators/backend.generator.ts
|
|
399
|
+
import path4 from "path";
|
|
400
|
+
import { fileURLToPath } from "url";
|
|
401
|
+
|
|
402
|
+
// src/utils/template-merger.ts
|
|
403
|
+
import fs2 from "fs-extra";
|
|
404
|
+
import path3 from "path";
|
|
405
|
+
async function mergeFeature(baseDir, featurePath, tokens) {
|
|
406
|
+
const featureSrcPath = path3.join(featurePath, "src");
|
|
407
|
+
const featurePrismaPath = path3.join(featurePath, "prisma");
|
|
408
|
+
if (await fs2.pathExists(featureSrcPath)) {
|
|
409
|
+
await copyDirRecursive(featureSrcPath, path3.join(baseDir, "src"), tokens);
|
|
410
|
+
}
|
|
411
|
+
if (await fs2.pathExists(featurePrismaPath)) {
|
|
412
|
+
await copyDirRecursive(featurePrismaPath, path3.join(baseDir, "prisma"), tokens);
|
|
413
|
+
}
|
|
414
|
+
const packageAdditionsPath = path3.join(featurePath, "package-additions.json");
|
|
415
|
+
if (await fs2.pathExists(packageAdditionsPath)) {
|
|
416
|
+
await mergePackageJson(baseDir, packageAdditionsPath);
|
|
417
|
+
}
|
|
418
|
+
const envAdditionsPath = path3.join(featurePath, "env-additions.txt");
|
|
419
|
+
if (await fs2.pathExists(envAdditionsPath)) {
|
|
420
|
+
await mergeEnvFile(baseDir, envAdditionsPath);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async function mergePackageJson(baseDir, additionsPath) {
|
|
424
|
+
const packageJsonPath = path3.join(baseDir, "package.json");
|
|
425
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath));
|
|
426
|
+
const additions = JSON.parse(await readFile(additionsPath));
|
|
427
|
+
if (additions.dependencies) {
|
|
428
|
+
packageJson.dependencies = {
|
|
429
|
+
...packageJson.dependencies,
|
|
430
|
+
...additions.dependencies
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (additions.devDependencies) {
|
|
434
|
+
packageJson.devDependencies = {
|
|
435
|
+
...packageJson.devDependencies,
|
|
436
|
+
...additions.devDependencies
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
if (additions.scripts) {
|
|
440
|
+
packageJson.scripts = {
|
|
441
|
+
...packageJson.scripts,
|
|
442
|
+
...additions.scripts
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
446
|
+
}
|
|
447
|
+
async function mergeEnvFile(baseDir, additionsPath) {
|
|
448
|
+
const envPath = path3.join(baseDir, ".env.example");
|
|
449
|
+
let envContent = "";
|
|
450
|
+
if (await fs2.pathExists(envPath)) {
|
|
451
|
+
envContent = await readFile(envPath);
|
|
452
|
+
if (!envContent.endsWith("\n")) {
|
|
453
|
+
envContent += "\n";
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const additions = await readFile(additionsPath);
|
|
457
|
+
envContent += "\n" + additions;
|
|
458
|
+
await writeFile(envPath, envContent);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/generators/backend.generator.ts
|
|
462
|
+
var __dirname = path4.dirname(fileURLToPath(import.meta.url));
|
|
463
|
+
var BackendGenerator = class extends BaseGenerator {
|
|
464
|
+
config;
|
|
465
|
+
constructor(options) {
|
|
466
|
+
super(options);
|
|
467
|
+
this.config = options.config;
|
|
468
|
+
}
|
|
469
|
+
async generate() {
|
|
470
|
+
const backendDir = path4.join(this.options.projectDir, "packages", "api");
|
|
471
|
+
await ensureDir(backendDir);
|
|
472
|
+
const tokens = this.getTokens({
|
|
473
|
+
__BACKEND_FRAMEWORK__: this.config.framework,
|
|
474
|
+
__DATABASE_PROVIDER__: this.config.database || "none",
|
|
475
|
+
__HAS_REDIS__: String(this.config.includeRedis),
|
|
476
|
+
__HAS_SMTP__: String(this.config.includeSmtp)
|
|
477
|
+
});
|
|
478
|
+
let templatePath;
|
|
479
|
+
if (this.config.type === "web") {
|
|
480
|
+
templatePath = path4.join(__dirname, "../../templates", `backend-${this.config.framework}`, "base");
|
|
481
|
+
} else if (this.config.type === "worker") {
|
|
482
|
+
templatePath = path4.join(__dirname, "../../templates", "backend-worker", "base");
|
|
483
|
+
} else {
|
|
484
|
+
templatePath = path4.join(__dirname, "../../templates", "backend-cli", "base");
|
|
485
|
+
}
|
|
486
|
+
await copyDirRecursive(templatePath, backendDir, tokens);
|
|
487
|
+
if (this.config.type === "web") {
|
|
488
|
+
const featuresPath = path4.join(__dirname, "../../templates", `backend-${this.config.framework}`, "features");
|
|
489
|
+
if (this.config.database) {
|
|
490
|
+
const dbFeaturePath = path4.join(featuresPath, `${this.config.database}-prisma`);
|
|
491
|
+
await mergeFeature(backendDir, dbFeaturePath, tokens);
|
|
492
|
+
}
|
|
493
|
+
if (this.config.includeRedis) {
|
|
494
|
+
const redisFeaturePath = path4.join(featuresPath, "redis");
|
|
495
|
+
await mergeFeature(backendDir, redisFeaturePath, tokens);
|
|
496
|
+
}
|
|
497
|
+
if (this.config.includeSmtp) {
|
|
498
|
+
const smtpFeaturePath = path4.join(featuresPath, "smtp");
|
|
499
|
+
await mergeFeature(backendDir, smtpFeaturePath, tokens);
|
|
500
|
+
}
|
|
501
|
+
if (this.config.includeSwagger) {
|
|
502
|
+
const swaggerFeaturePath = path4.join(featuresPath, "swagger");
|
|
503
|
+
await mergeFeature(backendDir, swaggerFeaturePath, tokens);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// src/generators/frontend.generator.ts
|
|
510
|
+
import path5 from "path";
|
|
511
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
512
|
+
var __dirname2 = path5.dirname(fileURLToPath2(import.meta.url));
|
|
513
|
+
var FrontendGenerator = class extends BaseGenerator {
|
|
514
|
+
config;
|
|
515
|
+
constructor(options) {
|
|
516
|
+
super(options);
|
|
517
|
+
this.config = options.config;
|
|
518
|
+
}
|
|
519
|
+
async generate() {
|
|
520
|
+
const frontendDir = path5.join(this.options.projectDir, "packages", "web");
|
|
521
|
+
await ensureDir(frontendDir);
|
|
522
|
+
const tokens = this.getTokens({
|
|
523
|
+
__API_URL__: this.config.apiUrl
|
|
524
|
+
});
|
|
525
|
+
const templatePath = path5.join(__dirname2, "../../templates", "frontend-nextjs", "base");
|
|
526
|
+
await copyDirRecursive(templatePath, frontendDir, tokens);
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// src/generators/shared.generator.ts
|
|
531
|
+
import path6 from "path";
|
|
532
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
533
|
+
var __dirname3 = path6.dirname(fileURLToPath3(import.meta.url));
|
|
534
|
+
var SharedGenerator = class extends BaseGenerator {
|
|
535
|
+
async generate() {
|
|
536
|
+
const sharedDir = path6.join(this.options.projectDir, "packages", "shared");
|
|
537
|
+
await ensureDir(sharedDir);
|
|
538
|
+
const tokens = this.getTokens();
|
|
539
|
+
const templatePath = path6.join(__dirname3, "../../templates", "shared", "base");
|
|
540
|
+
await copyDirRecursive(templatePath, sharedDir, tokens);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// src/commands/create.ts
|
|
545
|
+
async function createMonorepo(targetDir) {
|
|
546
|
+
console.log("");
|
|
547
|
+
logger.info("Welcome to create-monorepo!");
|
|
548
|
+
console.log("");
|
|
549
|
+
const projectName = await promptProjectName(targetDir ? path7.basename(targetDir) : void 0);
|
|
550
|
+
const projectDir = path7.resolve(process.cwd(), targetDir || projectName);
|
|
551
|
+
if (!await isDirEmpty(projectDir)) {
|
|
552
|
+
logger.error(`Directory "${projectDir}" is not empty. Please choose an empty directory.`);
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
const packageManager = await promptPackageManager();
|
|
556
|
+
const includeBackend = await promptIncludeBackend();
|
|
557
|
+
const includeFrontend = await promptIncludeFrontend();
|
|
558
|
+
if (!includeBackend && !includeFrontend) {
|
|
559
|
+
logger.error("You must include at least one package (backend or frontend).");
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
const config = {
|
|
563
|
+
projectName,
|
|
564
|
+
packageManager,
|
|
565
|
+
includeBackend,
|
|
566
|
+
includeFrontend
|
|
567
|
+
};
|
|
568
|
+
if (includeBackend) {
|
|
569
|
+
console.log("");
|
|
570
|
+
logger.info("Configure backend package:");
|
|
571
|
+
config.backend = await promptBackendConfig();
|
|
572
|
+
}
|
|
573
|
+
if (includeFrontend) {
|
|
574
|
+
console.log("");
|
|
575
|
+
logger.info("Configure frontend package:");
|
|
576
|
+
config.frontend = await promptFrontendConfig();
|
|
577
|
+
}
|
|
578
|
+
console.log("");
|
|
579
|
+
logger.info("Generating project...");
|
|
580
|
+
try {
|
|
581
|
+
const monorepoGenerator = new MonorepoGenerator({
|
|
582
|
+
projectName,
|
|
583
|
+
projectDir,
|
|
584
|
+
packageManager
|
|
585
|
+
});
|
|
586
|
+
const spinner = ora("Creating monorepo structure...").start();
|
|
587
|
+
await monorepoGenerator.generate(config);
|
|
588
|
+
spinner.succeed("Monorepo structure created");
|
|
589
|
+
if (includeBackend && includeFrontend) {
|
|
590
|
+
spinner.start("Generating shared package...");
|
|
591
|
+
const sharedGenerator = new SharedGenerator({
|
|
592
|
+
projectName,
|
|
593
|
+
projectDir,
|
|
594
|
+
packageManager
|
|
595
|
+
});
|
|
596
|
+
await sharedGenerator.generate();
|
|
597
|
+
spinner.succeed("Shared package generated");
|
|
598
|
+
}
|
|
599
|
+
if (includeBackend && config.backend) {
|
|
600
|
+
spinner.start("Generating backend package...");
|
|
601
|
+
const backendGenerator = new BackendGenerator({
|
|
602
|
+
projectName,
|
|
603
|
+
projectDir,
|
|
604
|
+
packageManager,
|
|
605
|
+
config: config.backend
|
|
606
|
+
});
|
|
607
|
+
await backendGenerator.generate();
|
|
608
|
+
spinner.succeed("Backend package generated");
|
|
609
|
+
}
|
|
610
|
+
if (includeFrontend && config.frontend) {
|
|
611
|
+
spinner.start("Generating frontend package...");
|
|
612
|
+
const frontendGenerator = new FrontendGenerator({
|
|
613
|
+
projectName,
|
|
614
|
+
projectDir,
|
|
615
|
+
packageManager,
|
|
616
|
+
config: config.frontend
|
|
617
|
+
});
|
|
618
|
+
await frontendGenerator.generate();
|
|
619
|
+
spinner.succeed("Frontend package generated");
|
|
620
|
+
}
|
|
621
|
+
spinner.start("Installing dependencies...");
|
|
622
|
+
await installDependencies(projectDir, packageManager);
|
|
623
|
+
spinner.succeed("Dependencies installed");
|
|
624
|
+
spinner.start("Initializing git repository...");
|
|
625
|
+
await execa2("git", ["init"], { cwd: projectDir });
|
|
626
|
+
await execa2("git", ["add", "."], { cwd: projectDir });
|
|
627
|
+
await execa2("git", ["commit", "-m", "Initial commit from create-monorepo"], { cwd: projectDir });
|
|
628
|
+
spinner.succeed("Git repository initialized");
|
|
629
|
+
console.log("");
|
|
630
|
+
logger.success(`Project "${projectName}" created successfully!`);
|
|
631
|
+
console.log("");
|
|
632
|
+
logger.info("Next steps:");
|
|
633
|
+
console.log(` cd ${targetDir || projectName}`);
|
|
634
|
+
console.log(` ${packageManager} dev`);
|
|
635
|
+
console.log("");
|
|
636
|
+
} catch (error) {
|
|
637
|
+
logger.error(`Failed to create project: ${error instanceof Error ? error.message : String(error)}`);
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/index.ts
|
|
643
|
+
var cli = cac("create-monorepo");
|
|
644
|
+
cli.command("[dir]", "Create a new monorepo project").action(async (dir) => {
|
|
645
|
+
await createMonorepo(dir);
|
|
646
|
+
});
|
|
647
|
+
cli.help();
|
|
648
|
+
cli.version("0.1.0");
|
|
649
|
+
cli.parse();
|