@hatem427/code-guard-ci 3.2.0 → 3.4.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.
Files changed (86) hide show
  1. package/config/angular.config.ts +29 -708
  2. package/config/guidelines.config.ts +5 -130
  3. package/config/nextjs.config.ts +27 -511
  4. package/config/react.config.ts +19 -614
  5. package/dist/config/angular.config.d.ts +5 -8
  6. package/dist/config/angular.config.d.ts.map +1 -1
  7. package/dist/config/angular.config.js +28 -666
  8. package/dist/config/angular.config.js.map +1 -1
  9. package/dist/config/guidelines.config.d.ts.map +1 -1
  10. package/dist/config/guidelines.config.js +5 -127
  11. package/dist/config/guidelines.config.js.map +1 -1
  12. package/dist/config/nextjs.config.d.ts +7 -9
  13. package/dist/config/nextjs.config.d.ts.map +1 -1
  14. package/dist/config/nextjs.config.js +26 -472
  15. package/dist/config/nextjs.config.js.map +1 -1
  16. package/dist/config/react.config.d.ts +4 -5
  17. package/dist/config/react.config.d.ts.map +1 -1
  18. package/dist/config/react.config.js +19 -586
  19. package/dist/config/react.config.js.map +1 -1
  20. package/dist/scripts/auto-fix.d.ts +0 -5
  21. package/dist/scripts/auto-fix.d.ts.map +1 -1
  22. package/dist/scripts/auto-fix.js +0 -5
  23. package/dist/scripts/auto-fix.js.map +1 -1
  24. package/dist/scripts/cli.js +209 -390
  25. package/dist/scripts/cli.js.map +1 -1
  26. package/dist/scripts/config-generators/ai-config-generator.d.ts.map +1 -1
  27. package/dist/scripts/config-generators/ai-config-generator.js +71 -15
  28. package/dist/scripts/config-generators/ai-config-generator.js.map +1 -1
  29. package/dist/scripts/config-generators/eslint-generator.d.ts.map +1 -1
  30. package/dist/scripts/config-generators/eslint-generator.js +13 -625
  31. package/dist/scripts/config-generators/eslint-generator.js.map +1 -1
  32. package/dist/scripts/config-generators/index.d.ts +0 -1
  33. package/dist/scripts/config-generators/index.d.ts.map +1 -1
  34. package/dist/scripts/config-generators/index.js +1 -5
  35. package/dist/scripts/config-generators/index.js.map +1 -1
  36. package/dist/scripts/config-generators/typescript-generator.d.ts.map +1 -1
  37. package/dist/scripts/config-generators/typescript-generator.js +0 -33
  38. package/dist/scripts/config-generators/typescript-generator.js.map +1 -1
  39. package/dist/scripts/config-generators/vscode-generator.d.ts.map +1 -1
  40. package/dist/scripts/config-generators/vscode-generator.js +28 -171
  41. package/dist/scripts/config-generators/vscode-generator.js.map +1 -1
  42. package/dist/scripts/generate-pr-checklist.d.ts +0 -5
  43. package/dist/scripts/generate-pr-checklist.d.ts.map +1 -1
  44. package/dist/scripts/generate-pr-checklist.js +1 -6
  45. package/dist/scripts/generate-pr-checklist.js.map +1 -1
  46. package/dist/scripts/postinstall.js +0 -38
  47. package/dist/scripts/postinstall.js.map +1 -1
  48. package/dist/scripts/precommit-check.d.ts +0 -5
  49. package/dist/scripts/precommit-check.d.ts.map +1 -1
  50. package/dist/scripts/precommit-check.js +137 -121
  51. package/dist/scripts/precommit-check.js.map +1 -1
  52. package/dist/scripts/utils/naming-validator.d.ts.map +1 -1
  53. package/dist/scripts/utils/naming-validator.js +2 -96
  54. package/dist/scripts/utils/naming-validator.js.map +1 -1
  55. package/dist/scripts/utils/project-detector.d.ts +9 -12
  56. package/dist/scripts/utils/project-detector.d.ts.map +1 -1
  57. package/dist/scripts/utils/project-detector.js +11 -63
  58. package/dist/scripts/utils/project-detector.js.map +1 -1
  59. package/dist/scripts/utils/report-generator.js +5 -17
  60. package/dist/scripts/utils/report-generator.js.map +1 -1
  61. package/dist/scripts/utils/structure-validator.d.ts.map +1 -1
  62. package/dist/scripts/utils/structure-validator.js +0 -50
  63. package/dist/scripts/utils/structure-validator.js.map +1 -1
  64. package/package.json +1 -12
  65. package/scripts/auto-fix.ts +0 -5
  66. package/scripts/cli.ts +224 -424
  67. package/scripts/config-generators/ai-config-generator.ts +78 -28
  68. package/scripts/config-generators/eslint-generator.ts +7 -621
  69. package/scripts/config-generators/index.ts +0 -1
  70. package/scripts/config-generators/typescript-generator.ts +0 -36
  71. package/scripts/config-generators/vscode-generator.ts +40 -178
  72. package/scripts/generate-pr-checklist.ts +1 -6
  73. package/scripts/postinstall.ts +0 -38
  74. package/scripts/precommit-check.ts +143 -224
  75. package/scripts/utils/naming-validator.ts +2 -104
  76. package/scripts/utils/project-detector.ts +11 -78
  77. package/scripts/utils/report-generator.ts +5 -19
  78. package/scripts/utils/structure-validator.ts +0 -54
  79. package/config/fastify.config.ts +0 -326
  80. package/config/hono.config.ts +0 -331
  81. package/config/nestjs.config.ts +0 -500
  82. package/config/python.config.ts +0 -512
  83. package/templates/feature-doc-api.md +0 -101
  84. package/templates/feature-doc-backend.md +0 -114
  85. package/templates/feature-doc-service.md +0 -113
  86. package/templates/feature-doc-ui.md +0 -91
