@yoms/create-monorepo 1.0.3 → 1.2.0
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/dist/index.js +22 -7
- package/package.json +14 -15
- package/templates/backend-hono/base/AGENT.md +326 -0
package/dist/index.js
CHANGED
|
@@ -460,6 +460,10 @@ async function mergeEnvFile(baseDir, additionsPath) {
|
|
|
460
460
|
|
|
461
461
|
// src/generators/backend.generator.ts
|
|
462
462
|
var __dirname = path4.dirname(fileURLToPath(import.meta.url));
|
|
463
|
+
var getPackageRoot = () => {
|
|
464
|
+
const dirname = __dirname;
|
|
465
|
+
return path4.resolve(dirname, "..");
|
|
466
|
+
};
|
|
463
467
|
var BackendGenerator = class extends BaseGenerator {
|
|
464
468
|
config;
|
|
465
469
|
constructor(options) {
|
|
@@ -475,17 +479,18 @@ var BackendGenerator = class extends BaseGenerator {
|
|
|
475
479
|
__HAS_REDIS__: String(this.config.includeRedis),
|
|
476
480
|
__HAS_SMTP__: String(this.config.includeSmtp)
|
|
477
481
|
});
|
|
482
|
+
const packageRoot = getPackageRoot();
|
|
478
483
|
let templatePath;
|
|
479
484
|
if (this.config.type === "web") {
|
|
480
|
-
templatePath = path4.join(
|
|
485
|
+
templatePath = path4.join(packageRoot, "templates", `backend-${this.config.framework}`, "base");
|
|
481
486
|
} else if (this.config.type === "worker") {
|
|
482
|
-
templatePath = path4.join(
|
|
487
|
+
templatePath = path4.join(packageRoot, "templates", "backend-worker", "base");
|
|
483
488
|
} else {
|
|
484
|
-
templatePath = path4.join(
|
|
489
|
+
templatePath = path4.join(packageRoot, "templates", "backend-cli", "base");
|
|
485
490
|
}
|
|
486
491
|
await copyDirRecursive(templatePath, backendDir, tokens);
|
|
487
492
|
if (this.config.type === "web") {
|
|
488
|
-
const featuresPath = path4.join(
|
|
493
|
+
const featuresPath = path4.join(packageRoot, "templates", `backend-${this.config.framework}`, "features");
|
|
489
494
|
if (this.config.database) {
|
|
490
495
|
const dbFeaturePath = path4.join(featuresPath, `${this.config.database}-prisma`);
|
|
491
496
|
await mergeFeature(backendDir, dbFeaturePath, tokens);
|
|
@@ -510,6 +515,10 @@ var BackendGenerator = class extends BaseGenerator {
|
|
|
510
515
|
import path5 from "path";
|
|
511
516
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
512
517
|
var __dirname2 = path5.dirname(fileURLToPath2(import.meta.url));
|
|
518
|
+
var getPackageRoot2 = () => {
|
|
519
|
+
const dirname = __dirname2;
|
|
520
|
+
return path5.resolve(dirname, "..");
|
|
521
|
+
};
|
|
513
522
|
var FrontendGenerator = class extends BaseGenerator {
|
|
514
523
|
config;
|
|
515
524
|
constructor(options) {
|
|
@@ -522,7 +531,8 @@ var FrontendGenerator = class extends BaseGenerator {
|
|
|
522
531
|
const tokens = this.getTokens({
|
|
523
532
|
__API_URL__: this.config.apiUrl
|
|
524
533
|
});
|
|
525
|
-
const
|
|
534
|
+
const packageRoot = getPackageRoot2();
|
|
535
|
+
const templatePath = path5.join(packageRoot, "templates", "frontend-nextjs", "base");
|
|
526
536
|
await copyDirRecursive(templatePath, frontendDir, tokens);
|
|
527
537
|
}
|
|
528
538
|
};
|
|
@@ -531,12 +541,17 @@ var FrontendGenerator = class extends BaseGenerator {
|
|
|
531
541
|
import path6 from "path";
|
|
532
542
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
533
543
|
var __dirname3 = path6.dirname(fileURLToPath3(import.meta.url));
|
|
544
|
+
var getPackageRoot3 = () => {
|
|
545
|
+
const dirname = __dirname3;
|
|
546
|
+
return path6.resolve(dirname, "..");
|
|
547
|
+
};
|
|
534
548
|
var SharedGenerator = class extends BaseGenerator {
|
|
535
549
|
async generate() {
|
|
536
550
|
const sharedDir = path6.join(this.options.projectDir, "packages", "shared");
|
|
537
551
|
await ensureDir(sharedDir);
|
|
538
552
|
const tokens = this.getTokens();
|
|
539
|
-
const
|
|
553
|
+
const packageRoot = getPackageRoot3();
|
|
554
|
+
const templatePath = path6.join(packageRoot, "templates", "shared", "base");
|
|
540
555
|
await copyDirRecursive(templatePath, sharedDir, tokens);
|
|
541
556
|
}
|
|
542
557
|
};
|
|
@@ -641,7 +656,7 @@ async function createMonorepo(targetDir) {
|
|
|
641
656
|
|
|
642
657
|
// src/index.ts
|
|
643
658
|
var cli = cac("create-monorepo");
|
|
644
|
-
cli.command("[dir]", "Create a
|
|
659
|
+
cli.command("[dir]", "Create a new monorepo project").action(async (dir) => {
|
|
645
660
|
await createMonorepo(dir);
|
|
646
661
|
});
|
|
647
662
|
cli.help();
|
package/package.json
CHANGED
|
@@ -1,24 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yoms/create-monorepo",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "CLI tool to scaffold monorepo projects from templates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"create-monorepo": "./dist/index.js"
|
|
8
8
|
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"dev": "tsx src/index.ts",
|
|
11
|
-
"build": "tsup src/index.ts --format esm --clean",
|
|
12
|
-
"typecheck": "tsc --noEmit",
|
|
13
|
-
"lint": "eslint src --ext .ts",
|
|
14
|
-
"format": "prettier --write \"src/**/*.ts\"",
|
|
15
|
-
"test:generate": "tsx scripts/test-generate.ts",
|
|
16
|
-
"test:validate": "tsx scripts/validate-templates.ts",
|
|
17
|
-
"changeset": "changeset",
|
|
18
|
-
"version": "changeset version",
|
|
19
|
-
"release": "pnpm build && changeset publish",
|
|
20
|
-
"prepublishOnly": "pnpm build && pnpm typecheck"
|
|
21
|
-
},
|
|
22
9
|
"keywords": [
|
|
23
10
|
"monorepo",
|
|
24
11
|
"template",
|
|
@@ -70,5 +57,17 @@
|
|
|
70
57
|
},
|
|
71
58
|
"engines": {
|
|
72
59
|
"node": ">=18.0.0"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"dev": "tsx src/index.ts",
|
|
63
|
+
"build": "tsup src/index.ts --format esm --clean",
|
|
64
|
+
"typecheck": "tsc --noEmit",
|
|
65
|
+
"lint": "eslint src --ext .ts",
|
|
66
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
67
|
+
"test:generate": "tsx scripts/test-generate.ts",
|
|
68
|
+
"test:validate": "tsx scripts/validate-templates.ts",
|
|
69
|
+
"changeset": "changeset",
|
|
70
|
+
"version": "changeset version && pnpm install --lockfile-only",
|
|
71
|
+
"release": "pnpm build && changeset publish"
|
|
73
72
|
}
|
|
74
|
-
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# AGENT.md - Backend API
|
|
2
|
+
|
|
3
|
+
This file provides guidance to AI agents (Claude, Cursor, etc.) when working with this backend API.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
This is a Hono-based backend API in a monorepo workspace. It's designed for high performance, full type safety, and production-ready patterns.
|
|
8
|
+
|
|
9
|
+
## Tech Stack
|
|
10
|
+
|
|
11
|
+
- **Framework**: Hono (lightweight, fast, type-safe)
|
|
12
|
+
- **Language**: TypeScript with strict mode
|
|
13
|
+
- **Runtime**: Node.js (compatible with Bun, Deno, Cloudflare Workers)
|
|
14
|
+
- **Validation**: Zod schemas
|
|
15
|
+
- **Logger**: Winston with structured logging
|
|
16
|
+
- **Build Tool**: tsup (fast ESM bundler)
|
|
17
|
+
- **Testing**: Vitest with coverage
|
|
18
|
+
|
|
19
|
+
## Development Commands
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Development
|
|
23
|
+
pnpm dev # Start with hot reload
|
|
24
|
+
pnpm build # Build for production
|
|
25
|
+
pnpm start # Run production build
|
|
26
|
+
pnpm test # Run tests
|
|
27
|
+
pnpm test:watch # Watch mode
|
|
28
|
+
pnpm test:coverage # Coverage report
|
|
29
|
+
pnpm typecheck # Type checking
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Project Structure
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
src/
|
|
36
|
+
├── config/ # Configuration (env, logger, db)
|
|
37
|
+
├── lib/ # Utilities and helpers
|
|
38
|
+
│ ├── errors.ts # Custom error classes
|
|
39
|
+
│ └── response.ts # Response helpers
|
|
40
|
+
├── middleware/ # Hono middleware
|
|
41
|
+
│ ├── cors.middleware.ts
|
|
42
|
+
│ ├── error.middleware.ts
|
|
43
|
+
│ ├── logger.middleware.ts
|
|
44
|
+
│ └── rate-limit.middleware.ts
|
|
45
|
+
├── routes/ # API route handlers
|
|
46
|
+
│ └── health.route.ts
|
|
47
|
+
├── services/ # Business logic (if database enabled)
|
|
48
|
+
├── types/ # TypeScript types
|
|
49
|
+
└── index.ts # App entry point
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Architecture Patterns
|
|
53
|
+
|
|
54
|
+
### Error Handling
|
|
55
|
+
|
|
56
|
+
Use custom error classes for consistent error responses:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { NotFoundError, BadRequestError } from '../lib/errors.js';
|
|
60
|
+
|
|
61
|
+
// Throw custom errors - they're automatically handled
|
|
62
|
+
throw new NotFoundError('User not found');
|
|
63
|
+
throw new BadRequestError('Invalid email format');
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Available errors:
|
|
67
|
+
- `BadRequestError` (400)
|
|
68
|
+
- `UnauthorizedError` (401)
|
|
69
|
+
- `ForbiddenError` (403)
|
|
70
|
+
- `NotFoundError` (404)
|
|
71
|
+
- `ConflictError` (409)
|
|
72
|
+
- `ValidationError` (422)
|
|
73
|
+
- `TooManyRequestsError` (429)
|
|
74
|
+
- `InternalServerError` (500)
|
|
75
|
+
|
|
76
|
+
### Response Helpers
|
|
77
|
+
|
|
78
|
+
Use standardized response helpers:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { success, created, noContent, paginated } from '../lib/response.js';
|
|
82
|
+
|
|
83
|
+
// Success response (200)
|
|
84
|
+
return success(c, { id: '123', name: 'John' });
|
|
85
|
+
|
|
86
|
+
// Created response (201)
|
|
87
|
+
return created(c, newUser);
|
|
88
|
+
|
|
89
|
+
// No content (204)
|
|
90
|
+
return noContent(c);
|
|
91
|
+
|
|
92
|
+
// Paginated response
|
|
93
|
+
return paginated(c, users, page, limit, total);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Route Structure
|
|
97
|
+
|
|
98
|
+
Follow this pattern for new routes:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { Hono } from 'hono';
|
|
102
|
+
import { zValidator } from '@hono/zod-validator';
|
|
103
|
+
import { success } from '../lib/response.js';
|
|
104
|
+
import { CreateItemSchema } from '@__PROJECT_NAME__/shared';
|
|
105
|
+
|
|
106
|
+
const items = new Hono();
|
|
107
|
+
|
|
108
|
+
// List items with pagination
|
|
109
|
+
items.get('/', zValidator('query', paginationSchema), async (c) => {
|
|
110
|
+
const { page, limit } = c.req.valid('query');
|
|
111
|
+
const result = await ItemService.getAll(page, limit);
|
|
112
|
+
return paginated(c, result.items, page, limit, result.total);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Get by ID
|
|
116
|
+
items.get('/:id', async (c) => {
|
|
117
|
+
const id = c.req.param('id');
|
|
118
|
+
const item = await ItemService.getById(id);
|
|
119
|
+
return success(c, item);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Create
|
|
123
|
+
items.post('/', zValidator('json', CreateItemSchema), async (c) => {
|
|
124
|
+
const data = c.req.valid('json');
|
|
125
|
+
const item = await ItemService.create(data);
|
|
126
|
+
return created(c, item);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export { items };
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Service Layer Pattern
|
|
133
|
+
|
|
134
|
+
Create services for business logic:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import { prisma } from '../config/database.js';
|
|
138
|
+
import { NotFoundError, ConflictError } from '../lib/errors.js';
|
|
139
|
+
|
|
140
|
+
export class ItemService {
|
|
141
|
+
static async getAll(page = 1, limit = 10) {
|
|
142
|
+
const skip = (page - 1) * limit;
|
|
143
|
+
const [items, total] = await Promise.all([
|
|
144
|
+
prisma.item.findMany({ skip, take: limit }),
|
|
145
|
+
prisma.item.count(),
|
|
146
|
+
]);
|
|
147
|
+
return { items, total, page, limit };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static async getById(id: string) {
|
|
151
|
+
const item = await prisma.item.findUnique({ where: { id } });
|
|
152
|
+
if (!item) throw new NotFoundError(`Item ${id} not found`);
|
|
153
|
+
return item;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static async create(data: CreateItem) {
|
|
157
|
+
// Check for duplicates
|
|
158
|
+
const existing = await prisma.item.findUnique({ where: { name: data.name } });
|
|
159
|
+
if (existing) throw new ConflictError('Item already exists');
|
|
160
|
+
|
|
161
|
+
return prisma.item.create({ data });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Environment Variables
|
|
167
|
+
|
|
168
|
+
All env vars are validated with Zod in `src/config/env.ts`. Add new variables:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
const envSchema = z.object({
|
|
172
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
173
|
+
PORT: z.coerce.number().default(3001),
|
|
174
|
+
// Add your vars here
|
|
175
|
+
NEW_VAR: z.string().min(1),
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Rate Limiting
|
|
180
|
+
|
|
181
|
+
Apply rate limiting to routes:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { rateLimits } from '../middleware/rate-limit.middleware.js';
|
|
185
|
+
|
|
186
|
+
// Use preset limits
|
|
187
|
+
app.use('/api/*', rateLimits.standard); // 100 req/min
|
|
188
|
+
|
|
189
|
+
// Custom limit
|
|
190
|
+
app.use('/auth/login', rateLimit({
|
|
191
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
192
|
+
max: 5,
|
|
193
|
+
message: 'Too many login attempts',
|
|
194
|
+
}));
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Testing
|
|
198
|
+
|
|
199
|
+
Write tests for all routes and services:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import { describe, it, expect } from 'vitest';
|
|
203
|
+
import { Hono } from 'hono';
|
|
204
|
+
import { items } from '../items.route.js';
|
|
205
|
+
|
|
206
|
+
describe('Items Route', () => {
|
|
207
|
+
const app = new Hono();
|
|
208
|
+
app.route('/items', items);
|
|
209
|
+
|
|
210
|
+
it('should list items', async () => {
|
|
211
|
+
const res = await app.request('/items?page=1&limit=10');
|
|
212
|
+
expect(res.status).toBe(200);
|
|
213
|
+
|
|
214
|
+
const json = await res.json();
|
|
215
|
+
expect(json.success).toBe(true);
|
|
216
|
+
expect(json.data).toBeInstanceOf(Array);
|
|
217
|
+
expect(json.pagination).toBeDefined();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Database (if enabled)
|
|
223
|
+
|
|
224
|
+
### Prisma Commands
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
pnpm prisma:generate # Generate Prisma client
|
|
228
|
+
pnpm prisma:migrate # Create and run migrations
|
|
229
|
+
pnpm prisma:studio # Open Prisma Studio UI
|
|
230
|
+
pnpm prisma:push # Push schema to database
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Creating Migrations
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# After editing schema.prisma
|
|
237
|
+
pnpm prisma:migrate
|
|
238
|
+
# Enter migration name when prompted
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Type Sharing
|
|
242
|
+
|
|
243
|
+
Import shared types from the shared package:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { User, CreateUser, UpdateUser } from '@__PROJECT_NAME__/shared';
|
|
247
|
+
import { UserSchema } from '@__PROJECT_NAME__/shared';
|
|
248
|
+
|
|
249
|
+
// Use schemas for validation
|
|
250
|
+
const result = UserSchema.parse(data);
|
|
251
|
+
|
|
252
|
+
// Use types for type safety
|
|
253
|
+
const user: User = await getUser(id);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Best Practices
|
|
257
|
+
|
|
258
|
+
### DO:
|
|
259
|
+
- ✅ Use custom error classes, not raw `throw new Error()`
|
|
260
|
+
- ✅ Use response helpers for all routes
|
|
261
|
+
- ✅ Validate all input with Zod schemas
|
|
262
|
+
- ✅ Put business logic in services, not routes
|
|
263
|
+
- ✅ Write tests for new features
|
|
264
|
+
- ✅ Use type-safe imports from shared package
|
|
265
|
+
- ✅ Log important operations with structured logging
|
|
266
|
+
- ✅ Handle async operations properly with try/catch
|
|
267
|
+
|
|
268
|
+
### DON'T:
|
|
269
|
+
- ❌ Don't return raw `c.json()` - use response helpers
|
|
270
|
+
- ❌ Don't put business logic in routes - use services
|
|
271
|
+
- ❌ Don't use `any` type - maintain type safety
|
|
272
|
+
- ❌ Don't skip validation - always validate input
|
|
273
|
+
- ❌ Don't commit `.env` files - use `.env.example`
|
|
274
|
+
- ❌ Don't use blocking operations in async handlers
|
|
275
|
+
- ❌ Don't modify shared types here - edit in `packages/shared`
|
|
276
|
+
|
|
277
|
+
## Common Tasks
|
|
278
|
+
|
|
279
|
+
### Adding a New Route
|
|
280
|
+
|
|
281
|
+
1. Create route file in `src/routes/`
|
|
282
|
+
2. Create service in `src/services/` (if needed)
|
|
283
|
+
3. Add validation schemas in `packages/shared/src/schemas/`
|
|
284
|
+
4. Register route in `src/index.ts`
|
|
285
|
+
5. Write tests in `src/routes/__tests__/`
|
|
286
|
+
|
|
287
|
+
### Adding Middleware
|
|
288
|
+
|
|
289
|
+
1. Create in `src/middleware/`
|
|
290
|
+
2. Export middleware function
|
|
291
|
+
3. Apply in `src/index.ts` with `app.use()`
|
|
292
|
+
|
|
293
|
+
### Adding Environment Variables
|
|
294
|
+
|
|
295
|
+
1. Add to `.env.example`
|
|
296
|
+
2. Add to `src/config/env.ts` schema
|
|
297
|
+
3. Use via `env.YOUR_VAR`
|
|
298
|
+
|
|
299
|
+
## Debugging
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// Use structured logging
|
|
303
|
+
import { logger } from './config/logger.js';
|
|
304
|
+
|
|
305
|
+
logger.info('User logged in', { userId: user.id });
|
|
306
|
+
logger.warn('Rate limit approaching', { ip, count });
|
|
307
|
+
logger.error('Database error', { error, query });
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Performance Tips
|
|
311
|
+
|
|
312
|
+
- Use database indexes for frequently queried fields
|
|
313
|
+
- Use pagination for list endpoints
|
|
314
|
+
- Cache expensive operations with Redis (if enabled)
|
|
315
|
+
- Use `prisma.$transaction()` for multiple related operations
|
|
316
|
+
- Profile with `pnpm test:coverage` to find slow tests
|
|
317
|
+
|
|
318
|
+
## Security Checklist
|
|
319
|
+
|
|
320
|
+
- [ ] All inputs are validated with Zod
|
|
321
|
+
- [ ] Rate limiting is applied to auth endpoints
|
|
322
|
+
- [ ] Sensitive data is not logged
|
|
323
|
+
- [ ] Database queries use parameterized queries (Prisma does this)
|
|
324
|
+
- [ ] CORS is configured correctly
|
|
325
|
+
- [ ] Environment variables are validated on startup
|
|
326
|
+
- [ ] Error messages don't leak sensitive info in production
|