@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.
Files changed (31) hide show
  1. package/README.md +306 -0
  2. package/bin/set-version.sh +86 -0
  3. package/bin/setup-claude-patterns.sh +51 -0
  4. package/bin/start.sh +107 -0
  5. package/bin/stop.sh +65 -0
  6. package/bin/use-local-webpieces.sh +89 -0
  7. package/bin/use-published-webpieces.sh +33 -0
  8. package/config/eslint/base.mjs +91 -0
  9. package/config/typescript/tsconfig.base.json +25 -0
  10. package/eslint-plugin/__tests__/catch-error-pattern.test.ts +360 -0
  11. package/eslint-plugin/__tests__/max-file-lines.test.ts +195 -0
  12. package/eslint-plugin/__tests__/max-method-lines.test.ts +246 -0
  13. package/eslint-plugin/index.d.ts +14 -0
  14. package/eslint-plugin/index.js +19 -0
  15. package/eslint-plugin/index.js.map +1 -0
  16. package/eslint-plugin/index.ts +18 -0
  17. package/eslint-plugin/rules/catch-error-pattern.d.ts +11 -0
  18. package/eslint-plugin/rules/catch-error-pattern.js +196 -0
  19. package/eslint-plugin/rules/catch-error-pattern.js.map +1 -0
  20. package/eslint-plugin/rules/catch-error-pattern.ts +281 -0
  21. package/eslint-plugin/rules/max-file-lines.d.ts +12 -0
  22. package/eslint-plugin/rules/max-file-lines.js +257 -0
  23. package/eslint-plugin/rules/max-file-lines.js.map +1 -0
  24. package/eslint-plugin/rules/max-file-lines.ts +272 -0
  25. package/eslint-plugin/rules/max-method-lines.d.ts +12 -0
  26. package/eslint-plugin/rules/max-method-lines.js +257 -0
  27. package/eslint-plugin/rules/max-method-lines.js.map +1 -0
  28. package/eslint-plugin/rules/max-method-lines.ts +304 -0
  29. package/package.json +54 -0
  30. package/patterns/CLAUDE.md +293 -0
  31. package/patterns/claude.patterns.md +798 -0