@@ -163,105 +163,6 @@ const frameworkNamingRules: Record<ProjectType, NamingRule[]> = {
163
163
  ],
164
164
 
165
165
  node: [...baseNamingRules],
166
-
167
- nestjs: [
168
- ...baseNamingRules,
169
- {
170
- filePattern: /\.module\.ts$/,
171
- convention: 'kebab-case',
172
- description: 'NestJS modules should use kebab-case: users.module.ts',
173
- },
174
- {
175
- filePattern: /\.controller\.(ts|spec\.ts)$/,
176
- convention: 'kebab-case',
177
- description: 'NestJS controllers should use kebab-case: users.controller.ts',
178
- },
179
- {
180
- filePattern: /\.service\.(ts|spec\.ts)$/,
181
- convention: 'kebab-case',
182
- description: 'NestJS services should use kebab-case: users.service.ts',
183
- },
184
- {
185
- filePattern: /\.dto\.ts$/,
186
- convention: 'kebab-case',
187
- description: 'NestJS DTOs should use kebab-case: create-user.dto.ts',
188
- },
189
- {
190
- filePattern: /\.guard\.ts$/,
191
- convention: 'kebab-case',
192
- description: 'NestJS guards should use kebab-case: auth.guard.ts',
193
- },
194
- {
195
- filePattern: /\.pipe\.ts$/,
196
- convention: 'kebab-case',
197
- description: 'NestJS pipes should use kebab-case: parse-id.pipe.ts',
198
- },
199
- {
200
- filePattern: /\.interceptor\.ts$/,
201
- convention: 'kebab-case',
202
- description: 'NestJS interceptors should use kebab-case: logging.interceptor.ts',
203
- },
204
- {
205
- filePattern: /\.filter\.ts$/,
206
- convention: 'kebab-case',
207
- description: 'NestJS exception filters should use kebab-case: http-exception.filter.ts',
208
- },
209
- {
210
- filePattern: /\.decorator\.ts$/,
211
- convention: 'kebab-case',
212
- description: 'NestJS decorators should use kebab-case: current-user.decorator.ts',
213
- },
214
- ],
215
-
216
- fastify: [
217
- ...baseNamingRules,
218
- {
219
- filePattern: /\.route\.ts$/,
220
- convention: 'kebab-case',
221
- description: 'Fastify route files should use kebab-case: users.route.ts',
222
- },
223
- {
224
- filePattern: /\.plugin\.ts$/,
225
- convention: 'kebab-case',
226
- description: 'Fastify plugins should use kebab-case: auth.plugin.ts',
227
- },
228
- {
229
- filePattern: /\.schema\.ts$/,
230
- convention: 'kebab-case',
231
- description: 'Schema files should use kebab-case: create-user.schema.ts',
232
- },
233
- ],
234
-
235
- hono: [
236
- ...baseNamingRules,
237
- {
238
- filePattern: /\.route\.ts$/,
239
- convention: 'kebab-case',
240
- description: 'Hono route files should use kebab-case: users.route.ts',
241
- },
242
- {
243
- filePattern: /\.handler\.ts$/,
244
- convention: 'kebab-case',
245
- description: 'Hono handler files should use kebab-case: create-user.handler.ts',
246
- },
247
- {
248
- filePattern: /\.middleware\.ts$/,
249
- convention: 'kebab-case',
250
- description: 'Hono middleware files should use kebab-case: auth.middleware.ts',
251
- },
252
- {
253
- filePattern: /\.schema\.ts$/,
254
- convention: 'kebab-case',
255
- description: 'Schema files should use kebab-case: user.schema.ts',
256
- },
257
- ],
258
-
259
- // Python frameworks use snake_case for all files
260
- python: [...baseNamingRules],
261
- django: [...baseNamingRules],
262
- fastapi: [...baseNamingRules],
263
- flask: [...baseNamingRules],
264
-
265
166
  unknown: [...baseNamingRules],
