@levironexe/architect 0.1.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/CHANGELOG.md +32 -0
- package/CONTRIBUTING.md +55 -0
- package/README.md +341 -0
- package/dist/analyzers/ast-parser.d.ts +3 -0
- package/dist/analyzers/ast-parser.js +305 -0
- package/dist/analyzers/ast-parser.js.map +1 -0
- package/dist/analyzers/dependency-graph.d.ts +2 -0
- package/dist/analyzers/dependency-graph.js +67 -0
- package/dist/analyzers/dependency-graph.js.map +1 -0
- package/dist/analyzers/duplication.d.ts +2 -0
- package/dist/analyzers/duplication.js +56 -0
- package/dist/analyzers/duplication.js.map +1 -0
- package/dist/analyzers/file-walker.d.ts +3 -0
- package/dist/analyzers/file-walker.js +80 -0
- package/dist/analyzers/file-walker.js.map +1 -0
- package/dist/cli/context-runner.d.ts +1 -0
- package/dist/cli/context-runner.js +16 -0
- package/dist/cli/context-runner.js.map +1 -0
- package/dist/cli/index.d.ts +24 -0
- package/dist/cli/index.js +217 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init-runner.d.ts +25 -0
- package/dist/cli/init-runner.js +152 -0
- package/dist/cli/init-runner.js.map +1 -0
- package/dist/cli/scan-runner.d.ts +8 -0
- package/dist/cli/scan-runner.js +133 -0
- package/dist/cli/scan-runner.js.map +1 -0
- package/dist/formatters/plan-json.d.ts +2 -0
- package/dist/formatters/plan-json.js +4 -0
- package/dist/formatters/plan-json.js.map +1 -0
- package/dist/formatters/plan-markdown.d.ts +2 -0
- package/dist/formatters/plan-markdown.js +42 -0
- package/dist/formatters/plan-markdown.js.map +1 -0
- package/dist/formatters/plan-prompt.d.ts +4 -0
- package/dist/formatters/plan-prompt.js +5 -0
- package/dist/formatters/plan-prompt.js.map +1 -0
- package/dist/formatters/plan-terminal.d.ts +5 -0
- package/dist/formatters/plan-terminal.js +62 -0
- package/dist/formatters/plan-terminal.js.map +1 -0
- package/dist/generators/blueprint-renderer.d.ts +3 -0
- package/dist/generators/blueprint-renderer.js +27 -0
- package/dist/generators/blueprint-renderer.js.map +1 -0
- package/dist/generators/claudeWriter.d.ts +3 -0
- package/dist/generators/claudeWriter.js +9 -0
- package/dist/generators/claudeWriter.js.map +1 -0
- package/dist/generators/copilotWriter.d.ts +3 -0
- package/dist/generators/copilotWriter.js +11 -0
- package/dist/generators/copilotWriter.js.map +1 -0
- package/dist/generators/cursorWriter.d.ts +3 -0
- package/dist/generators/cursorWriter.js +14 -0
- package/dist/generators/cursorWriter.js.map +1 -0
- package/dist/generators/genericWriter.d.ts +3 -0
- package/dist/generators/genericWriter.js +9 -0
- package/dist/generators/genericWriter.js.map +1 -0
- package/dist/generators/template-context.d.ts +18 -0
- package/dist/generators/template-context.js +126 -0
- package/dist/generators/template-context.js.map +1 -0
- package/dist/generators/templateRenderer.d.ts +2 -0
- package/dist/generators/templateRenderer.js +19 -0
- package/dist/generators/templateRenderer.js.map +1 -0
- package/dist/generators/windsurfWriter.d.ts +3 -0
- package/dist/generators/windsurfWriter.js +14 -0
- package/dist/generators/windsurfWriter.js.map +1 -0
- package/dist/generators/writer-types.d.ts +11 -0
- package/dist/generators/writer-types.js +40 -0
- package/dist/generators/writer-types.js.map +1 -0
- package/dist/llm/claude-provider.d.ts +8 -0
- package/dist/llm/claude-provider.js +22 -0
- package/dist/llm/claude-provider.js.map +1 -0
- package/dist/llm/concern-classifier.d.ts +15 -0
- package/dist/llm/concern-classifier.js +61 -0
- package/dist/llm/concern-classifier.js.map +1 -0
- package/dist/llm/config.d.ts +11 -0
- package/dist/llm/config.js +120 -0
- package/dist/llm/config.js.map +1 -0
- package/dist/llm/ollama-provider.d.ts +8 -0
- package/dist/llm/ollama-provider.js +27 -0
- package/dist/llm/ollama-provider.js.map +1 -0
- package/dist/llm/openai-provider.d.ts +8 -0
- package/dist/llm/openai-provider.js +19 -0
- package/dist/llm/openai-provider.js.map +1 -0
- package/dist/llm/prompt-builder.d.ts +12 -0
- package/dist/llm/prompt-builder.js +132 -0
- package/dist/llm/prompt-builder.js.map +1 -0
- package/dist/llm/provider.d.ts +17 -0
- package/dist/llm/provider.js +2 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/llm/response-parser.d.ts +6 -0
- package/dist/llm/response-parser.js +128 -0
- package/dist/llm/response-parser.js.map +1 -0
- package/dist/planner/plan-generator.d.ts +7 -0
- package/dist/planner/plan-generator.js +275 -0
- package/dist/planner/plan-generator.js.map +1 -0
- package/dist/planner/plan-prompt-builder.d.ts +9 -0
- package/dist/planner/plan-prompt-builder.js +92 -0
- package/dist/planner/plan-prompt-builder.js.map +1 -0
- package/dist/planner/plan-response-parser.d.ts +7 -0
- package/dist/planner/plan-response-parser.js +21 -0
- package/dist/planner/plan-response-parser.js.map +1 -0
- package/dist/planner/plan-validator.d.ts +3 -0
- package/dist/planner/plan-validator.js +49 -0
- package/dist/planner/plan-validator.js.map +1 -0
- package/dist/reporters/scan-json.d.ts +13 -0
- package/dist/reporters/scan-json.js +26 -0
- package/dist/reporters/scan-json.js.map +1 -0
- package/dist/reporters/terminal.d.ts +6 -0
- package/dist/reporters/terminal.js +224 -0
- package/dist/reporters/terminal.js.map +1 -0
- package/dist/scoring/consistency-score.d.ts +3 -0
- package/dist/scoring/consistency-score.js +23 -0
- package/dist/scoring/consistency-score.js.map +1 -0
- package/dist/scoring/duplication-score.d.ts +3 -0
- package/dist/scoring/duplication-score.js +16 -0
- package/dist/scoring/duplication-score.js.map +1 -0
- package/dist/scoring/health-score.d.ts +4 -0
- package/dist/scoring/health-score.js +20 -0
- package/dist/scoring/health-score.js.map +1 -0
- package/dist/scoring/issue-builder.d.ts +4 -0
- package/dist/scoring/issue-builder.js +62 -0
- package/dist/scoring/issue-builder.js.map +1 -0
- package/dist/scoring/modularity-score.d.ts +3 -0
- package/dist/scoring/modularity-score.js +56 -0
- package/dist/scoring/modularity-score.js.map +1 -0
- package/dist/scoring/pattern-analysis.d.ts +3 -0
- package/dist/scoring/pattern-analysis.js +74 -0
- package/dist/scoring/pattern-analysis.js.map +1 -0
- package/dist/scoring/separation-score.d.ts +3 -0
- package/dist/scoring/separation-score.js +35 -0
- package/dist/scoring/separation-score.js.map +1 -0
- package/dist/skills/detector.d.ts +4 -0
- package/dist/skills/detector.js +104 -0
- package/dist/skills/detector.js.map +1 -0
- package/dist/skills/lister.d.ts +9 -0
- package/dist/skills/lister.js +35 -0
- package/dist/skills/lister.js.map +1 -0
- package/dist/skills/loader.d.ts +6 -0
- package/dist/skills/loader.js +76 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/structure-check.d.ts +2 -0
- package/dist/skills/structure-check.js +37 -0
- package/dist/skills/structure-check.js.map +1 -0
- package/dist/skills/validator.d.ts +6 -0
- package/dist/skills/validator.js +229 -0
- package/dist/skills/validator.js.map +1 -0
- package/dist/types/analysis.d.ts +130 -0
- package/dist/types/analysis.js +41 -0
- package/dist/types/analysis.js.map +1 -0
- package/dist/types/concern.d.ts +48 -0
- package/dist/types/concern.js +16 -0
- package/dist/types/concern.js.map +1 -0
- package/dist/types/generation.d.ts +32 -0
- package/dist/types/generation.js +2 -0
- package/dist/types/generation.js.map +1 -0
- package/dist/types/issue.d.ts +12 -0
- package/dist/types/issue.js +2 -0
- package/dist/types/issue.js.map +1 -0
- package/dist/types/pattern.d.ts +15 -0
- package/dist/types/pattern.js +2 -0
- package/dist/types/pattern.js.map +1 -0
- package/dist/types/plan.d.ts +56 -0
- package/dist/types/plan.js +2 -0
- package/dist/types/plan.js.map +1 -0
- package/dist/types/scan-output.d.ts +84 -0
- package/dist/types/scan-output.js +2 -0
- package/dist/types/scan-output.js.map +1 -0
- package/dist/types/scoring.d.ts +15 -0
- package/dist/types/scoring.js +2 -0
- package/dist/types/scoring.js.map +1 -0
- package/dist/types/skill.d.ts +97 -0
- package/dist/types/skill.js +2 -0
- package/dist/types/skill.js.map +1 -0
- package/dist/utils/agent-detector.d.ts +2 -0
- package/dist/utils/agent-detector.js +22 -0
- package/dist/utils/agent-detector.js.map +1 -0
- package/dist/utils/interactive.d.ts +6 -0
- package/dist/utils/interactive.js +15 -0
- package/dist/utils/interactive.js.map +1 -0
- package/dist/utils/path.d.ts +5 -0
- package/dist/utils/path.js +31 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/progress.d.ts +17 -0
- package/dist/utils/progress.js +48 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/utils/thresholds.d.ts +6 -0
- package/dist/utils/thresholds.js +48 -0
- package/dist/utils/thresholds.js.map +1 -0
- package/package.json +63 -0
- package/skills/meta/general-js.skill.yaml +131 -0
- package/skills/patterns/clerk-auth.skill.yaml +349 -0
- package/skills/patterns/docker-deploy.skill.yaml +214 -0
- package/skills/patterns/drizzle.skill.yaml +277 -0
- package/skills/patterns/mongoose.skill.yaml +290 -0
- package/skills/patterns/nextauth.skill.yaml +308 -0
- package/skills/patterns/playwright-e2e.skill.yaml +265 -0
- package/skills/patterns/prisma.skill.yaml +255 -0
- package/skills/patterns/s3-storage.skill.yaml +235 -0
- package/skills/patterns/selenium-e2e.skill.yaml +276 -0
- package/skills/patterns/supabase-auth.skill.yaml +298 -0
- package/skills/patterns/supabase.skill.yaml +304 -0
- package/skills/patterns/vercel-deploy.skill.yaml +219 -0
- package/skills/patterns/vitest-testing.skill.yaml +262 -0
- package/skills/stacks/express-api.skill.yaml +155 -0
- package/skills/stacks/fastify-api.skill.yaml +119 -0
- package/skills/stacks/hono-api.skill.yaml +130 -0
- package/skills/stacks/nestjs.skill.yaml +135 -0
- package/skills/stacks/nextjs-app-router.skill.yaml +176 -0
- package/skills/stacks/react-spa.skill.yaml +153 -0
- package/skills/stacks/vue-nuxt.skill.yaml +115 -0
- package/templates/architect-plan.md +139 -0
- package/templates/architect-refactor.md +119 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
schema_version: "2.0.0"
|
|
2
|
+
id: vitest-testing
|
|
3
|
+
name: "Vitest"
|
|
4
|
+
version: "2.0.0"
|
|
5
|
+
description: "Unit and integration testing with Vitest — native ESM, vi.mock() module replacement, coverage thresholds, async assertion patterns, and test behavior verification over implementation details."
|
|
6
|
+
category: pattern
|
|
7
|
+
language: javascript
|
|
8
|
+
frameworks:
|
|
9
|
+
- vitest
|
|
10
|
+
dependencies:
|
|
11
|
+
none:
|
|
12
|
+
- jest
|
|
13
|
+
- "@jest/globals"
|
|
14
|
+
detection:
|
|
15
|
+
dependencies:
|
|
16
|
+
any:
|
|
17
|
+
- vitest
|
|
18
|
+
source_indicators:
|
|
19
|
+
- "from 'vitest'"
|
|
20
|
+
- "vi.fn("
|
|
21
|
+
- "vi.mock("
|
|
22
|
+
- "describe("
|
|
23
|
+
- "vi.spyOn("
|
|
24
|
+
structure:
|
|
25
|
+
required_dirs:
|
|
26
|
+
- path: src/__tests__
|
|
27
|
+
purpose: "Unit tests co-located near source — mirrors the src/ structure. Test files are named [filename].test.ts where [filename] matches the source file they cover. E.g., src/services/user.service.ts → src/__tests__/services/user.service.test.ts. This makes it immediately obvious which tests cover which source file."
|
|
28
|
+
recommended_dirs:
|
|
29
|
+
- path: tests/integration
|
|
30
|
+
purpose: "Integration tests that span multiple modules or require a real database connection. Use a test database (test schema or separate DB) isolated from development data. Do not mock the database in integration tests — the point is to test the full stack together."
|
|
31
|
+
- path: tests/fixtures
|
|
32
|
+
purpose: "Shared test factories, mock data objects, and test utilities imported by multiple test files. Examples: createTestUser() factory for Prisma, mockHttpServer using MSW for API mocking, shared vitest setup files that configure global mocks."
|
|
33
|
+
separation:
|
|
34
|
+
rules:
|
|
35
|
+
- concern: unit_test_location
|
|
36
|
+
belongs_in: src/__tests__
|
|
37
|
+
rule_text: "Unit tests live in src/__tests__/ and mirror the source structure. Test files are named [source-filename].test.ts. Each test file imports exactly the module it tests — not its entire dependency tree. The goal is to test the public API of each module in isolation."
|
|
38
|
+
example: |
|
|
39
|
+
// src/__tests__/services/user.service.test.ts
|
|
40
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
41
|
+
import { UserService } from '@/services/user.service';
|
|
42
|
+
|
|
43
|
+
// Mock the repository — unit tests don't hit the database
|
|
44
|
+
vi.mock('@/repositories/user.repository', () => ({
|
|
45
|
+
findUserByEmail: vi.fn(),
|
|
46
|
+
createUser: vi.fn(),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
describe('UserService', () => {
|
|
50
|
+
beforeEach(() => vi.clearAllMocks());
|
|
51
|
+
|
|
52
|
+
it('should throw if email already exists', async () => {
|
|
53
|
+
const { findUserByEmail } = await import('@/repositories/user.repository');
|
|
54
|
+
vi.mocked(findUserByEmail).mockResolvedValue({ id: '1', email: 'a@b.com' } as any);
|
|
55
|
+
await expect(UserService.register('a@b.com', 'password')).rejects.toThrow('Email already in use');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
indicators:
|
|
59
|
+
- ".test.ts"
|
|
60
|
+
- ".spec.ts"
|
|
61
|
+
- "vi.mock("
|
|
62
|
+
- concern: mocking
|
|
63
|
+
belongs_in: src/__tests__
|
|
64
|
+
rule_text: "Use vi.mock() to mock modules at module load time — call it at the top level (hoisted). Use vi.fn() for function mocks with mockReturnValue/mockResolvedValue to control behavior. Reset all mocks in beforeEach with vi.clearAllMocks() to prevent state leaking between tests."
|
|
65
|
+
example: |
|
|
66
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
67
|
+
import { sendEmail } from '@/lib/email';
|
|
68
|
+
import { notifyUser } from '@/services/notification.service';
|
|
69
|
+
|
|
70
|
+
// vi.mock() is hoisted — runs before imports
|
|
71
|
+
vi.mock('@/lib/email', () => ({
|
|
72
|
+
sendEmail: vi.fn().mockResolvedValue({ messageId: 'test-123' }),
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
describe('notifyUser', () => {
|
|
76
|
+
beforeEach(() => vi.clearAllMocks()); // reset call counts and return values
|
|
77
|
+
|
|
78
|
+
it('sends an email to the user', async () => {
|
|
79
|
+
await notifyUser('user@example.com', 'Welcome!');
|
|
80
|
+
expect(sendEmail).toHaveBeenCalledOnce();
|
|
81
|
+
expect(sendEmail).toHaveBeenCalledWith('user@example.com', expect.objectContaining({ subject: 'Welcome!' }));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('does not send if user has opted out', async () => {
|
|
85
|
+
await notifyUser('optout@example.com', 'Welcome!', { optedOut: true });
|
|
86
|
+
expect(sendEmail).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
indicators:
|
|
90
|
+
- "vi.mock("
|
|
91
|
+
- "vi.fn("
|
|
92
|
+
- "vi.spyOn("
|
|
93
|
+
- concern: coverage
|
|
94
|
+
belongs_in: vitest.config.ts
|
|
95
|
+
rule_text: "Set coverage thresholds in vitest.config.ts to enforce minimum coverage. Use @vitest/coverage-v8 as the provider (faster, native). Include/exclude patterns control which files are measured. CI fails if thresholds are not met — protecting against coverage regression."
|
|
96
|
+
example: |
|
|
97
|
+
// vitest.config.ts
|
|
98
|
+
import { defineConfig } from 'vitest/config';
|
|
99
|
+
import path from 'path';
|
|
100
|
+
|
|
101
|
+
export default defineConfig({
|
|
102
|
+
test: {
|
|
103
|
+
globals: true,
|
|
104
|
+
environment: 'node',
|
|
105
|
+
setupFiles: ['./tests/setup.ts'],
|
|
106
|
+
coverage: {
|
|
107
|
+
provider: 'v8',
|
|
108
|
+
reporter: ['text', 'json', 'html'],
|
|
109
|
+
include: ['src/**/*.ts'],
|
|
110
|
+
exclude: ['src/**/*.d.ts', 'src/**/index.ts', 'src/types/**'],
|
|
111
|
+
thresholds: {
|
|
112
|
+
lines: 80,
|
|
113
|
+
functions: 80,
|
|
114
|
+
branches: 70,
|
|
115
|
+
statements: 80,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
resolve: {
|
|
120
|
+
alias: { '@': path.resolve(__dirname, './src') },
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
indicators:
|
|
124
|
+
- "vitest.config.ts"
|
|
125
|
+
- "coverage:"
|
|
126
|
+
- "thresholds:"
|
|
127
|
+
- concern: async_testing
|
|
128
|
+
belongs_in: src/__tests__
|
|
129
|
+
rule_text: "Always await async assertions and use the correct Vitest assertion for async operations. Use expect(...).rejects.toThrow() for expected errors. Always return or await the assertion chain — unawaited assertions pass even when they should fail."
|
|
130
|
+
example: |
|
|
131
|
+
import { it, expect, vi } from 'vitest';
|
|
132
|
+
import { getUser } from '@/services/user.service';
|
|
133
|
+
|
|
134
|
+
it('returns null for unknown user', async () => {
|
|
135
|
+
const user = await getUser('nonexistent-id');
|
|
136
|
+
expect(user).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('throws for invalid ID format', async () => {
|
|
140
|
+
// ✓ Must await rejects assertions
|
|
141
|
+
await expect(getUser('not-a-uuid')).rejects.toThrow('Invalid user ID');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('resolves with user data', async () => {
|
|
145
|
+
const user = await getUser('valid-uuid-123');
|
|
146
|
+
expect(user).toMatchObject({ id: 'valid-uuid-123', email: expect.any(String) });
|
|
147
|
+
});
|
|
148
|
+
indicators:
|
|
149
|
+
- "await expect("
|
|
150
|
+
- ".rejects."
|
|
151
|
+
- ".resolves."
|
|
152
|
+
patterns:
|
|
153
|
+
data_flow:
|
|
154
|
+
direction: "Test → vi.mock() (module replacement) → Module Under Test → Assertion"
|
|
155
|
+
rules:
|
|
156
|
+
- "vi.mock() replaces modules at load time — mock the dependencies, not the module under test."
|
|
157
|
+
- "vi.clearAllMocks() in beforeEach ensures each test starts with clean mock state."
|
|
158
|
+
- "Test observable behavior (return values, thrown errors, side effects) not internal state."
|
|
159
|
+
- "Integration tests in tests/integration/ use real dependencies — no mocking."
|
|
160
|
+
- "Coverage thresholds in vitest.config.ts — CI fails if coverage drops below threshold."
|
|
161
|
+
error_handling:
|
|
162
|
+
recommended: "For expected errors, use await expect(fn()).rejects.toThrow(). Always await the assertion — an unawaited .rejects check never runs the assertion and passes silently."
|
|
163
|
+
naming:
|
|
164
|
+
unit_tests: "src/__tests__/[path]/[module].test.ts — mirrors source path"
|
|
165
|
+
integration_tests: "tests/integration/[feature].integration.test.ts"
|
|
166
|
+
fixtures: "tests/fixtures/[resource].factory.ts — factory functions returning test data"
|
|
167
|
+
setup: "tests/setup.ts — global beforeAll/afterAll hooks (DB connection, etc.)"
|
|
168
|
+
anti_patterns:
|
|
169
|
+
- id: jest_api_in_vitest
|
|
170
|
+
severity: warning
|
|
171
|
+
description: "Using Jest APIs (jest.fn(), jest.mock(), @jest/globals) in a Vitest project. Jest and Vitest have similar but distinct APIs — jest.mock() does not work in Vitest, and jest.fn() creates an incompatible mock object."
|
|
172
|
+
bad_example: |
|
|
173
|
+
// ❌ Jest API in a Vitest project — jest is undefined at runtime
|
|
174
|
+
import { jest } from '@jest/globals';
|
|
175
|
+
const fn = jest.fn();
|
|
176
|
+
jest.mock('../lib/db');
|
|
177
|
+
jest.spyOn(console, 'error');
|
|
178
|
+
good_example: |
|
|
179
|
+
// ✓ Vitest API — same patterns, correct module
|
|
180
|
+
import { vi } from 'vitest';
|
|
181
|
+
const fn = vi.fn();
|
|
182
|
+
vi.mock('../lib/db');
|
|
183
|
+
vi.spyOn(console, 'error');
|
|
184
|
+
- id: no_coverage_threshold
|
|
185
|
+
severity: warning
|
|
186
|
+
description: "Configuring coverage reporting without thresholds. Coverage numbers are displayed but no CI failure is triggered when they drop. Coverage silently regresses to 0% over time without developer awareness."
|
|
187
|
+
bad_example: |
|
|
188
|
+
// vitest.config.ts — coverage runs but thresholds not enforced
|
|
189
|
+
coverage: {
|
|
190
|
+
provider: 'v8',
|
|
191
|
+
reporter: ['text'],
|
|
192
|
+
// Missing: thresholds
|
|
193
|
+
}
|
|
194
|
+
good_example: |
|
|
195
|
+
coverage: {
|
|
196
|
+
provider: 'v8',
|
|
197
|
+
reporter: ['text', 'html'],
|
|
198
|
+
thresholds: { lines: 80, functions: 80, branches: 70 },
|
|
199
|
+
}
|
|
200
|
+
- id: shared_mock_state
|
|
201
|
+
severity: warning
|
|
202
|
+
description: "Not calling vi.clearAllMocks() between tests — mock call counts and return values from one test persist into the next test. A mock that returns a value in test 1 will return the same value in test 2 even if the test doesn't configure it, causing false passes."
|
|
203
|
+
bad_example: |
|
|
204
|
+
// ❌ Mock state leaks between tests — no clearAllMocks
|
|
205
|
+
it('test 1', () => { mockFn.mockReturnValue('hello'); mockFn(); });
|
|
206
|
+
it('test 2', () => {
|
|
207
|
+
// mockFn still has call count = 1 and return value 'hello' from test 1
|
|
208
|
+
expect(mockFn).not.toHaveBeenCalled(); // FALSE POSITIVE — passes incorrectly
|
|
209
|
+
});
|
|
210
|
+
good_example: |
|
|
211
|
+
// ✓ Clean mock state for every test
|
|
212
|
+
beforeEach(() => vi.clearAllMocks()); // resets all mock state before each test
|
|
213
|
+
- id: testing_implementation_details
|
|
214
|
+
severity: warning
|
|
215
|
+
description: "Testing internal state, private methods, or how a function does its work instead of what it returns. Tests tied to implementation break on every refactor even when behavior is correct — they add maintenance burden without adding safety."
|
|
216
|
+
bad_example: |
|
|
217
|
+
// ❌ Testing internal implementation — breaks on refactor
|
|
218
|
+
it('should call _formatEmail before sending', () => {
|
|
219
|
+
const spy = vi.spyOn(service, '_formatEmail' as any);
|
|
220
|
+
service.sendWelcomeEmail('user@example.com');
|
|
221
|
+
expect(spy).toHaveBeenCalled(); // testing HOW, not WHAT
|
|
222
|
+
});
|
|
223
|
+
good_example: |
|
|
224
|
+
// ✓ Test observable behavior — what the function produces
|
|
225
|
+
it('sends a welcome email with the correct subject', async () => {
|
|
226
|
+
await service.sendWelcomeEmail('user@example.com');
|
|
227
|
+
expect(sendEmail).toHaveBeenCalledWith('user@example.com', expect.objectContaining({
|
|
228
|
+
subject: 'Welcome to the platform',
|
|
229
|
+
}));
|
|
230
|
+
});
|
|
231
|
+
- id: async_test_without_await
|
|
232
|
+
severity: critical
|
|
233
|
+
description: "Not awaiting async assertions in tests. Without await, expect(...).rejects.toThrow() returns a Promise that is ignored — the test passes even if the function throws synchronously or doesn't throw at all."
|
|
234
|
+
bad_example: |
|
|
235
|
+
// ❌ Unawaited assertion — always passes regardless of behavior
|
|
236
|
+
it('should throw for invalid input', () => {
|
|
237
|
+
// No await — this Promise is created and immediately discarded
|
|
238
|
+
expect(getUser('invalid')).rejects.toThrow(); // always passes!
|
|
239
|
+
});
|
|
240
|
+
good_example: |
|
|
241
|
+
// ✓ Await the assertion — test fails if the function doesn't throw
|
|
242
|
+
it('should throw for invalid input', async () => {
|
|
243
|
+
await expect(getUser('invalid')).rejects.toThrow('Invalid user ID');
|
|
244
|
+
});
|
|
245
|
+
- id: snapshot_overuse
|
|
246
|
+
severity: warning
|
|
247
|
+
description: "Using toMatchSnapshot() for business logic assertions — snapshot tests hide intent (the snapshot is not readable), break on irrelevant UI/format changes, and make it impossible to understand what a test is verifying without looking up the snapshot file."
|
|
248
|
+
bad_example: |
|
|
249
|
+
// ❌ Snapshot for business logic — what does this test?
|
|
250
|
+
it('should calculate order total', () => {
|
|
251
|
+
const order = calculateOrder([{ price: 10, qty: 2 }, { price: 5, qty: 1 }]);
|
|
252
|
+
expect(order).toMatchSnapshot(); // snapshot: { total: 25, items: [...] }
|
|
253
|
+
// When the snapshot fails, is it a bug or an intentional change?
|
|
254
|
+
});
|
|
255
|
+
good_example: |
|
|
256
|
+
// ✓ Explicit assertions — intent is immediately clear
|
|
257
|
+
it('should calculate order total with correct subtotals', () => {
|
|
258
|
+
const order = calculateOrder([{ price: 10, qty: 2 }, { price: 5, qty: 1 }]);
|
|
259
|
+
expect(order.total).toBe(25);
|
|
260
|
+
expect(order.items).toHaveLength(2);
|
|
261
|
+
expect(order.items[0].subtotal).toBe(20);
|
|
262
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
schema_version: "2.0.0"
|
|
2
|
+
id: express-api
|
|
3
|
+
name: "Express.js REST API"
|
|
4
|
+
version: "1.1.0"
|
|
5
|
+
description: "Layered Express REST API with routing, request handling, business logic, data access, middleware, configuration, and utilities separated."
|
|
6
|
+
category: stack
|
|
7
|
+
language: javascript
|
|
8
|
+
frameworks:
|
|
9
|
+
- express
|
|
10
|
+
detection:
|
|
11
|
+
dependencies:
|
|
12
|
+
any:
|
|
13
|
+
- express
|
|
14
|
+
none:
|
|
15
|
+
- next
|
|
16
|
+
source_indicators:
|
|
17
|
+
- "express()"
|
|
18
|
+
- "app.listen"
|
|
19
|
+
- "router.get"
|
|
20
|
+
- "router.post"
|
|
21
|
+
structure:
|
|
22
|
+
required_dirs:
|
|
23
|
+
- path: src/routes
|
|
24
|
+
purpose: "Route definitions organized by resource."
|
|
25
|
+
- path: src/controllers
|
|
26
|
+
purpose: "Request handlers that receive HTTP input, call services, and return responses."
|
|
27
|
+
- path: src/services
|
|
28
|
+
purpose: "Business logic with no HTTP request or response awareness."
|
|
29
|
+
- path: src/models
|
|
30
|
+
purpose: "Data models and database interactions."
|
|
31
|
+
- path: src/middleware
|
|
32
|
+
purpose: "Cross-cutting request behavior such as auth, validation, and error handling."
|
|
33
|
+
recommended_dirs:
|
|
34
|
+
- path: src/config
|
|
35
|
+
purpose: "Environment and application configuration."
|
|
36
|
+
- path: src/utils
|
|
37
|
+
purpose: "Small shared helpers with no framework coupling."
|
|
38
|
+
- path: src/validators
|
|
39
|
+
purpose: "Request and domain validation schemas."
|
|
40
|
+
separation:
|
|
41
|
+
rules:
|
|
42
|
+
- concern: routing
|
|
43
|
+
belongs_in: src/routes
|
|
44
|
+
rule_text: "Route files define HTTP endpoints and delegate request flow to controllers. They should not contain persistence or business rules."
|
|
45
|
+
example: |
|
|
46
|
+
router.get('/users', UserController.list);
|
|
47
|
+
router.post('/users', UserController.create);
|
|
48
|
+
indicators:
|
|
49
|
+
- "router.get"
|
|
50
|
+
- "router.post"
|
|
51
|
+
- "app.get"
|
|
52
|
+
- concern: request_handling
|
|
53
|
+
belongs_in: src/controllers
|
|
54
|
+
rule_text: "Controllers translate req/res objects into plain inputs for services and shape the HTTP response from service results."
|
|
55
|
+
example: |
|
|
56
|
+
export async function create(req, res) {
|
|
57
|
+
const user = await userService.create(req.body);
|
|
58
|
+
res.status(201).json(user);
|
|
59
|
+
}
|
|
60
|
+
indicators:
|
|
61
|
+
- "req.body"
|
|
62
|
+
- "res.json"
|
|
63
|
+
- "res.status"
|
|
64
|
+
- concern: business_logic
|
|
65
|
+
belongs_in: src/services
|
|
66
|
+
rule_text: "Services contain business rules and orchestration. They accept plain data and return plain data without touching HTTP objects."
|
|
67
|
+
example: |
|
|
68
|
+
export async function createUser(input) {
|
|
69
|
+
await validateUser(input);
|
|
70
|
+
return userModel.create(input);
|
|
71
|
+
}
|
|
72
|
+
anti_indicators:
|
|
73
|
+
- "req."
|
|
74
|
+
- "res."
|
|
75
|
+
- "next("
|
|
76
|
+
- concern: data_access
|
|
77
|
+
belongs_in: src/models
|
|
78
|
+
rule_text: "Models and repositories own database queries and persistence details so higher layers stay storage-agnostic."
|
|
79
|
+
example: |
|
|
80
|
+
export async function createUserRecord(data) {
|
|
81
|
+
return prisma.user.create({ data });
|
|
82
|
+
}
|
|
83
|
+
indicators:
|
|
84
|
+
- "prisma."
|
|
85
|
+
- ".findOne"
|
|
86
|
+
- ".create("
|
|
87
|
+
- "SELECT"
|
|
88
|
+
patterns:
|
|
89
|
+
error_handling:
|
|
90
|
+
recommended: "Use centralized error-handling middleware."
|
|
91
|
+
data_flow:
|
|
92
|
+
direction: "Route -> Controller -> Service -> Model"
|
|
93
|
+
rules:
|
|
94
|
+
- "Routes call controllers."
|
|
95
|
+
- "Controllers call services."
|
|
96
|
+
- "Services do not import request or response objects."
|
|
97
|
+
naming:
|
|
98
|
+
files: "Use kebab-case for all file names. Suffix by layer: users.route.ts, users.controller.ts, users.service.ts, users.model.ts, auth.middleware.ts."
|
|
99
|
+
controllers: "[resource].controller.ts"
|
|
100
|
+
services: "[resource].service.ts"
|
|
101
|
+
anti_patterns:
|
|
102
|
+
- id: god_file
|
|
103
|
+
severity: critical
|
|
104
|
+
description: "Single file mixes routes, data access, validation, and business logic."
|
|
105
|
+
bad_example: |
|
|
106
|
+
app.post('/users', async (req, res) => {
|
|
107
|
+
const hash = await bcrypt.hash(req.body.password, 10);
|
|
108
|
+
const user = await db.query('INSERT INTO users ...');
|
|
109
|
+
res.status(201).json(user);
|
|
110
|
+
});
|
|
111
|
+
good_example: |
|
|
112
|
+
// src/routes/users.ts — route only delegates
|
|
113
|
+
router.post('/users', UserController.create);
|
|
114
|
+
|
|
115
|
+
// src/controllers/users.controller.ts — translates HTTP to plain data
|
|
116
|
+
export async function create(req: Request, res: Response) {
|
|
117
|
+
const user = await userService.createUser(req.body);
|
|
118
|
+
res.status(201).json(user);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/services/users.service.ts — business rules, no HTTP awareness
|
|
122
|
+
export async function createUser(input: CreateUserDTO) {
|
|
123
|
+
const hash = await bcrypt.hash(input.password, 10);
|
|
124
|
+
return userModel.create({ ...input, password: hash });
|
|
125
|
+
}
|
|
126
|
+
- id: raw_sql_in_routes
|
|
127
|
+
severity: warning
|
|
128
|
+
description: "SQL queries appear directly in route handlers."
|
|
129
|
+
bad_example: |
|
|
130
|
+
router.get('/users', async (req, res) => {
|
|
131
|
+
const rows = await db.query('SELECT * FROM users');
|
|
132
|
+
res.json(rows);
|
|
133
|
+
});
|
|
134
|
+
good_example: |
|
|
135
|
+
router.get('/users', UserController.list);
|
|
136
|
+
- id: hardcoded_secrets
|
|
137
|
+
severity: critical
|
|
138
|
+
description: "Secrets or credentials are hardcoded in source files."
|
|
139
|
+
bad_example: |
|
|
140
|
+
const jwtSecret = 'super-secret-key';
|
|
141
|
+
good_example: |
|
|
142
|
+
// src/config/index.ts — validated at startup, never scattered across the app
|
|
143
|
+
import { z } from 'zod';
|
|
144
|
+
const env = z.object({
|
|
145
|
+
JWT_SECRET: z.string().min(32),
|
|
146
|
+
DATABASE_URL: z.string().url(),
|
|
147
|
+
}).parse(process.env);
|
|
148
|
+
export const config = {
|
|
149
|
+
auth: { jwtSecret: env.JWT_SECRET },
|
|
150
|
+
db: { url: env.DATABASE_URL },
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// In middleware — reads from config, not from process.env
|
|
154
|
+
import { config } from '@/config';
|
|
155
|
+
const jwtSecret = config.auth.jwtSecret;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
schema_version: "2.0.0"
|
|
2
|
+
id: fastify-api
|
|
3
|
+
name: "Fastify API"
|
|
4
|
+
version: "1.1.0"
|
|
5
|
+
description: "High-performance Fastify API with plugin architecture, TypeBox schema validation, and decorator patterns."
|
|
6
|
+
category: stack
|
|
7
|
+
language: javascript
|
|
8
|
+
frameworks:
|
|
9
|
+
- fastify
|
|
10
|
+
detection:
|
|
11
|
+
dependencies:
|
|
12
|
+
any:
|
|
13
|
+
- fastify
|
|
14
|
+
none:
|
|
15
|
+
- express
|
|
16
|
+
- next
|
|
17
|
+
- hono
|
|
18
|
+
source_indicators:
|
|
19
|
+
- "fastify()"
|
|
20
|
+
- "Fastify()"
|
|
21
|
+
- "fastify.register"
|
|
22
|
+
structure:
|
|
23
|
+
required_dirs:
|
|
24
|
+
- path: src/routes
|
|
25
|
+
purpose: "Route definitions registered as Fastify plugins — one file per resource."
|
|
26
|
+
- path: src/services
|
|
27
|
+
purpose: "Business logic with no Fastify imports — receives plain data, returns plain data."
|
|
28
|
+
- path: src/schemas
|
|
29
|
+
purpose: "TypeBox JSON schemas for request/response validation shared between routes and types."
|
|
30
|
+
- path: src/plugins
|
|
31
|
+
purpose: "Fastify plugins — auth, DB connections, decorators, and shared utilities."
|
|
32
|
+
recommended_dirs:
|
|
33
|
+
- path: src/types
|
|
34
|
+
purpose: "Shared TypeScript interfaces and custom error types."
|
|
35
|
+
- path: src/lib
|
|
36
|
+
purpose: "Client singletons such as database connections."
|
|
37
|
+
separation:
|
|
38
|
+
rules:
|
|
39
|
+
- concern: schema_validation
|
|
40
|
+
belongs_in: src/schemas
|
|
41
|
+
rule_text: "Define all request/response schemas using TypeBox in src/schemas/. Register them on routes using the schema option. Never skip schema validation on public endpoints."
|
|
42
|
+
example: |
|
|
43
|
+
// src/schemas/user.schema.ts
|
|
44
|
+
import { Type } from '@sinclair/typebox';
|
|
45
|
+
export const CreateUserBody = Type.Object({
|
|
46
|
+
email: Type.String({ format: 'email' }),
|
|
47
|
+
password: Type.String({ minLength: 8 }),
|
|
48
|
+
});
|
|
49
|
+
indicators:
|
|
50
|
+
- "Type.Object"
|
|
51
|
+
- "@sinclair/typebox"
|
|
52
|
+
- concern: plugins
|
|
53
|
+
belongs_in: src/plugins
|
|
54
|
+
rule_text: "Encapsulate cross-cutting concerns as Fastify plugins. Register DB clients, auth decorators, and shared utilities as plugins — not inline in route handlers. Use fastify-plugin (fp) to avoid scope encapsulation so decorators are visible across the entire app."
|
|
55
|
+
example: |
|
|
56
|
+
// src/plugins/db.ts
|
|
57
|
+
import fp from 'fastify-plugin';
|
|
58
|
+
export default fp(async (fastify) => {
|
|
59
|
+
fastify.decorate('db', drizzle(connection));
|
|
60
|
+
});
|
|
61
|
+
indicators:
|
|
62
|
+
- "fastify-plugin"
|
|
63
|
+
- "fp("
|
|
64
|
+
- "fastify.decorate"
|
|
65
|
+
- "fastify.register"
|
|
66
|
+
- concern: services
|
|
67
|
+
belongs_in: src/services
|
|
68
|
+
rule_text: "Services must not import Fastify types (FastifyRequest, FastifyReply). They receive plain objects and return plain data."
|
|
69
|
+
example: |
|
|
70
|
+
// src/services/users.service.ts
|
|
71
|
+
export async function createUser(data: CreateUserDTO) {
|
|
72
|
+
return db.insert(users).values(data).returning();
|
|
73
|
+
}
|
|
74
|
+
anti_indicators:
|
|
75
|
+
- "FastifyRequest"
|
|
76
|
+
- "FastifyReply"
|
|
77
|
+
patterns:
|
|
78
|
+
data_flow:
|
|
79
|
+
direction: "Route Plugin → Service → Data Access"
|
|
80
|
+
rules:
|
|
81
|
+
- "Route plugins define schemas and delegate to services."
|
|
82
|
+
- "Services contain business logic with no Fastify imports."
|
|
83
|
+
- "Plugins manage cross-cutting concerns like auth and DB connections."
|
|
84
|
+
error_handling:
|
|
85
|
+
recommended: "Use Fastify's setErrorHandler for centralized error handling."
|
|
86
|
+
naming:
|
|
87
|
+
routes: "[resource].route.ts"
|
|
88
|
+
services: "[resource].service.ts"
|
|
89
|
+
schemas: "[resource].schema.ts"
|
|
90
|
+
anti_patterns:
|
|
91
|
+
- id: no_schema
|
|
92
|
+
severity: critical
|
|
93
|
+
description: "Defining Fastify routes without a schema object loses type safety, automatic serialization, and OpenAPI doc generation."
|
|
94
|
+
bad_example: |
|
|
95
|
+
// ❌ No schema — no type safety or serialization
|
|
96
|
+
fastify.post('/users', async (req, reply) => { ... });
|
|
97
|
+
good_example: |
|
|
98
|
+
// ✓ Schema-validated route
|
|
99
|
+
fastify.post('/users', { schema: { body: CreateUserBody } }, handler);
|
|
100
|
+
- id: business_logic_in_handler
|
|
101
|
+
severity: critical
|
|
102
|
+
description: "Putting business rules and DB queries directly in route handlers instead of services."
|
|
103
|
+
bad_example: |
|
|
104
|
+
fastify.post('/users', async (req) => {
|
|
105
|
+
const hash = await bcrypt.hash(req.body.password, 10);
|
|
106
|
+
return fastify.db.users.create({ ...req.body, password: hash });
|
|
107
|
+
});
|
|
108
|
+
good_example: |
|
|
109
|
+
fastify.post('/users', { schema: { body: CreateUserBody } }, async (req) => {
|
|
110
|
+
return userService.createUser(req.body);
|
|
111
|
+
});
|
|
112
|
+
- id: fastify_type_in_service
|
|
113
|
+
severity: warning
|
|
114
|
+
description: "Importing FastifyRequest or FastifyReply in service files couples business logic to the HTTP framework."
|
|
115
|
+
bad_example: |
|
|
116
|
+
import type { FastifyRequest } from 'fastify';
|
|
117
|
+
export function handleUser(req: FastifyRequest) { ... }
|
|
118
|
+
good_example: |
|
|
119
|
+
export function handleUser(data: CreateUserDTO) { ... }
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
schema_version: "2.0.0"
|
|
2
|
+
id: hono-api
|
|
3
|
+
name: "Hono API"
|
|
4
|
+
version: "1.1.0"
|
|
5
|
+
description: "Lightweight, edge-first Hono API with Zod validation, platform-agnostic handlers, and middleware chaining."
|
|
6
|
+
category: stack
|
|
7
|
+
language: javascript
|
|
8
|
+
frameworks:
|
|
9
|
+
- hono
|
|
10
|
+
detection:
|
|
11
|
+
dependencies:
|
|
12
|
+
any:
|
|
13
|
+
- hono
|
|
14
|
+
none:
|
|
15
|
+
- express
|
|
16
|
+
- fastify
|
|
17
|
+
- "@nestjs/core"
|
|
18
|
+
source_indicators:
|
|
19
|
+
- "new Hono()"
|
|
20
|
+
- "from 'hono'"
|
|
21
|
+
- "from \"hono\""
|
|
22
|
+
structure:
|
|
23
|
+
required_dirs:
|
|
24
|
+
- path: src/routes
|
|
25
|
+
purpose: "Route definitions grouped by resource — registered on the Hono app instance."
|
|
26
|
+
- path: src/services
|
|
27
|
+
purpose: "Business logic with no Hono or platform-specific imports."
|
|
28
|
+
- path: src/middleware
|
|
29
|
+
purpose: "Reusable Hono middleware — auth, logging, CORS, error handling."
|
|
30
|
+
recommended_dirs:
|
|
31
|
+
- path: src/schemas
|
|
32
|
+
purpose: "Zod schemas for request/response validation."
|
|
33
|
+
- path: src/types
|
|
34
|
+
purpose: "Shared TypeScript interfaces and error types."
|
|
35
|
+
- path: src/lib
|
|
36
|
+
purpose: "Singleton instances (database connections, KV clients) initialized once and reused across the app. Environment parsing and config validation live here. No route definitions or business logic."
|
|
37
|
+
separation:
|
|
38
|
+
rules:
|
|
39
|
+
- concern: routing
|
|
40
|
+
belongs_in: src/routes
|
|
41
|
+
rule_text: "Route files define HTTP endpoints and call services. Use Hono's zValidator middleware for input validation. No business logic or DB queries in route handlers."
|
|
42
|
+
indicators:
|
|
43
|
+
- "new Hono()"
|
|
44
|
+
- ".get("
|
|
45
|
+
- ".post("
|
|
46
|
+
- "zValidator"
|
|
47
|
+
example: |
|
|
48
|
+
// src/routes/users.ts
|
|
49
|
+
import { Hono } from 'hono';
|
|
50
|
+
import { zValidator } from '@hono/zod-validator';
|
|
51
|
+
import { createUserSchema } from '../schemas/user.schema';
|
|
52
|
+
import { userService } from '../services/users.service';
|
|
53
|
+
const users = new Hono();
|
|
54
|
+
users.post('/', zValidator('json', createUserSchema), async (c) => {
|
|
55
|
+
const data = c.req.valid('json');
|
|
56
|
+
return c.json(await userService.create(data), 201);
|
|
57
|
+
});
|
|
58
|
+
- concern: platform_agnostic_handlers
|
|
59
|
+
belongs_in: src/services
|
|
60
|
+
rule_text: "Services must not import Hono context types (Context, HonoRequest). They receive plain data and return plain data — this keeps them portable across Cloudflare Workers, Node.js, and Bun."
|
|
61
|
+
example: |
|
|
62
|
+
// src/services/users.service.ts
|
|
63
|
+
export async function createUser(data: CreateUserDTO) {
|
|
64
|
+
return db.insert(users).values(data);
|
|
65
|
+
}
|
|
66
|
+
anti_indicators:
|
|
67
|
+
- "Context"
|
|
68
|
+
- "HonoRequest"
|
|
69
|
+
- "c.req"
|
|
70
|
+
- concern: validation
|
|
71
|
+
belongs_in: src/schemas
|
|
72
|
+
rule_text: "Define Zod schemas in src/schemas/ and use @hono/zod-validator to validate request bodies. Never manually parse and validate req.json() in handlers."
|
|
73
|
+
example: |
|
|
74
|
+
// src/schemas/user.schema.ts
|
|
75
|
+
import { z } from 'zod';
|
|
76
|
+
export const createUserSchema = z.object({
|
|
77
|
+
email: z.string().email(),
|
|
78
|
+
password: z.string().min(8),
|
|
79
|
+
});
|
|
80
|
+
patterns:
|
|
81
|
+
data_flow:
|
|
82
|
+
direction: "Route → Service → Data Access"
|
|
83
|
+
rules:
|
|
84
|
+
- "Routes validate input with Zod and delegate to services."
|
|
85
|
+
- "Services are platform-agnostic — no Hono or env-specific imports."
|
|
86
|
+
- "Middleware handles cross-cutting concerns like auth and CORS."
|
|
87
|
+
error_handling:
|
|
88
|
+
recommended: "Use app.onError() for centralized error handling."
|
|
89
|
+
naming:
|
|
90
|
+
routes: "[resource].ts"
|
|
91
|
+
services: "[resource].service.ts"
|
|
92
|
+
schemas: "[resource].schema.ts"
|
|
93
|
+
anti_patterns:
|
|
94
|
+
- id: node_api_in_handler
|
|
95
|
+
severity: critical
|
|
96
|
+
description: "Using Node.js-specific APIs (fs, process, Buffer) in route handlers breaks Cloudflare Workers and Bun compatibility."
|
|
97
|
+
bad_example: |
|
|
98
|
+
// ❌ Node.js API breaks edge compatibility
|
|
99
|
+
app.get('/file', (c) => {
|
|
100
|
+
const data = require('fs').readFileSync('./data.json');
|
|
101
|
+
return c.json(JSON.parse(data));
|
|
102
|
+
});
|
|
103
|
+
good_example: |
|
|
104
|
+
// ✓ Use env bindings or fetch — platform-agnostic
|
|
105
|
+
app.get('/file', async (c) => {
|
|
106
|
+
const data = await c.env.KV.get('data');
|
|
107
|
+
return c.json(JSON.parse(data));
|
|
108
|
+
});
|
|
109
|
+
- id: business_logic_in_handler
|
|
110
|
+
severity: critical
|
|
111
|
+
description: "Placing business rules and DB queries directly in route handlers instead of services."
|
|
112
|
+
bad_example: |
|
|
113
|
+
app.post('/users', async (c) => {
|
|
114
|
+
const body = await c.req.json();
|
|
115
|
+
const hash = await bcrypt.hash(body.password, 10);
|
|
116
|
+
const user = await db.insert(users).values({ ...body, password: hash });
|
|
117
|
+
return c.json(user);
|
|
118
|
+
});
|
|
119
|
+
good_example: |
|
|
120
|
+
app.post('/users', zValidator('json', createUserSchema), async (c) => {
|
|
121
|
+
return c.json(await userService.create(c.req.valid('json')), 201);
|
|
122
|
+
});
|
|
123
|
+
- id: no_zod_validation
|
|
124
|
+
severity: warning
|
|
125
|
+
description: "Parsing request body with c.req.json() without schema validation — trusts unvalidated user input."
|
|
126
|
+
bad_example: |
|
|
127
|
+
const body = await c.req.json(); // unvalidated
|
|
128
|
+
good_example: |
|
|
129
|
+
// Use zValidator middleware — body is typed and validated
|
|
130
|
+
zValidator('json', createUserSchema)
|