@@ -0,0 +1,798 @@
1
+ # Coding Patterns for webpieces-ts
2
+
3
+ This file contains specific coding patterns and conventions used in the webpieces-ts project. These patterns should be followed consistently when adding new features or modifying existing code.
4
+
5
+ ## Table of Contents
6
+ 1. [Classes vs Interfaces](#classes-vs-interfaces)
7
+ 2. [Data Structure Patterns](#data-structure-patterns)
8
+ 3. [Filter Chain Patterns](#filter-chain-patterns)
9
+ 4. [Dependency Injection Patterns](#dependency-injection-patterns)
10
+ 5. [Decorator Patterns](#decorator-patterns)
11
+ 6. [Testing Patterns](#testing-patterns)
12
+
13
+ ---
14
+
15
+ ## Classes vs Interfaces
16
+
17
+ ### The Golden Rule
18
+
19
+ **DATA ONLY → Class**
20
+ **BUSINESS LOGIC → Interface**
21
+
22
+ ### Decision Tree
23
+
24
+ ```
25
+ Does the type have methods with business logic?
26
+ ├─ YES → Use Interface
27
+ │ └─ Examples: Filter, Routes, RouteBuilder, WebAppMeta, SaveApi
28
+
29
+ └─ NO → Use Class
30
+ └─ Is it just data/configuration?
31
+ └─ YES → Use Class
32
+ └─ Examples: ClientConfig, FilterDefinition, RouteDefinition,
33
+ MethodMeta, Action, RouteMetadata
34
+ ```
35
+
36
+ ### Why This Matters
37
+
38
+ **Classes provide:**
39
+ 1. **Explicit construction** - No anonymous object literals
40
+ 2. **Validation at creation** - Enforce required fields
41
+ 3. **Default values** - Set defaults in constructor
42
+ 4. **Type safety** - Clear instantiation points
43
+ 5. **Debuggability** - Explicit class names in stack traces
44
+
45
+ **Interfaces provide:**
46
+ 1. **Polymorphism** - Multiple implementations
47
+ 2. **Abstraction** - Define contracts without implementation
48
+ 3. **Dependency Inversion** - Depend on abstractions
49
+
50
+ ---
51
+
52
+ ## Data Structure Patterns
53
+
54
+ ### Pattern 1: Simple Data Class
55
+
56
+ For data with all required fields:
57
+
58
+ ```typescript
59
+ export class SaveRequest {
60
+ query: string;
61
+ meta?: RequestMetadata;
62
+
63
+ constructor(query: string, meta?: RequestMetadata) {
64
+ this.query = query;
65
+ this.meta = meta;
66
+ }
67
+ }
68
+
69
+ // Usage
70
+ const request = new SaveRequest('search term', metadata);
71
+ ```
72
+
73
+ ### Pattern 2: Configuration Class with Defaults
74
+
75
+ For configuration with optional fields and defaults:
76
+
77
+ ```typescript
78
+ export class JsonFilterConfig {
79
+ validationEnabled: boolean;
80
+ loggingEnabled: boolean;
81
+
82
+ constructor(
83
+ validationEnabled: boolean = true,
84
+ loggingEnabled: boolean = false
85
+ ) {
86
+ this.validationEnabled = validationEnabled;
87
+ this.loggingEnabled = loggingEnabled;
88
+ }
89
+ }
90
+
91
+ // Usage
92
+ const config = new JsonFilterConfig(); // Uses defaults
93
+ const customConfig = new JsonFilterConfig(false, true); // Custom values
94
+ ```
95
+
96
+ ### Pattern 3: Metadata Class
97
+
98
+ For metadata with many optional fields:
99
+
100
+ ```typescript
101
+ export class MethodMeta {
102
+ httpMethod: string;
103
+ path: string;
104
+ methodName: string;
105
+ params: any[];
106
+ request?: any;
107
+ response?: any;
108
+ metadata?: Map<string, any>;
109
+
110
+ constructor(
111
+ httpMethod: string,
112
+ path: string,
113
+ methodName: string,
114
+ params: any[],
115
+ request?: any,
116
+ response?: any,
117
+ metadata?: Map<string, any>
118
+ ) {
119
+ this.httpMethod = httpMethod;
120
+ this.path = path;
121
+ this.methodName = methodName;
122
+ this.params = params;
123
+ this.request = request;
124
+ this.response = response;
125
+ this.metadata = metadata;
126
+ }
127
+ }
128
+
129
+ // Usage
130
+ const meta = new MethodMeta(
131
+ 'POST',
132
+ '/api/save',
133
+ 'save',
134
+ [requestBody],
135
+ requestData,
136
+ undefined,
137
+ new Map()
138
+ );
139
+ ```
140
+
141
+ ### Pattern 4: Extending Data Classes
142
+
143
+ When one data class extends another:
144
+
145
+ ```typescript
146
+ export class RegisteredRoute<TResult = unknown> extends RouteDefinition<TResult> {
147
+ routeMetadata?: RouteMetadata;
148
+ controllerClass?: any;
149
+
150
+ constructor(
151
+ method: string,
152
+ path: string,
153
+ handler: RouteHandler<TResult>,
154
+ controllerFilepath?: string,
155
+ routeMetadata?: RouteMetadata,
156
+ controllerClass?: any
157
+ ) {
158
+ super(method, path, handler, controllerFilepath);
159
+ this.routeMetadata = routeMetadata;
160
+ this.controllerClass = controllerClass;
161
+ }
162
+ }
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Filter Chain Patterns
168
+
169
+ ### Pattern 1: Global Filter
170
+
171
+ Applies to all routes:
172
+
173
+ ```typescript
174
+ export class FilterRoutes implements Routes {
175
+ configure(routeBuilder: RouteBuilder): void {
176
+ routeBuilder.addFilter(
177
+ new FilterDefinition(140, ContextFilter, '*')
178
+ );
179
+ }
180
+ }
181
+ ```
182
+
183
+ ### Pattern 2: Path-Scoped Filter
184
+
185
+ Applies to controllers matching a pattern:
186
+
187
+ ```typescript
188
+ // All admin controllers
189
+ routeBuilder.addFilter(
190
+ new FilterDefinition(
191
+ 100,
192
+ AdminAuthFilter,
193
+ 'src/controllers/admin/**/*.ts'
194
+ )
195
+ );
196
+
197
+ // Specific controller
198
+ routeBuilder.addFilter(
199
+ new FilterDefinition(
200
+ 80,
201
+ SpecialFilter,
202
+ '**/SaveController.ts'
203
+ )
204
+ );
205
+
206
+ // Any controller in 'admin' directory
207
+ routeBuilder.addFilter(
208
+ new FilterDefinition(
209
+ 90,
210
+ AdminFilter,
211
+ '**/admin/**'
212
+ )
213
+ );
214
+ ```
215
+
216
+ ### Pattern 3: Filter Implementation
217
+
218
+ ```typescript
219
+ import { injectable } from 'inversify';
220
+ import { Filter, MethodMeta, Action, NextFilter } from '@webpieces/http-filters';
221
+
222
+ @injectable()
223
+ export class MyFilter implements Filter {
224
+ priority = 100; // Higher = executes earlier
225
+
226
+ async filter(meta: MethodMeta, next: NextFilter): Promise<Action> {
227
+ // 1. Pre-processing
228
+ console.log(`Before: ${meta.httpMethod} ${meta.path}`);
229
+
230
+ // 2. Call next filter/controller
231
+ const action = await next.execute();
232
+
233
+ // 3. Post-processing
234
+ console.log(`After: ${action.statusCode}`);
235
+
236
+ return action;
237
+ }
238
+ }
239
+ ```
240
+
241
+ ### Pattern 4: Filter Priority Convention
242
+
243
+ ```
244
+ 140 - Context setup (ContextFilter)
245
+ 120 - Request attributes
246
+ 100 - Authorization/Authentication
247
+ 90 - Metrics
248
+ 80 - Logging
249
+ 60 - JSON serialization (JsonFilter)
250
+ 40 - Transactions
251
+ 20 - Caching
252
+ 0 - Controller execution
253
+ ```
254
+
255
+ ---
256
+
257
+ ## Dependency Injection Patterns
258
+
259
+ ### Pattern 1: Controller with Dependencies
260
+
261
+ ```typescript
262
+ import { injectable, inject } from 'inversify';
263
+ import { provideSingleton, Controller } from '@webpieces/http-routing';
264
+
265
+ @provideSingleton()
266
+ @Controller()
267
+ export class SaveController extends SaveApiPrototype implements SaveApi {
268
+ private readonly __validator!: ValidateImplementation<SaveController, SaveApi>;
269
+
270
+ constructor(
271
+ @inject(TYPES.Counter) private counter: Counter,
272
+ @inject(TYPES.RemoteApi) private remoteService: RemoteApi
273
+ ) {
274
+ super();
275
+ }
276
+
277
+ override async save(request: SaveRequest): Promise<SaveResponse> {
278
+ // Implementation
279
+ }
280
+ }
281
+ ```
282
+
283
+ ### Pattern 2: Filter with Unmanaged Config
284
+
285
+ ```typescript
286
+ @injectable()
287
+ export class JsonFilter implements Filter {
288
+ constructor(
289
+ @unmanaged() private config: JsonFilterConfig = new JsonFilterConfig()
290
+ ) {
291
+ // config is not injected from DI container
292
+ }
293
+ }
294
+ ```
295
+
296
+ ### Pattern 3: DI Module Registration
297
+
298
+ ```typescript
299
+ import { ContainerModule } from 'inversify';
300
+ import { buildProviderModule } from '@inversifyjs/binding-decorators';
301
+
302
+ export class MyModule {
303
+ getModule(): ContainerModule {
304
+ return new ContainerModule((bind) => {
305
+ // Manual bindings
306
+ bind<Counter>(TYPES.Counter).to(SimpleCounter).inSingletonScope();
307
+
308
+ // Auto-scan for @provideSingleton decorators
309
+ // (handled by buildProviderModule)
310
+ });
311
+ }
312
+ }
313
+ ```
314
+
315
+ ---
316
+
317
+ ## Decorator Patterns
318
+
319
+ ### Pattern 1: API Interface Declaration
320
+
321
+ ```typescript
322
+ import { ApiInterface, Post, Path } from '@webpieces/http-api';
323
+
324
+ @ApiInterface()
325
+ export abstract class SaveApiPrototype {
326
+ @Post()
327
+ @Path('/search/item')
328
+ abstract save(request: SaveRequest): Promise<SaveResponse>;
329
+ }
330
+
331
+ export interface SaveApi extends SaveApiPrototype {}
332
+ export const SaveApiPrototype = SaveApiPrototype;
333
+ ```
334
+
335
+ ### Pattern 2: Controller Implementation
336
+
337
+ ```typescript
338
+ import { Controller, provideSingleton, SourceFile } from '@webpieces/http-routing';
339
+
340
+ @SourceFile('src/controllers/SaveController.ts') // Optional: explicit filepath
341
+ @provideSingleton()
342
+ @Controller()
343
+ export class SaveController extends SaveApiPrototype implements SaveApi {
344
+ override async save(request: SaveRequest): Promise<SaveResponse> {
345
+ // Implementation
346
+ }
347
+ }
348
+ ```
349
+
350
+ ### Pattern 3: Validation Helper
351
+
352
+ ```typescript
353
+ import { ValidateImplementation } from '@webpieces/http-api';
354
+
355
+ export class SaveController extends SaveApiPrototype implements SaveApi {
356
+ // Compile-time validator: Ensures all SaveApi methods are implemented
357
+ private readonly __validator!: ValidateImplementation<SaveController, SaveApi>;
358
+ }
359
+ ```
360
+
361
+ ---
362
+
363
+ ## Testing Patterns
364
+
365
+ ### Pattern 1: Unit Test for Filter Matching
366
+
367
+ ```typescript
368
+ import { FilterMatcher } from './FilterMatcher';
369
+ import { FilterDefinition } from '@webpieces/core-meta';
370
+
371
+ describe('FilterMatcher', () => {
372
+ it('should match admin controllers', () => {
373
+ const adminFilter = new MockFilter(100);
374
+
375
+ const registry = [
376
+ {
377
+ filter: adminFilter,
378
+ definition: new FilterDefinition(
379
+ 100,
380
+ MockFilter,
381
+ 'src/controllers/admin/**/*.ts'
382
+ ),
383
+ },
384
+ ];
385
+
386
+ const result = FilterMatcher.findMatchingFilters(
387
+ 'src/controllers/admin/UserController.ts',
388
+ registry
389
+ );
390
+
391
+ expect(result).toEqual([adminFilter]);
392
+ });
393
+ });
394
+ ```
395
+
396
+ ### Pattern 2: Integration Test without HTTP
397
+
398
+ ```typescript
399
+ import { WebpiecesServer } from '@webpieces/http-server';
400
+ import { ProdServerMeta } from '../ProdServerMeta';
401
+ import { SaveApi, SaveApiPrototype } from '../api/SaveApi';
402
+
403
+ describe('SaveApi Integration', () => {
404
+ let server: WebpiecesServer;
405
+ let saveApi: SaveApi;
406
+
407
+ beforeEach(() => {
408
+ server = new WebpiecesServer(new ProdServerMeta());
409
+ server.initialize();
410
+ saveApi = server.createApiClient<SaveApi>(SaveApiPrototype);
411
+ });
412
+
413
+ it('should process request through filter chain', async () => {
414
+ const request = new SaveRequest('test query');
415
+ const response = await saveApi.save(request);
416
+
417
+ expect(response.success).toBe(true);
418
+ });
419
+ });
420
+ ```
421
+
422
+ ### Pattern 3: HTTP Client Test with Mock
423
+
424
+ ```typescript
425
+ import { createClient, ClientConfig } from '@webpieces/http-client';
426
+
427
+ describe('SaveApi Client', () => {
428
+ let mockFetch: jest.Mock;
429
+ let originalFetch: typeof fetch;
430
+
431
+ beforeEach(() => {
432
+ originalFetch = global.fetch;
433
+ mockFetch = jest.fn();
434
+ global.fetch = mockFetch as any;
435
+ });
436
+
437
+ afterEach(() => {
438
+ global.fetch = originalFetch;
439
+ });
440
+
441
+ it('should make HTTP request', async () => {
442
+ mockFetch.mockResolvedValue({
443
+ ok: true,
444
+ json: async () => ({ success: true }),
445
+ });
446
+
447
+ const config = new ClientConfig('http://localhost:3000');
448
+ const client = createClient(SaveApiPrototype, config);
449
+
450
+ const response = await client.save(new SaveRequest('test'));
451
+
452
+ expect(mockFetch).toHaveBeenCalledWith(
453
+ 'http://localhost:3000/search/item',
454
+ expect.objectContaining({
455
+ method: 'POST',
456
+ body: JSON.stringify({ query: 'test' }),
457
+ })
458
+ );
459
+ });
460
+ });
461
+ ```
462
+
463
+ ---
464
+
465
+ ## File Organization Patterns
466
+
467
+ ### Package Structure
468
+
469
+ ```
470
+ packages/
471
+ core/
472
+ core-meta/ - Core type definitions (RouteDefinition, FilterDefinition, etc.)
473
+ core-context/ - AsyncLocalStorage context management
474
+ http/
475
+ http-api/ - API decorators (shared by client & server)
476
+ http-routing/ - Server-side routing (RESTApiRoutes)
477
+ http-client/ - Client-side HTTP client generation
478
+ http-filters/ - Filter implementations
479
+ http-server/ - WebpiecesServer, FilterMatcher
480
+ ```
481
+
482
+ ### Export Pattern
483
+
484
+ Always export from `index.ts`:
485
+
486
+ ```typescript
487
+ // packages/http/http-client/src/index.ts
488
+ export { createClient, ClientConfig } from './ClientFactory';
489
+
490
+ // Re-export API decorators for convenience
491
+ export {
492
+ ApiInterface,
493
+ Post,
494
+ Get,
495
+ Path,
496
+ } from '@webpieces/http-api';
497
+ ```
498
+
499
+ ---
500
+
501
+ ## Migration from Java Patterns
502
+
503
+ When porting features from Java webpieces:
504
+
505
+ ### Java → TypeScript Equivalents
506
+
507
+ | Java | TypeScript |
508
+ |------|------------|
509
+ | `interface` (data) | `class` |
510
+ | `interface` (with methods) | `interface` |
511
+ | Guice | Inversify |
512
+ | `@Inject` | `@inject(TYPES.Something)` |
513
+ | `@Singleton` | `@provideSingleton()` |
514
+ | Package regex | Filepath glob pattern |
515
+ | `Pattern.compile("...")` | `minimatch(path, pattern)` |
516
+ | JAX-RS annotations | Decorators (`@Post`, `@Path`) |
517
+
518
+ ### Common Conversions
519
+
520
+ **Java Filter:**
521
+ ```java
522
+ public class MyFilter implements RouteFilter {
523
+ @Inject
524
+ public MyFilter(SomeService service) {
525
+ this.service = service;
526
+ }
527
+
528
+ @Override
529
+ public CompletableFuture<Action> filter(MethodMeta meta, Service<MethodMeta, Action> next) {
530
+ // Logic
531
+ }
532
+ }
533
+ ```
534
+
535
+ **TypeScript Filter:**
536
+ ```typescript
537
+ @injectable()
538
+ export class MyFilter implements Filter {
539
+ constructor(
540
+ @inject(TYPES.SomeService) private service: SomeService
541
+ ) {}
542
+
543
+ async filter(meta: MethodMeta, next: NextFilter): Promise<Action> {
544
+ // Logic
545
+ }
546
+ }
547
+ ```
548
+
549
+ ---
550
+
551
+ ## Anti-Patterns to Avoid
552
+
553
+ ### ❌ Anonymous Object Literals for Data
554
+
555
+ ```typescript
556
+ // BAD
557
+ routeBuilder.addRoute({
558
+ method: 'POST',
559
+ path: '/api/save',
560
+ handler: myHandler,
561
+ });
562
+
563
+ // GOOD
564
+ routeBuilder.addRoute(
565
+ new RouteDefinition('POST', '/api/save', myHandler)
566
+ );
567
+ ```
568
+
569
+ ### ❌ Interface for Data-Only Structures
570
+
571
+ ```typescript
572
+ // BAD
573
+ export interface UserConfig {
574
+ name: string;
575
+ age: number;
576
+ }
577
+
578
+ const config = { name: 'John', age: 30 }; // Anonymous
579
+
580
+ // GOOD
581
+ export class UserConfig {
582
+ name: string;
583
+ age: number;
584
+
585
+ constructor(name: string, age: number) {
586
+ this.name = name;
587
+ this.age = age;
588
+ }
589
+ }
590
+
591
+ const config = new UserConfig('John', 30); // Explicit
592
+ ```
593
+
594
+ ### ❌ Using 'any' Instead of 'unknown'
595
+
596
+ ```typescript
597
+ // BAD
598
+ export class RouteHandler<TResult = any> {
599
+ abstract execute(context: RouteContext): Promise<TResult>;
600
+ }
601
+
602
+ // GOOD
603
+ export class RouteHandler<TResult = unknown> {
604
+ abstract execute(context: RouteContext): Promise<TResult>;
605
+ }
606
+ ```
607
+
608
+ ### ❌ Not Exporting Helper Functions
609
+
610
+ ```typescript
611
+ // BAD - Helper function not exported
612
+ function jsonAction(data: any): Action {
613
+ return new Action('json', data);
614
+ }
615
+
616
+ // GOOD - Helper function exported for reuse
617
+ export function jsonAction(data: any, statusCode: number = 200): Action {
618
+ return new Action('json', data, statusCode);
619
+ }
620
+ ```
621
+
622
+ ---
623
+
624
+ ## Advanced Patterns
625
+
626
+ ### Pattern 1: Type-Safe API Client
627
+
628
+ The client generator creates type-safe proxies:
629
+
630
+ ```typescript
631
+ // Define API interface
632
+ @ApiInterface()
633
+ export abstract class SaveApiPrototype {
634
+ @Post()
635
+ @Path('/search/item')
636
+ abstract save(request: SaveRequest): Promise<SaveResponse>;
637
+ }
638
+
639
+ // Create client
640
+ const config = new ClientConfig('http://localhost:3000');
641
+ const client = createClient(SaveApiPrototype, config);
642
+
643
+ // Type-safe method call
644
+ const response: SaveResponse = await client.save(request); // ✓ Type checked
645
+ ```
646
+
647
+ ### Pattern 2: Filter Chain Execution
648
+
649
+ ```
650
+ Request → Filter 1 (priority 140) → Filter 2 (priority 60) → Controller
651
+ ↓ ↓ ↓
652
+ wraps next wraps next returns result
653
+ ↓ ↓ ↓
654
+ Response ← modifies response ← modifies response ← original result
655
+ ```
656
+
657
+ Implementation:
658
+ ```typescript
659
+ @injectable()
660
+ export class LoggingFilter implements Filter {
661
+ priority = 80;
662
+
663
+ async filter(meta: MethodMeta, next: NextFilter): Promise<Action> {
664
+ const start = Date.now();
665
+
666
+ // Execute next in chain
667
+ const action = await next.execute();
668
+
669
+ const duration = Date.now() - start;
670
+ console.log(`${meta.httpMethod} ${meta.path} - ${duration}ms`);
671
+
672
+ return action;
673
+ }
674
+ }
675
+ ```
676
+
677
+ ### Pattern 3: Context Management with AsyncLocalStorage
678
+
679
+ ```typescript
680
+ import { Context } from '@webpieces/core-context';
681
+
682
+ // In ContextFilter (priority 140 - executes first)
683
+ @injectable()
684
+ export class ContextFilter implements Filter {
685
+ priority = 140;
686
+
687
+ async filter(meta: MethodMeta, next: NextFilter): Promise<Action> {
688
+ return Context.run(() => {
689
+ Context.set('REQUEST_PATH', meta.path);
690
+ Context.set('START_TIME', Date.now());
691
+ return next.execute();
692
+ });
693
+ }
694
+ }
695
+
696
+ // In any controller or filter
697
+ const path = Context.get('REQUEST_PATH');
698
+ const startTime = Context.get('START_TIME');
699
+ ```
700
+
701
+ ### Pattern 4: Two-Container DI Pattern
702
+
703
+ Similar to Java WebPieces:
704
+
705
+ ```typescript
706
+ export class WebpiecesServer {
707
+ // Framework-level bindings
708
+ private webpiecesContainer: Container;
709
+
710
+ // Application bindings (child of webpiecesContainer)
711
+ private appContainer: Container;
712
+
713
+ constructor(meta: WebAppMeta) {
714
+ this.webpiecesContainer = new Container();
715
+ this.appContainer = new Container({ parent: this.webpiecesContainer });
716
+
717
+ // Load user modules into app container
718
+ const modules = meta.getDIModules();
719
+ for (const module of modules) {
720
+ this.appContainer.load(module);
721
+ }
722
+ }
723
+ }
724
+ ```
725
+
726
+ ---
727
+
728
+ ## Filepath-Based Filter Matching
729
+
730
+ ### How It Works
731
+
732
+ Similar to Java's `SharedMatchUtil.findMatchingFilters()`:
733
+
734
+ 1. **Route Registration**: Controller filepath is captured during route registration
735
+ - Uses `@SourceFile()` decorator if present
736
+ - Falls back to class name pattern: `**/SaveController.ts`
737
+
738
+ 2. **Filter Matching**: At startup, `FilterMatcher` matches filters to routes
739
+ - Pattern `'*'` matches all controllers (global)
740
+ - Pattern `'src/controllers/admin/**/*.ts'` matches admin controllers
741
+ - Uses `minimatch` library for glob pattern matching
742
+
743
+ 3. **Filter Chain Creation**: Matched filters are sorted by priority and cached
744
+ - No runtime overhead - matching happens once at startup
745
+ - Each route gets its own filter chain
746
+
747
+ ### Controller Filepath Extraction
748
+
749
+ ```typescript
750
+ private getControllerFilepath(): string | undefined {
751
+ // 1. Check for explicit @SourceFile decorator
752
+ const filepath = Reflect.getMetadata(
753
+ ROUTING_METADATA_KEYS.SOURCE_FILEPATH,
754
+ this.controllerClass
755
+ );
756
+ if (filepath) {
757
+ return filepath;
758
+ }
759
+
760
+ // 2. Fallback to class name pattern
761
+ const className = (this.controllerClass as any).name;
762
+ return className ? `**/${className}.ts` : undefined;
763
+ }
764
+ ```
765
+
766
+ ### Glob Pattern Examples
767
+
768
+ ```typescript
769
+ '*' // All controllers (global)
770
+ '**/*' // All controllers (alternative)
771
+ 'src/controllers/**/*.ts' // All controllers in src/controllers
772
+ 'src/controllers/admin/**/*.ts' // All admin controllers
773
+ '**/admin/**' // Any file in admin directory
774
+ '**/SaveController.ts' // Specific controller file
775
+ 'apps/example-app/src/**/*.ts' // All controllers in example-app
776
+ ```
777
+
778
+ ---
779
+
780
+ ## Summary Checklist
781
+
782
+ When adding new code to webpieces-ts:
783
+
784
+ - [ ] Is it data-only? → Use `class`, not `interface`
785
+ - [ ] Does it have business logic methods? → Use `interface`
786
+ - [ ] Are you creating config/metadata? → Use `class` with constructor defaults
787
+ - [ ] Adding a new filter? → Implement `Filter` interface, use `@injectable()`
788
+ - [ ] Adding a new controller? → Extend API prototype, use `@Controller()` and `@provideSingleton()`
789
+ - [ ] Need to scope a filter? → Use `filepathPattern` in `FilterDefinition`
790
+ - [ ] Writing tests? → Unit tests for logic, integration tests for behavior
791
+ - [ ] Updated exports? → Add to package's `index.ts`
792
+ - [ ] Documented patterns? → Update this file if introducing new patterns
793
+
794
+ ---
795
+
796
+ ## Questions?
797
+
798
+ See `CLAUDE.md` for higher-level guidelines and architecture overview.