266
167
  };
267
168
 
@@ -372,12 +273,9 @@ export function validateNaming(
372
273
  }
373
274
 
374
275
  function getNameWithoutExtensions(fileName: string): string {
375
- // Remove all known suffixes before checking the convention
276
+ // Remove all known suffixes: .component.ts, .test.tsx, .service.ts, etc.
376
277
  return fileName
377
- .replace(
378
- /\.(component|service|module|pipe|directive|guard|interceptor|filter|decorator|controller|dto|handler|plugin|route|schema|test|spec|util|constants?|types?|middleware)\./g,
379
- '.'
380
- )
278
+ .replace(/\.(component|service|module|pipe|directive|guard|interceptor|test|spec|util|constants?|types?)\./g, '.')
381
279
  .replace(/\.[^.]+$/, ''); // Remove final extension
382
280
  }
383
281
 
@@ -21,14 +21,7 @@ export type ProjectType =
21
21
  | 'vue'
22
22
  | 'nuxt'
23
23
  | 'svelte'
24
- | 'nestjs'
25
- | 'fastify'
26
- | 'hono'
27
24
  | 'node'
28
- | 'django'
29
- | 'fastapi'
30
- | 'flask'
31
- | 'python'
32
25
  | 'unknown';
33
26
 
34
27
  export type MonorepoType = 'nx' | 'turborepo' | 'lerna' | 'pnpm-workspaces' | 'none';
@@ -169,17 +162,14 @@ function detectExistingTooling(dir: string, pkg: Record<string, any>): ExistingT
169
162
  * Detect the project type based on the nearest package.json.
170
163
  *
171
164
  * Priority order (first match wins):
172
- * 1. Nuxt — has `nuxt` as a dependency
173
- * 2. NextJS — has `next` as a dependency
174
- * 3. Angular — has `@angular/core` as a dependency
175
- * 4. Svelte — has `svelte` as a dependency
176
- * 5. Vue — has `vue` as a dependency
177
- * 6. React — has `react` as a dependency
178
- * 7. NestJS — has `@nestjs/core` as a dependency
179
- * 8. Fastify has `fastify` as a dependency
180
- * 9. Hono — has `hono` as a dependency
181
- * 10. Node — has `express`/`koa` or other Node.js frameworks
182
- * 11. Unknown — none of the above
165
+ * 1. Nuxt — has `nuxt` as a dependency
166
+ * 2. NextJS — has `next` as a dependency
167
+ * 3. Angular — has `@angular/core` as a dependency
168
+ * 4. Svelte — has `svelte` as a dependency
169
+ * 5. Vue — has `vue` as a dependency
170
+ * 6. React — has `react` as a dependency
171
+ * 7. Node — has `express`/`fastify`/`koa`/`nestjs`
172
+ * 8. Unknownnone of the above
183
173
  */
184
174
  export function detectProject(startDir: string = process.cwd()): DetectionResult {
185
175
  const result = findPackageJson(startDir);
@@ -242,24 +232,9 @@ export function detectProject(startDir: string = process.cwd()): DetectionResult
242
232
  return { ...base, type: 'react', label: 'React' };
243
233
  }
244
234
 
