@webpieces/dev-config 0.0.0-dev
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/README.md +306 -0
- package/bin/set-version.sh +86 -0
- package/bin/setup-claude-patterns.sh +51 -0
- package/bin/start.sh +107 -0
- package/bin/stop.sh +65 -0
- package/bin/use-local-webpieces.sh +89 -0
- package/bin/use-published-webpieces.sh +33 -0
- package/config/eslint/base.mjs +91 -0
- package/config/typescript/tsconfig.base.json +25 -0
- package/eslint-plugin/__tests__/catch-error-pattern.test.ts +360 -0
- package/eslint-plugin/__tests__/max-file-lines.test.ts +195 -0
- package/eslint-plugin/__tests__/max-method-lines.test.ts +246 -0
- package/eslint-plugin/index.d.ts +14 -0
- package/eslint-plugin/index.js +19 -0
- package/eslint-plugin/index.js.map +1 -0
- package/eslint-plugin/index.ts +18 -0
- package/eslint-plugin/rules/catch-error-pattern.d.ts +11 -0
- package/eslint-plugin/rules/catch-error-pattern.js +196 -0
- package/eslint-plugin/rules/catch-error-pattern.js.map +1 -0
- package/eslint-plugin/rules/catch-error-pattern.ts +281 -0
- package/eslint-plugin/rules/max-file-lines.d.ts +12 -0
- package/eslint-plugin/rules/max-file-lines.js +257 -0
- package/eslint-plugin/rules/max-file-lines.js.map +1 -0
- package/eslint-plugin/rules/max-file-lines.ts +272 -0
- package/eslint-plugin/rules/max-method-lines.d.ts +12 -0
- package/eslint-plugin/rules/max-method-lines.js +257 -0
- package/eslint-plugin/rules/max-method-lines.js.map +1 -0
- package/eslint-plugin/rules/max-method-lines.ts +304 -0
- package/package.json +54 -0
- package/patterns/CLAUDE.md +293 -0
- package/patterns/claude.patterns.md +798 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to enforce maximum method length
|
|
3
|
+
*
|
|
4
|
+
* Enforces a configurable maximum line count for methods, functions, and arrow functions.
|
|
5
|
+
* Default: 70 lines
|
|
6
|
+
*
|
|
7
|
+
* Configuration:
|
|
8
|
+
* '@webpieces/max-method-lines': ['error', { max: 70 }]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Rule } from 'eslint';
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
|
|
15
|
+
interface MethodLinesOptions {
|
|
16
|
+
max: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface FunctionNode {
|
|
20
|
+
type:
|
|
21
|
+
| 'FunctionDeclaration'
|
|
22
|
+
| 'FunctionExpression'
|
|
23
|
+
| 'ArrowFunctionExpression'
|
|
24
|
+
| 'MethodDefinition';
|
|
25
|
+
body?: any;
|
|
26
|
+
loc?: {
|
|
27
|
+
start: { line: number };
|
|
28
|
+
end: { line: number };
|
|
29
|
+
};
|
|
30
|
+
key?: {
|
|
31
|
+
name?: string;
|
|
32
|
+
};
|
|
33
|
+
id?: {
|
|
34
|
+
name?: string;
|
|
35
|
+
};
|
|
36
|
+
[key: string]: any;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const METHOD_DOC_CONTENT = `# AI Agent Instructions: Method Too Long
|
|
40
|
+
|
|
41
|
+
**READ THIS FILE to fix methods that are too long**
|
|
42
|
+
|
|
43
|
+
## Core Principle
|
|
44
|
+
Every method should read like a TABLE OF CONTENTS of a book.
|
|
45
|
+
- Each method call is a "chapter"
|
|
46
|
+
- When you dive into a method, you find another table of contents
|
|
47
|
+
- Keeping methods under 70 lines is achievable with proper extraction
|
|
48
|
+
|
|
49
|
+
## Command: Extract Code into Named Methods
|
|
50
|
+
|
|
51
|
+
### Pattern 1: Extract Loop Bodies
|
|
52
|
+
\`\`\`typescript
|
|
53
|
+
// ❌ BAD: 50 lines embedded in loop
|
|
54
|
+
for (const order of orders) {
|
|
55
|
+
// 20 lines of validation logic
|
|
56
|
+
// 15 lines of processing logic
|
|
57
|
+
// 10 lines of notification logic
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ✅ GOOD: Extracted to named methods
|
|
61
|
+
for (const order of orders) {
|
|
62
|
+
validateOrder(order);
|
|
63
|
+
processOrderItems(order);
|
|
64
|
+
sendNotifications(order);
|
|
65
|
+
}
|
|
66
|
+
\`\`\`
|
|
67
|
+
|
|
68
|
+
### Pattern 2: Try-Catch Wrapper for Exception Handling
|
|
69
|
+
\`\`\`typescript
|
|
70
|
+
// ✅ GOOD: Separates success path from error handling
|
|
71
|
+
async function handleRequest(req: Request): Promise<Response> {
|
|
72
|
+
try {
|
|
73
|
+
return await executeRequest(req);
|
|
74
|
+
} catch (err: any) {
|
|
75
|
+
const error = toError(err);
|
|
76
|
+
return createErrorResponse(error);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
\`\`\`
|
|
80
|
+
|
|
81
|
+
### Pattern 3: Sequential Method Calls (Table of Contents)
|
|
82
|
+
\`\`\`typescript
|
|
83
|
+
// ✅ GOOD: Self-documenting steps
|
|
84
|
+
function processOrder(order: Order): void {
|
|
85
|
+
validateOrderData(order);
|
|
86
|
+
calculateTotals(order);
|
|
87
|
+
applyDiscounts(order);
|
|
88
|
+
processPayment(order);
|
|
89
|
+
updateInventory(order);
|
|
90
|
+
sendConfirmation(order);
|
|
91
|
+
}
|
|
92
|
+
\`\`\`
|
|
93
|
+
|
|
94
|
+
### Pattern 4: Separate Data Object Creation
|
|
95
|
+
\`\`\`typescript
|
|
96
|
+
// ❌ BAD: 15 lines of inline object creation
|
|
97
|
+
doSomething({ field1: ..., field2: ..., field3: ..., /* 15 more fields */ });
|
|
98
|
+
|
|
99
|
+
// ✅ GOOD: Extract to factory method
|
|
100
|
+
const request = createRequestObject(data);
|
|
101
|
+
doSomething(request);
|
|
102
|
+
\`\`\`
|
|
103
|
+
|
|
104
|
+
### Pattern 5: Extract Inline Logic to Named Functions
|
|
105
|
+
\`\`\`typescript
|
|
106
|
+
// ❌ BAD: Complex inline logic
|
|
107
|
+
if (user.role === 'admin' && user.permissions.includes('write') && !user.suspended) {
|
|
108
|
+
// 30 lines of admin logic
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ✅ GOOD: Extract to named methods
|
|
112
|
+
if (isAdminWithWriteAccess(user)) {
|
|
113
|
+
performAdminOperation(user);
|
|
114
|
+
}
|
|
115
|
+
\`\`\`
|
|
116
|
+
|
|
117
|
+
## AI Agent Action Steps
|
|
118
|
+
|
|
119
|
+
1. **IDENTIFY** the long method in the error message
|
|
120
|
+
2. **READ** the method to understand its logical sections
|
|
121
|
+
3. **EXTRACT** logical units into separate methods with descriptive names
|
|
122
|
+
4. **REPLACE** inline code with method calls
|
|
123
|
+
5. **VERIFY** each extracted method is <70 lines
|
|
124
|
+
6. **TEST** that functionality remains unchanged
|
|
125
|
+
|
|
126
|
+
## Examples of "Logical Units" to Extract
|
|
127
|
+
- Validation logic → \`validateX()\`
|
|
128
|
+
- Data transformation → \`transformXToY()\`
|
|
129
|
+
- API calls → \`fetchXFromApi()\`
|
|
130
|
+
- Object creation → \`createX()\`
|
|
131
|
+
- Loop bodies → \`processItem()\`
|
|
132
|
+
- Error handling → \`handleXError()\`
|
|
133
|
+
|
|
134
|
+
Remember: Methods should read like a table of contents. Each line should be a "chapter title" (method call) that describes what happens, not how it happens.
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
// Module-level flag to prevent redundant file creation
|
|
138
|
+
let methodDocCreated = false;
|
|
139
|
+
|
|
140
|
+
function getWorkspaceRoot(context: Rule.RuleContext): string {
|
|
141
|
+
const filename = context.filename || context.getFilename();
|
|
142
|
+
let dir = path.dirname(filename);
|
|
143
|
+
|
|
144
|
+
// Walk up directory tree to find workspace root
|
|
145
|
+
while (dir !== path.dirname(dir)) {
|
|
146
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
147
|
+
if (fs.existsSync(pkgPath)) {
|
|
148
|
+
try {
|
|
149
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
150
|
+
if (pkg.workspaces || pkg.name === 'webpieces-ts') {
|
|
151
|
+
return dir;
|
|
152
|
+
}
|
|
153
|
+
} catch (err: any) {
|
|
154
|
+
//const error = toError(err);
|
|
155
|
+
// Continue searching if JSON parse fails
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
dir = path.dirname(dir);
|
|
159
|
+
}
|
|
160
|
+
return process.cwd(); // Fallback
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function ensureDocFile(docPath: string, content: string): boolean {
|
|
164
|
+
try {
|
|
165
|
+
fs.mkdirSync(path.dirname(docPath), { recursive: true });
|
|
166
|
+
fs.writeFileSync(docPath, content, 'utf-8');
|
|
167
|
+
return true;
|
|
168
|
+
} catch (err: any) {
|
|
169
|
+
//const error = toError(err);
|
|
170
|
+
// Graceful degradation: log warning but don't break lint
|
|
171
|
+
console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function ensureMethodDoc(context: Rule.RuleContext): void {
|
|
177
|
+
if (methodDocCreated) return; // Performance: only create once per lint run
|
|
178
|
+
|
|
179
|
+
const workspaceRoot = getWorkspaceRoot(context);
|
|
180
|
+
const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.methods.md');
|
|
181
|
+
|
|
182
|
+
if (ensureDocFile(docPath, METHOD_DOC_CONTENT)) {
|
|
183
|
+
methodDocCreated = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const rule: Rule.RuleModule = {
|
|
188
|
+
meta: {
|
|
189
|
+
type: 'suggestion',
|
|
190
|
+
docs: {
|
|
191
|
+
description: 'Enforce maximum method length',
|
|
192
|
+
category: 'Best Practices',
|
|
193
|
+
recommended: false,
|
|
194
|
+
url: 'https://github.com/deanhiller/webpieces-ts',
|
|
195
|
+
},
|
|
196
|
+
messages: {
|
|
197
|
+
tooLong:
|
|
198
|
+
'AI Agent: READ tmp/webpieces/webpieces.methods.md for fix instructions. Method "{{name}}" has {{actual}} lines (max: {{max}})',
|
|
199
|
+
},
|
|
200
|
+
fixable: undefined,
|
|
201
|
+
schema: [
|
|
202
|
+
{
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
max: {
|
|
206
|
+
type: 'integer',
|
|
207
|
+
minimum: 1,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
additionalProperties: false,
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
create(context: Rule.RuleContext): Rule.RuleListener {
|
|
216
|
+
const options = context.options[0] as MethodLinesOptions | undefined;
|
|
217
|
+
const maxLines = options?.max ?? 70;
|
|
218
|
+
|
|
219
|
+
function checkFunction(node: any): void {
|
|
220
|
+
ensureMethodDoc(context);
|
|
221
|
+
|
|
222
|
+
const funcNode = node as FunctionNode;
|
|
223
|
+
|
|
224
|
+
// Skip if this is a function expression that's part of a method definition
|
|
225
|
+
// (method definitions will be handled by checkMethod)
|
|
226
|
+
if (
|
|
227
|
+
funcNode.type === 'FunctionExpression' &&
|
|
228
|
+
funcNode['parent']?.type === 'MethodDefinition'
|
|
229
|
+
) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Skip if no location info or no body
|
|
234
|
+
if (!funcNode.loc || !funcNode.body) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Get function name
|
|
239
|
+
let name = 'anonymous';
|
|
240
|
+
if (funcNode.type === 'FunctionDeclaration' && funcNode.id?.name) {
|
|
241
|
+
name = funcNode.id.name;
|
|
242
|
+
} else if (funcNode.type === 'FunctionExpression' && funcNode.id?.name) {
|
|
243
|
+
name = funcNode.id.name;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Calculate line count
|
|
247
|
+
const startLine = funcNode.loc.start.line;
|
|
248
|
+
const endLine = funcNode.loc.end.line;
|
|
249
|
+
const lineCount = endLine - startLine + 1;
|
|
250
|
+
|
|
251
|
+
if (lineCount > maxLines) {
|
|
252
|
+
context.report({
|
|
253
|
+
node: funcNode as any,
|
|
254
|
+
messageId: 'tooLong',
|
|
255
|
+
data: {
|
|
256
|
+
name,
|
|
257
|
+
actual: String(lineCount),
|
|
258
|
+
max: String(maxLines),
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function checkMethod(node: any): void {
|
|
265
|
+
ensureMethodDoc(context);
|
|
266
|
+
|
|
267
|
+
const methodNode = node;
|
|
268
|
+
|
|
269
|
+
// Skip if no location info
|
|
270
|
+
if (!methodNode.loc || !methodNode.value) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Get method name from key
|
|
275
|
+
const name = methodNode.key?.name || 'anonymous';
|
|
276
|
+
|
|
277
|
+
// Calculate line count for the method (including the method definition)
|
|
278
|
+
const startLine = methodNode.loc.start.line;
|
|
279
|
+
const endLine = methodNode.loc.end.line;
|
|
280
|
+
const lineCount = endLine - startLine + 1;
|
|
281
|
+
|
|
282
|
+
if (lineCount > maxLines) {
|
|
283
|
+
context.report({
|
|
284
|
+
node: methodNode as any,
|
|
285
|
+
messageId: 'tooLong',
|
|
286
|
+
data: {
|
|
287
|
+
name,
|
|
288
|
+
actual: String(lineCount),
|
|
289
|
+
max: String(maxLines),
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
FunctionDeclaration: checkFunction,
|
|
297
|
+
FunctionExpression: checkFunction,
|
|
298
|
+
ArrowFunctionExpression: checkFunction,
|
|
299
|
+
MethodDefinition: checkMethod,
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export = rule;
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webpieces/dev-config",
|
|
3
|
+
"version": "0.0.0-dev",
|
|
4
|
+
"description": "Development configuration, scripts, and patterns for WebPieces projects",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"wp-start": "./bin/start.sh",
|
|
8
|
+
"wp-stop": "./bin/stop.sh",
|
|
9
|
+
"wp-set-version": "./bin/set-version.sh",
|
|
10
|
+
"wp-use-local": "./bin/use-local-webpieces.sh",
|
|
11
|
+
"wp-use-published": "./bin/use-published-webpieces.sh",
|
|
12
|
+
"wp-setup-patterns": "./bin/setup-claude-patterns.sh"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
"./eslint": "./config/eslint/base.mjs",
|
|
16
|
+
"./eslint-plugin": "./eslint-plugin/index.js",
|
|
17
|
+
"./jest": "./config/jest/preset.js",
|
|
18
|
+
"./tsconfig": "./config/typescript/tsconfig.base.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin/**/*",
|
|
22
|
+
"config/**/*",
|
|
23
|
+
"eslint-plugin/**/*",
|
|
24
|
+
"patterns/**/*",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"eslint": ">=8.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/eslint": "^9.6.1",
|
|
32
|
+
"eslint": "^9.39.1"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"webpieces",
|
|
36
|
+
"config",
|
|
37
|
+
"eslint",
|
|
38
|
+
"scripts",
|
|
39
|
+
"typescript",
|
|
40
|
+
"jest"
|
|
41
|
+
],
|
|
42
|
+
"author": "Dean Hiller",
|
|
43
|
+
"license": "Apache-2.0",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/deanhiller/webpieces-ts.git",
|
|
47
|
+
"directory": "packages/tooling/dev-config"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"types": "./src/index.d.ts",
|
|
53
|
+
"main": "./src/index.js"
|
|
54
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Claude Code Guidelines for webpieces-ts
|
|
2
|
+
|
|
3
|
+
This document contains guidelines and patterns for Claude Code when working on the webpieces-ts codebase.
|
|
4
|
+
|
|
5
|
+
## Core Principles
|
|
6
|
+
|
|
7
|
+
### 1. Classes Over Interfaces for Data Structures
|
|
8
|
+
|
|
9
|
+
**RULE: All data-only structures MUST be classes, not interfaces.**
|
|
10
|
+
|
|
11
|
+
**What is a data-only structure?**
|
|
12
|
+
- Contains only fields/properties
|
|
13
|
+
- No methods with business logic
|
|
14
|
+
- Used purely for data transfer or configuration
|
|
15
|
+
|
|
16
|
+
**Examples of DATA ONLY (use classes):**
|
|
17
|
+
- `ClientConfig` - Configuration data
|
|
18
|
+
- `FilterDefinition` - Filter metadata
|
|
19
|
+
- `RouteDefinition` - Route metadata
|
|
20
|
+
- `RouteRequest` - Request data
|
|
21
|
+
- `RouteContext` - Context data
|
|
22
|
+
- `MethodMeta` - Method metadata
|
|
23
|
+
- `Action` - Response data
|
|
24
|
+
- `RouteMetadata` - Route decorator metadata
|
|
25
|
+
- `JsonFilterConfig` - Configuration data
|
|
26
|
+
- `RegisteredRoute` - Extended route data
|
|
27
|
+
|
|
28
|
+
**Examples of BUSINESS LOGIC (use interfaces):**
|
|
29
|
+
- `Filter` - Has `filter(meta, next)` method with logic
|
|
30
|
+
- `Routes` - Has `configure(routeBuilder)` method with logic
|
|
31
|
+
- `RouteBuilder` - Has `addRoute()`, `addFilter()` methods
|
|
32
|
+
- `WebAppMeta` - Has `getDIModules()`, `getRoutes()` methods
|
|
33
|
+
- `SaveApi` - Has `save(request)` method with logic
|
|
34
|
+
- `RemoteApi` - Has `fetchValue(request)` method with logic
|
|
35
|
+
- `Counter` - Has `inc()`, `get()` methods with logic
|
|
36
|
+
|
|
37
|
+
**Why classes for data?**
|
|
38
|
+
1. No anonymous object literals - explicit construction
|
|
39
|
+
2. Better type safety
|
|
40
|
+
3. Clear instantiation points
|
|
41
|
+
4. Easier to trace in debugger
|
|
42
|
+
5. Can add validation/defaults in constructor
|
|
43
|
+
|
|
44
|
+
**Pattern:**
|
|
45
|
+
```typescript
|
|
46
|
+
// BAD - Interface for data
|
|
47
|
+
export interface UserData {
|
|
48
|
+
name: string;
|
|
49
|
+
age: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const user = { name: 'John', age: 30 }; // Anonymous object literal
|
|
53
|
+
|
|
54
|
+
// GOOD - Class for data
|
|
55
|
+
export class UserData {
|
|
56
|
+
name: string;
|
|
57
|
+
age: number;
|
|
58
|
+
|
|
59
|
+
constructor(name: string, age: number) {
|
|
60
|
+
this.name = name;
|
|
61
|
+
this.age = age;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const user = new UserData('John', 30); // Explicit construction
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. Filter Chain Architecture
|
|
69
|
+
|
|
70
|
+
**Pattern inspired by Java webpieces:**
|
|
71
|
+
|
|
72
|
+
The filter system uses filepath-based matching:
|
|
73
|
+
- Filters are registered with glob patterns (e.g., `'src/controllers/admin/**/*.ts'`)
|
|
74
|
+
- `FilterMatcher` matches filters to routes based on controller filepath
|
|
75
|
+
- Filters without a pattern (or pattern `'*'`) apply globally
|
|
76
|
+
- Filter matching happens at startup (no runtime overhead)
|
|
77
|
+
|
|
78
|
+
**Key classes:**
|
|
79
|
+
- `FilterDefinition(priority, filterClass, filepathPattern)` - Filter registration
|
|
80
|
+
- `FilterMatcher.findMatchingFilters()` - Pattern matching logic
|
|
81
|
+
- `FilterChain` - Executes filters in priority order
|
|
82
|
+
|
|
83
|
+
**Example:**
|
|
84
|
+
```typescript
|
|
85
|
+
export class FilterRoutes implements Routes {
|
|
86
|
+
configure(routeBuilder: RouteBuilder): void {
|
|
87
|
+
// Global filter (pattern '*' matches all)
|
|
88
|
+
routeBuilder.addFilter(
|
|
89
|
+
new FilterDefinition(140, ContextFilter, '*')
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Admin-only filter
|
|
93
|
+
routeBuilder.addFilter(
|
|
94
|
+
new FilterDefinition(100, AdminAuthFilter, 'src/controllers/admin/**/*.ts')
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 3. No Anonymous Object Literals
|
|
101
|
+
|
|
102
|
+
**RULE: Avoid anonymous object structures - use explicit class constructors.**
|
|
103
|
+
|
|
104
|
+
**BAD:**
|
|
105
|
+
```typescript
|
|
106
|
+
routeBuilder.addRoute({
|
|
107
|
+
method: 'POST',
|
|
108
|
+
path: '/api/save',
|
|
109
|
+
handler: myHandler,
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**GOOD:**
|
|
114
|
+
```typescript
|
|
115
|
+
routeBuilder.addRoute(
|
|
116
|
+
new RouteDefinition('POST', '/api/save', myHandler)
|
|
117
|
+
);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 4. Type Safety
|
|
121
|
+
|
|
122
|
+
- Use `unknown` instead of `any` for better type safety
|
|
123
|
+
- Use generics for type-safe route handlers: `RouteHandler<TResult>`
|
|
124
|
+
- Prefer explicit types over inference when defining public APIs
|
|
125
|
+
|
|
126
|
+
### 5. Dependency Injection
|
|
127
|
+
|
|
128
|
+
**Use Inversify for DI:**
|
|
129
|
+
- `@injectable()` - Mark classes as injectable
|
|
130
|
+
- `@inject(TYPES.Something)` - Inject dependencies
|
|
131
|
+
- `@provideSingleton()` - Register singleton in container
|
|
132
|
+
- `@unmanaged()` - Mark constructor params that aren't injected
|
|
133
|
+
|
|
134
|
+
**Pattern:**
|
|
135
|
+
```typescript
|
|
136
|
+
@provideSingleton()
|
|
137
|
+
@Controller()
|
|
138
|
+
export class SaveController {
|
|
139
|
+
constructor(
|
|
140
|
+
@inject(TYPES.Counter) private counter: Counter,
|
|
141
|
+
@inject(TYPES.RemoteApi) private remoteService: RemoteApi
|
|
142
|
+
) {}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 6. Decorators
|
|
147
|
+
|
|
148
|
+
**API Decorators (shared between client and server):**
|
|
149
|
+
- `@ApiInterface()` - Mark API prototype class
|
|
150
|
+
- `@Post()`, `@Get()`, `@Put()`, `@Delete()`, `@Patch()` - HTTP methods
|
|
151
|
+
- `@Path('/path')` - Route path
|
|
152
|
+
|
|
153
|
+
**Server-only Decorators:**
|
|
154
|
+
- `@Controller()` - Mark controller class
|
|
155
|
+
- `@SourceFile('path/to/controller.ts')` - Explicit filepath for filter matching
|
|
156
|
+
- `@provideSingleton()` - Register as singleton
|
|
157
|
+
|
|
158
|
+
### 7. Testing
|
|
159
|
+
|
|
160
|
+
**Unit tests:**
|
|
161
|
+
- Test filter matching logic in isolation
|
|
162
|
+
- Mock dependencies using classes
|
|
163
|
+
- Verify priority ordering
|
|
164
|
+
|
|
165
|
+
**Integration tests:**
|
|
166
|
+
- Use `WebpiecesServer.createApiClient()` for testing without HTTP
|
|
167
|
+
- Test full filter chain execution
|
|
168
|
+
- Verify end-to-end behavior
|
|
169
|
+
|
|
170
|
+
### 8. Documentation
|
|
171
|
+
|
|
172
|
+
- Use JSDoc for all public APIs
|
|
173
|
+
- Explain WHY, not just WHAT
|
|
174
|
+
- Include usage examples
|
|
175
|
+
- Document differences from Java version when applicable
|
|
176
|
+
|
|
177
|
+
## Common Patterns
|
|
178
|
+
|
|
179
|
+
### Creating a New Filter
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { injectable } from 'inversify';
|
|
183
|
+
import { Filter, MethodMeta, Action, NextFilter } from '@webpieces/http-filters';
|
|
184
|
+
|
|
185
|
+
@injectable()
|
|
186
|
+
export class MyFilter implements Filter {
|
|
187
|
+
priority = 100;
|
|
188
|
+
|
|
189
|
+
async filter(meta: MethodMeta, next: NextFilter): Promise<Action> {
|
|
190
|
+
// Before logic
|
|
191
|
+
console.log(`Request: ${meta.httpMethod} ${meta.path}`);
|
|
192
|
+
|
|
193
|
+
// Execute next filter/controller
|
|
194
|
+
const action = await next.execute();
|
|
195
|
+
|
|
196
|
+
// After logic
|
|
197
|
+
console.log(`Response: ${action.statusCode}`);
|
|
198
|
+
|
|
199
|
+
return action;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Creating a New Controller
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { provideSingleton, Controller } from '@webpieces/http-routing';
|
|
208
|
+
|
|
209
|
+
@provideSingleton()
|
|
210
|
+
@Controller()
|
|
211
|
+
export class MyController extends MyApiPrototype implements MyApi {
|
|
212
|
+
private readonly __validator!: ValidateImplementation<MyController, MyApi>;
|
|
213
|
+
|
|
214
|
+
async myMethod(request: MyRequest): Promise<MyResponse> {
|
|
215
|
+
// Implementation
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Registering Routes and Filters
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
export class MyRoutes implements Routes {
|
|
224
|
+
configure(routeBuilder: RouteBuilder): void {
|
|
225
|
+
// Register filters
|
|
226
|
+
routeBuilder.addFilter(
|
|
227
|
+
new FilterDefinition(140, ContextFilter, '*')
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Register API routes
|
|
231
|
+
// (handled automatically by RESTApiRoutes)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Architecture Overview
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
┌─────────────────────────────────────────────────┐
|
|
240
|
+
│ WebAppMeta │
|
|
241
|
+
│ - getDIModules() - Returns DI modules │
|
|
242
|
+
│ - getRoutes() - Returns route configurations │
|
|
243
|
+
└─────────────────────────────────────────────────┘
|
|
244
|
+
│
|
|
245
|
+
▼
|
|
246
|
+
┌─────────────────────────────────────────────────┐
|
|
247
|
+
│ WebpiecesServer │
|
|
248
|
+
│ - Initializes DI containers │
|
|
249
|
+
│ - Registers routes using RouteBuilder │
|
|
250
|
+
│ - Matches filters to routes (FilterMatcher) │
|
|
251
|
+
│ - Creates filter chains per route │
|
|
252
|
+
└─────────────────────────────────────────────────┘
|
|
253
|
+
│
|
|
254
|
+
▼
|
|
255
|
+
┌─────────────────────────────────────────────────┐
|
|
256
|
+
│ FilterChain │
|
|
257
|
+
│ - Executes filters in priority order │
|
|
258
|
+
│ - Wraps controller invocation │
|
|
259
|
+
└─────────────────────────────────────────────────┘
|
|
260
|
+
│
|
|
261
|
+
▼
|
|
262
|
+
┌─────────────────────────────────────────────────┐
|
|
263
|
+
│ Controller │
|
|
264
|
+
│ - Implements API interface │
|
|
265
|
+
│ - Business logic │
|
|
266
|
+
│ - Returns response │
|
|
267
|
+
└─────────────────────────────────────────────────┘
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Key Differences from Java Version
|
|
271
|
+
|
|
272
|
+
1. **Glob patterns instead of Regex**: TypeScript uses glob patterns for filepath matching
|
|
273
|
+
2. **Class-based data structures**: All data structures are classes, not interfaces
|
|
274
|
+
3. **Decorator-based metadata**: Uses TypeScript decorators instead of annotations
|
|
275
|
+
4. **Inversify instead of Guice**: Different DI framework but similar patterns
|
|
276
|
+
5. **Class name fallback**: Since TypeScript doesn't provide source paths at runtime, we use class name patterns like `**/SaveController.ts`
|
|
277
|
+
|
|
278
|
+
## When Adding New Features
|
|
279
|
+
|
|
280
|
+
1. **Check for data-only structures** - If it's just data, use a class
|
|
281
|
+
2. **Add filter matching support** - Consider if filters need to scope to it
|
|
282
|
+
3. **Write tests first** - Unit tests for logic, integration tests for behavior
|
|
283
|
+
4. **Update documentation** - Keep this file and claude.patterns.md up to date
|
|
284
|
+
5. **Follow existing patterns** - Look at similar features for consistency
|
|
285
|
+
|
|
286
|
+
## Common Mistakes to Avoid
|
|
287
|
+
|
|
288
|
+
1. ❌ Using interfaces for data structures
|
|
289
|
+
2. ❌ Creating anonymous object literals for configs/definitions
|
|
290
|
+
3. ❌ Forgetting to export classes from index.ts
|
|
291
|
+
4. ❌ Using `any` instead of `unknown` for generic types
|
|
292
|
+
5. ❌ Skipping tests for new features
|
|
293
|
+
6. ❌ Not documenting differences from Java version
|