245
- // NestJS check (before generic Node)
246
- if (hasDep(pkg, '@nestjs/core')) {
247
- return { ...base, type: 'nestjs', label: 'NestJS' };
248
- }
249
-
250
- // Fastify check
251
- if (hasDep(pkg, 'fastify')) {
252
- return { ...base, type: 'fastify', label: 'Fastify' };
253
- }
254
-
255
- // Hono check
256
- if (hasDep(pkg, 'hono')) {
257
- return { ...base, type: 'hono', label: 'Hono' };
258
- }
259
-
260
- // Generic Node.js backend check (Express, Koa, etc.)
261
- if (hasDep(pkg, 'express') || hasDep(pkg, 'koa')) {
262
- return { ...base, type: 'node', label: 'Express (Node.js)' };
235
+ // Node.js backend check
236
+ if (hasDep(pkg, 'express') || hasDep(pkg, 'fastify') || hasDep(pkg, '@nestjs/core') || hasDep(pkg, 'koa')) {
237
+ return { ...base, type: 'node', label: 'Node.js Backend' };
263
238
  }
264
239
 
265
240
  // Fallback — additional heuristics
@@ -279,47 +254,5 @@ export function detectProject(startDir: string = process.cwd()): DetectionResult
279
254
  return { ...base, type: 'vue', label: 'Vue.js (heuristic)' };
280
255
  }
281
256
 
282
- // ── Python project detection ─────────────────────────────────────────────
283
- // Python projects have no package.json; detection is purely filesystem-based.
284
-
285
- // Django — has manage.py at the root (the canonical Django project marker)
286
- if (fs.existsSync(path.join(dir, 'manage.py'))) {
287
- return { ...base, type: 'django', label: 'Django' };
288
- }
289
-
290
- // FastAPI — check pyproject.toml or requirements.txt for fastapi dependency
291
- const requirementsFiles = ['requirements.txt', 'requirements-dev.txt', 'requirements/base.txt'];
292
- const hasFastApi = requirementsFiles.some((f) => {
293
- const fp = path.join(dir, f);
294
- if (!fs.existsSync(fp)) return false;
295
- const content = fs.readFileSync(fp, 'utf-8').toLowerCase();
296
- return content.includes('fastapi');
297
- });
298
- if (hasFastApi) {
299
- return { ...base, type: 'fastapi', label: 'FastAPI' };
300
- }
301
-
302
- // Flask — check requirements files for flask dependency
303
- const hasFlask = requirementsFiles.some((f) => {
304
- const fp = path.join(dir, f);
305
- if (!fs.existsSync(fp)) return false;
306
- const content = fs.readFileSync(fp, 'utf-8').toLowerCase();
307
- return content.includes('flask');
308
- });
309
- if (hasFlask) {
310
- return { ...base, type: 'flask', label: 'Flask' };
311
- }
312
-
313
- // Generic Python — has pyproject.toml, Pipfile, setup.py, or any .py files at root
314
- if (
315
- fs.existsSync(path.join(dir, 'pyproject.toml')) ||
316
- fs.existsSync(path.join(dir, 'Pipfile')) ||
317
- fs.existsSync(path.join(dir, 'setup.py')) ||
318
- fs.existsSync(path.join(dir, 'setup.cfg')) ||
319
- requirementsFiles.some((f) => fs.existsSync(path.join(dir, f)))
320
- ) {
321
- return { ...base, type: 'python', label: 'Python' };
322
- }
323
-
324
257
  return { ...base, type: 'unknown', label: 'Unknown Project' };
325
258
  }
@@ -261,11 +261,10 @@ function buildViolationSection(violations: Violation[]): string {
261
261
 
262
262
  ruleItems.forEach((v, idx) => {
263
263
  const line = v.line !== null ? String(v.line) : '—';
264
- // Extract first line only for table (before "Why:" or newline)
265
- const firstLine = v.message.split(/\n|Why:/).filter(Boolean)[0].trim();
266
- let shortMsg = firstLine
264
+ // Truncate long messages for the table, escape pipes
265
+ let shortMsg = v.message
267
266
  .replace(/\|/g, '\\|')
268
- .substring(0, 100);
267
+ .substring(0, 120);
269
268
 
270
269
  // Add quick fix hint if available
271
270
  const hint = getQuickFixHint(v.ruleId);
@@ -278,22 +277,9 @@ function buildViolationSection(violations: Violation[]): string {
278
277
 
279
278
  sections.push('');
280
279
 
281
- // Full message details with examples — shown once per rule
282
- const hasDetailedMessage = ruleItems.some(v => v.message.includes('Why:') || v.message.includes('\n'));
283
-
284
- if (hasDetailedMessage) {
285
- sections.push(`<details>`);
286
- sections.push(`<summary>💡 How to fix (click to expand)</summary>\n`);
287
- sections.push('```');
288
- // Get the full message from the first item
289
- sections.push(ruleItems[0].message);
290
- sections.push('```');
291
- sections.push(`\n</details>\n`);
292
- }
293
-
294
- // Additional solution example from solution-examples.ts
280
+ // Solution example — shown once per rule
295
281
  const solution = getSolutionExample(ruleId);
296
- if (solution && !hasDetailedMessage) {
282
+ if (solution) {
297
283
  sections.push(`<details>`);
298
284
  sections.push(`<summary>💡 How to fix (click to expand)</summary>\n`);
299
285
  sections.push(solution);
@@ -114,60 +114,6 @@ const frameworkStructures: Record<ProjectType, FolderRule[]> = {
114
114
  { path: 'src/__tests__', required: false, description: 'Test files' },
115
115
  ],
116
116
 
117
- nestjs: [
118
- { path: 'src', required: true, description: 'Source code root' },
119
- { path: 'src/modules', required: false, description: 'Feature modules (each domain gets a module)' },
120
- { path: 'src/common', required: false, description: 'Shared utilities across modules' },
121
- { path: 'src/common/guards', required: false, description: 'Global authentication/authorization guards' },
122
- { path: 'src/common/interceptors', required: false, description: 'Global interceptors (logging, response transform)' },
123
- { path: 'src/common/pipes', required: false, description: 'Global validation/transformation pipes' },
124
- { path: 'src/common/filters', required: false, description: 'Global exception filters' },
125
- { path: 'src/common/decorators', required: false, description: 'Custom parameter/class decorators' },
126
- { path: 'src/config', required: false, description: 'App configuration (ConfigModule setup)' },
127
- { path: 'test', required: false, description: 'End-to-end tests' },
128
- ],
129
-
130
- fastify: [
131
- { path: 'src', required: true, description: 'Source code root' },
132
- { path: 'src/routes', required: false, description: 'Fastify route plugins' },
133
- { path: 'src/plugins', required: false, description: 'Shared Fastify plugins (db, auth, etc.)' },
134
- { path: 'src/schemas', required: false, description: 'JSON/TypeBox schemas for validation & serialization' },
135
- { path: 'src/services', required: false, description: 'Business logic services' },
136
- { path: 'src/hooks', required: false, description: 'Shared lifecycle hooks' },
137
- { path: 'src/config', required: false, description: 'Configuration and env validation' },
138
- ],
139
-
140
- hono: [
141
- { path: 'src', required: true, description: 'Source code root' },
142
- { path: 'src/routes', required: false, description: 'Hono sub-app route files' },
143
- { path: 'src/middleware', required: false, description: 'Custom middleware functions' },
144
- { path: 'src/handlers', required: false, description: 'Route handler functions' },
145
- { path: 'src/schemas', required: false, description: 'Zod schemas for request/response validation' },
146
- { path: 'src/config', required: false, description: 'Configuration and env validation' },
147
- ],
148
-
149
- // Python project structures
150
- python: [
151
- { path: 'src', required: false, description: 'Source code root (or top-level package)' },
152
- { path: 'tests', required: false, description: 'Test suite' },
153
- ],
154
- django: [
155
- { path: 'manage.py', required: true, description: 'Django management entry point' },
156
- { path: 'tests', required: false, description: 'Test suite' },
157
- ],
158
- fastapi: [
159
- { path: 'app', required: false, description: 'FastAPI application package' },
160
- { path: 'app/routers', required: false, description: 'Route modules (APIRouter instances)' },
161
- { path: 'app/schemas', required: false, description: 'Pydantic request/response schemas' },
162
- { path: 'app/services', required: false, description: 'Business logic layer' },
163
- { path: 'tests', required: false, description: 'Test suite' },
164
- ],
165
- flask: [
166
- { path: 'app', required: false, description: 'Flask application package' },
167
- { path: 'app/blueprints', required: false, description: 'Flask Blueprint modules' },
168
- { path: 'tests', required: false, description: 'Test suite' },
169
- ],
170
-
171
117
  unknown: [...baseStructure],
172
118
  };
173
119
 
@@ -1,326 +0,0 @@
1
- /**
2
- * ============================================================================
3
- * fastify.config.ts — Fastify-specific coding rules
4
- * ============================================================================
5
- *
6
- * Registers rules that apply to Fastify projects:
7
- * - Schema-first validation enforcement
8
- * - Plugin scope correctness
9
- * - Async handler patterns
10
- * - Security: CORS, helmet, rate limiting
11
- * - Error response shape
12
- * - Hook lifecycle usage
13
- */
14
-
15
- import { registerRules, Rule } from './guidelines.config';
16
-
17
- const fastifyRules: Rule[] = [
18
-
19
- // ── Schema-First Validation ───────────────────────────────────────────────
20
-
21
- // ─────────────────────────────────────────
22
- // RULE: fastify-schema-required
23
- // ROLE: Enforce schema definitions on all routes
24
- // PURPOSE: Fastify uses JSON Schema / TypeBox for both request validation
25
- // and response serialization. Without schemas, inputs are not
26
- // validated and responses include all properties (potential data leak).
27
- // Schemas also enable Fastify's fast-json-stringify for ~2× serialization speed.
28
- // EXAMPLE:
29
- // WRONG:
30
- // fastify.post('/users', async (req, reply) => {
31
- // return userService.create(req.body);
32
- // });
33
- // RIGHT:
34
- // fastify.post('/users', {
35
- // schema: {
36
- // body: CreateUserSchema,
37
- // response: { 201: UserResponseSchema },
38
- // },
39
- // }, async (req, reply) => {
40
- // return userService.create(req.body);
41
- // });
42
- // ─────────────────────────────────────────
43
- {
44
- id: 'fastify-schema-required',
45
- label: 'Define schema for every route (body, params, response)',
46
- description:
47
- 'Every Fastify route must define a schema object with at minimum body/params validation and response schemas. This enables validation, serialization speed, and prevents data leaks.',
48
- severity: 'error',
49
- fileExtensions: ['ts', 'js'],
50
- pattern: null,
51
- customCheck: (file) => {
52
- const violations: Array<{ line: number | null; message: string }> = [];
53
-
54
- if (
55
- !file.relativePath.includes('/routes/') &&
56
- !file.relativePath.endsWith('.route.ts') &&
57
- !file.relativePath.endsWith('.routes.ts')
58
- ) {
59
- return [];
60
- }
61
-
62
- // Look for fastify.post/put/patch/delete without a schema key
63
- const routeRegex = /fastify\.\s*(?:post|put|patch|delete)\s*\(\s*['"`][^'"]+['"`]\s*,\s*async/g;
64
- let match;
65
- while ((match = routeRegex.exec(file.content)) !== null) {
66
- const near = file.content.slice(Math.max(0, match.index - 200), match.index + 50);
67
- if (!near.includes('schema')) {
68
- const lineNum = file.content.slice(0, match.index).split('\n').length;
69
- violations.push({
70
- line: lineNum,
71
- message:
72
- 'Mutating route (POST/PUT/PATCH/DELETE) is missing a schema definition. Add body validation schema and response schema.',
73
- });
74
- }
75
- }
76
-
77
- return violations;
78
- },
79
- applicableTo: ['fastify'],
80
- category: 'Validation',
81
- },
82
-
83
- // ─────────────────────────────────────────
84
- // RULE: fastify-response-schema
85
- // ROLE: Enforce response schema definitions
86
- // PURPOSE: Without a response schema, Fastify falls back to slow JSON.stringify
87
- // and may expose internal fields. Response schemas activate fast-json-stringify
88
- // and act as a serialisation allowlist.
89
- // EXAMPLE:
90
- // WRONG:
91
- // fastify.get('/users/:id', async (req, reply) => { return user; });
92
- // RIGHT:
93
- // fastify.get('/users/:id', {
94
- // schema: { response: { 200: UserResponseSchema } },
95
- // }, async (req, reply) => { return user; });
96
- // ─────────────────────────────────────────
97
- {
98
- id: 'fastify-response-schema',
99
- label: 'Always define a response schema',
100
- description:
101
- 'Define response schemas for all routes to enable fast-json-stringify serialization and prevent accidental exposure of sensitive fields.',
102
- severity: 'warning',
103
- fileExtensions: ['ts', 'js'],
104
- pattern: null,
105
- customCheck: (file) => {
106
- const violations: Array<{ line: number | null; message: string }> = [];
107
-
108
- if (
109
- !file.relativePath.includes('/routes/') &&
110
- !file.relativePath.endsWith('.route.ts') &&
111
- !file.relativePath.endsWith('.routes.ts')
112
- ) {
113
- return [];
114
- }
115
-
116
- const hasSchema = /schema\s*:\s*\{/.test(file.content);
117
- const hasResponseSchema = /response\s*:\s*\{/.test(file.content);
118
-
119
- if (hasSchema && !hasResponseSchema) {
120
- violations.push({
121
- line: null,
122
- message:
123
- 'Route schema found but no response schema defined. Add response: { 200: Schema } to enable fast serialization and prevent data leaks.',
124
- });
125
- }
126
-
127
- return violations;
128
- },
129
- applicableTo: ['fastify'],
130
- category: 'Validation',
131
- },
132
-
133
- // ── Plugin Architecture ────────────────────────────────────────────────────
134
-
135
- // ─────────────────────────────────────────
136
- // RULE: fastify-use-fastify-plugin
137
- // ROLE: Enforce plugin scope correctness
138
- // PURPOSE: In Fastify, plugins registered without fastify-plugin are
139
- // scoped to a child context — decorations, hooks, and routes
140
- // added inside won't be visible to siblings or the root instance.
141
- // Shared utilities (auth, db) must use fastify-plugin to break scope.
142
- // EXAMPLE:
143
- // WRONG:
144
- // export async function authPlugin(fastify: FastifyInstance) {
145
- // fastify.decorate('verifyToken', verifyToken);
146
- // }
147
- // RIGHT:
148
- // import fp from 'fastify-plugin';
149
- // export default fp(async function authPlugin(fastify: FastifyInstance) {
150
- // fastify.decorate('verifyToken', verifyToken);
151
- // });
152
- // ─────────────────────────────────────────
153
- {
154
- id: 'fastify-use-fastify-plugin',
155
- label: 'Shared plugins must use fastify-plugin (fp)',
156
- description:
157
- 'Plugins that add decorations, hooks, or services used by other plugins must be wrapped with fastify-plugin (fp) to share scope with the parent context.',
158
- severity: 'warning',
159
- fileExtensions: ['ts', 'js'],
160
- pattern: null,
161
- customCheck: (file) => {
162
- if (
163
- !file.relativePath.includes('/plugins/') &&
164
- !file.relativePath.endsWith('.plugin.ts') &&
165
- !file.relativePath.endsWith('.plugin.js')
166
- ) {
167
- return [];
168
- }
169
-
170
- const usesDecorate = /fastify\.decorate\s*\(/.test(file.content);
171
- const usesFastifyPlugin = /fastify-plugin|fp\s*\(/.test(file.content);
172
-
173
- if (usesDecorate && !usesFastifyPlugin) {
174
- return [
175
- {
176
- line: null,
177
- message:
178
- 'Plugin adds decorators but is not wrapped with fastify-plugin (fp). Other plugins/routes will not see these decorations.',
179
- },
180
- ];
181
- }
182
-
183
- return [];
184
- },
185
- applicableTo: ['fastify'],
186
- category: 'Architecture',
187
- },
188
-
189
- // ── Async Handler Patterns ────────────────────────────────────────────────
190
-
191
- // ─────────────────────────────────────────
192
- // RULE: fastify-async-handler
193
- // ROLE: Enforce async handler pattern
194
- // PURPOSE: Mixing reply.send() with async return values causes double-send
195
- // errors in Fastify. Async handlers should return the value;
196
- // Fastify handles serialization. Use reply.send() OR return, not both.
197
- // EXAMPLE:
198
- // WRONG:
199
- // fastify.get('/users', async (req, reply) => {
200
- // const users = await userService.findAll();
201
- // reply.send(users); // + implicit return → double send!
202
- // });
203
- // RIGHT:
204
- // fastify.get('/users', async (req, reply) => {
205
- // return userService.findAll(); // Fastify handles send automatically
206
- // });
207
- // ─────────────────────────────────────────
208
- {
209
- id: 'fastify-async-handler',
210
- label: 'Return values from async handlers — avoid reply.send() + return',
211
- description:
212
- 'In async Fastify handlers, return the response value directly. Calling reply.send() AND returning a value causes a double-send error.',
213
- severity: 'error',
214
- fileExtensions: ['ts', 'js'],
215
- pattern: null,
216
- customCheck: (file) => {
217
- const violations: Array<{ line: number | null; message: string }> = [];
218
-
219
- if (
220
- !file.relativePath.includes('/routes/') &&
221
- !file.relativePath.endsWith('.route.ts')
222
- ) {
223
- return [];
224
- }
225
-
226
- for (let i = 0; i < file.lines.length; i++) {
227
- if (/reply\.send\s*\(/.test(file.lines[i])) {
228
- // Check if there's a return after send on same or next line
229
- const nearby = file.lines.slice(i, i + 3).join(' ');
230
- if (/reply\.send[^;]*;\s*(?:return|})/.test(nearby)) {
231
- violations.push({
232
- line: i + 1,
233
- message:
234
- 'reply.send() followed by implicit return in async handler. Use: return myValue; instead of reply.send(myValue).',
235
- });
236
- }
237
- }
238
- }
239
-
240
- return violations;
241
- },
242
- applicableTo: ['fastify'],
243
- category: 'Async Patterns',
244
- },
245
-
246
- // ── Security ──────────────────────────────────────────────────────────────
247
-
248
- // ─────────────────────────────────────────
249
- // RULE: fastify-use-helmet
250
- // ROLE: Enforce security headers
251
- // PURPOSE: @fastify/helmet sets a baseline of secure HTTP headers.
252
- // Without it, Fastify serves responses without X-Frame-Options,
253
- // Content-Security-Policy, or HSTS headers.
254
- // EXAMPLE:
255
- // WRONG:
256
- // const fastify = Fastify();
257
- // fastify.register(cors);
258
- // RIGHT:
259
- // const fastify = Fastify();
260
- // fastify.register(helmet);
261
- // fastify.register(cors, { origin: allowedOrigins });
262
- // ─────────────────────────────────────────
263
- {
264
- id: 'fastify-use-helmet',
265
- label: 'Register @fastify/helmet for security headers',
266
- description:
267
- 'Register @fastify/helmet to add baseline security headers (HSTS, X-Frame-Options, CSP, etc.) to all responses.',
268
- severity: 'error',
269
- fileExtensions: ['ts', 'js'],
270
- pattern: null,
271
- customCheck: (file) => {
272
- if (
273
- !file.relativePath.endsWith('server.ts') &&
274
- !file.relativePath.endsWith('app.ts') &&
275
- !file.relativePath.endsWith('index.ts')
276
- ) {
277
- return [];
278
- }
279
-
280
- const hasFastify = /Fastify\s*\(|fastify\s*\(\s*\)/.test(file.content);
281
- const hasHelmet = /helmet/.test(file.content);
282
-
283
- if (hasFastify && !hasHelmet) {
284
- return [
285
- {
286
- line: null,
287
- message:
288
- 'Fastify server missing @fastify/helmet. Register it: fastify.register(import("@fastify/helmet")).',
289
- },
290
- ];
291
- }
292
-
293
- return [];
294
- },
295
- applicableTo: ['fastify'],
296
- category: 'Security',
297
- },
298
-
299
- // ─────────────────────────────────────────
300
- // RULE: fastify-cors-options
301
- // ROLE: Enforce CORS configuration
302
- // PURPOSE: Registering @fastify/cors without an origin restriction allows
303
- // cross-origin requests from any domain, defeating CORS entirely.
304
- // EXAMPLE:
305
- // WRONG:
306
- // fastify.register(cors); // allows all origins
307
- // RIGHT:
308
- // fastify.register(cors, { origin: ['https://myapp.com'] });
309
- // ─────────────────────────────────────────
310
- {
311
- id: 'fastify-cors-options',
312
- label: '@fastify/cors must restrict origin',
313
- description:
314
- 'Do not register @fastify/cors without an explicit origin allowlist. An unrestricted CORS policy allows any website to make authenticated requests to your API.',
315
- severity: 'warning',
316
- fileExtensions: ['ts', 'js'],
317
- pattern: /register\s*\(\s*cors\s*\)/g,
318
- applicableTo: ['fastify'],
319
- category: 'Security',
320
- },
321
- ];
322
-
323
- // Register all Fastify-specific rules
324
- registerRules('fastify', fastifyRules);
325
-
326
- export { fastifyRules };