@contractspec/lib.source-extractors 2.7.5 → 2.7.7

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 CHANGED
@@ -1,86 +1,68 @@
1
1
  # @contractspec/lib.source-extractors
2
2
 
3
- Extract contract candidates from TypeScript source code across multiple frameworks.
3
+ Website: https://contractspec.io
4
4
 
5
- ## Supported Frameworks
5
+ **Extract contract candidates from TypeScript source code across multiple frameworks (NestJS, Express, Fastify, Hono, Elysia, tRPC, Next.js).**
6
6
 
7
- | Framework | Detection | Routes | Schemas | Status |
8
- |-----------|-----------|--------|---------|--------|
9
- | NestJS | ✅ | ✅ | ✅ | Ready |
10
- | Express | ✅ | ✅ | ✅ | Ready |
11
- | Fastify | | | | Ready |
12
- | Hono | | | | Ready |
13
- | Elysia | ✅ | ✅ | ✅ | Ready |
14
- | tRPC | ✅ | ✅ | ✅ | Ready |
15
- | Next.js API | ✅ | ✅ | ✅ | Ready |
16
- | Zod Schemas | ✅ | N/A | ✅ | Ready |
7
+ ## What It Provides
8
+
9
+ - **Layer**: lib.
10
+ - **Consumers**: CLI, bundles.
11
+ - Related ContractSpec packages include `@contractspec/lib.contracts-spec`, `@contractspec/lib.schema`, `@contractspec/tool.bun`, `@contractspec/tool.typescript`.
12
+ - Related ContractSpec packages include `@contractspec/lib.contracts-spec`, `@contractspec/lib.schema`, `@contractspec/tool.bun`, `@contractspec/tool.typescript`.
17
13
 
18
14
  ## Installation
19
15
 
20
- ```bash
21
- bun add @contractspec/lib.source-extractors
22
- ```
16
+ `npm install @contractspec/lib.source-extractors`
17
+
18
+ or
19
+
20
+ `bun add @contractspec/lib.source-extractors`
23
21
 
24
22
  ## Usage
25
23
 
26
- ```typescript
27
- import { detectFramework, extractFromProject } from '@contractspec/lib.source-extractors';
28
- import { registerAllExtractors } from '@contractspec/lib.source-extractors/extractors';
29
- import { generateOperations } from '@contractspec/lib.source-extractors/codegen';
30
-
31
- // Register all built-in extractors
32
- registerAllExtractors();
33
-
34
- // Detect frameworks in a project
35
- const project = await detectFramework('./my-project', {
36
- readFile: (path) => fs.readFile(path, 'utf-8'),
37
- glob: (pattern) => glob(pattern),
38
- });
39
-
40
- // Extract contract candidates
41
- const result = await extractFromProject(project, {
42
- scope: ['src/controllers'], // Optional: limit scope
43
- });
44
-
45
- if (result.success && result.ir) {
46
- console.log(`Found ${result.ir.endpoints.length} endpoints`);
47
- console.log(`Found ${result.ir.schemas.length} schemas`);
48
-
49
- // Generate ContractSpec code
50
- const files = generateOperations(result.ir, {
51
- outputDir: './generated',
52
- defaultAuth: 'user',
53
- });
54
- }
55
- ```
56
-
57
- ## IR Schema
58
-
59
- The Intermediate Representation (IR) provides a framework-agnostic view of extracted contracts:
60
-
61
- ```typescript
62
- interface ImportIR {
63
- version: '1.0';
64
- extractedAt: string;
65
- project: ProjectInfo;
66
- endpoints: EndpointCandidate[];
67
- schemas: SchemaCandidate[];
68
- errors: ErrorCandidate[];
69
- events: EventCandidate[];
70
- ambiguities: Ambiguity[];
71
- stats: { ... };
72
- }
73
- ```
74
-
75
- ## Confidence Levels
76
-
77
- Each extracted item has a confidence score:
78
-
79
- - **high**: Explicit schema found (Zod, class-validator, JSON Schema)
80
- - **medium**: TypeScript types or framework decorators present
81
- - **low**: Inferred from naming conventions or partial information
82
- - **ambiguous**: Requires manual review
83
-
84
- ## License
85
-
86
- MIT
24
+ Import the root entrypoint from `@contractspec/lib.source-extractors`, or choose a documented subpath when you only need one part of the package surface.
25
+
26
+ ## Architecture
27
+
28
+ - `src/__fixtures__` is part of the package's public or composition surface.
29
+ - `src/__snapshots__` is part of the package's public or composition surface.
30
+ - `src/codegen` is part of the package's public or composition surface.
31
+ - `src/codegen.test.ts` is part of the package's public or composition surface.
32
+ - `src/detect.test.ts` is part of the package's public or composition surface.
33
+ - `src/detect.ts` is part of the package's public or composition surface.
34
+ - `src/edge-cases.test.ts` is part of the package's public or composition surface.
35
+ - `src/index.ts` is the root public barrel and package entrypoint.
36
+ - `src/types.ts` is shared public type definitions.
37
+
38
+ ## Public Entry Points
39
+
40
+ - Export `.` resolves through `./src/index.ts`.
41
+ - Export `./codegen` resolves through `./src/codegen/index.ts`.
42
+ - Export `./extractors` resolves through `./src/extractors/index.ts`.
43
+ - Export `./types` resolves through `./src/types.ts`.
44
+
45
+ ## Local Commands
46
+
47
+ - `bun run dev` — contractspec-bun-build dev
48
+ - `bun run build` — bun run prebuild && bun run build:bundle && bun run build:types
49
+ - `bun run test` — bun test
50
+ - `bun run lint` — bun lint:fix
51
+ - `bun run lint:check` — biome check .
52
+ - `bun run lint:fix` — biome check --write --unsafe --only=nursery/useSortedClasses . && biome check --write .
53
+ - `bun run typecheck` — tsc --noEmit
54
+ - `bun run publish:pkg` — bun publish --tolerate-republish --ignore-scripts --verbose
55
+ - `bun run publish:pkg:canary` — bun publish:pkg --tag canary
56
+ - `bun run clean` — rimraf dist .turbo
57
+ - `bun run build:bundle` contractspec-bun-build transpile
58
+ - `bun run build:types` — contractspec-bun-build types
59
+ - `bun run prebuild` — contractspec-bun-build prebuild
60
+
61
+ ## Recent Updates
62
+
63
+ - Replace eslint+prettier by biomejs to optimize speed.
64
+
65
+ ## Notes
66
+
67
+ - Extractor interface must support multiple frameworks — keep it generic.
68
+ - Codegen output must stay deterministic (same input → same output, always).
@@ -104,6 +104,42 @@ function generateOperationCode(endpoint, specName, options) {
104
104
  return lines.join(`
105
105
  `);
106
106
  }
107
+ // src/codegen/registry-gen.ts
108
+ function generateRegistry(operationFiles) {
109
+ const operationImports = operationFiles.filter((f) => f.type === "operation").map((f) => {
110
+ const name = f.path.replace(".ts", "").replace(/-/g, "_");
111
+ const specName = toPascalCase(name) + "Spec";
112
+ return { path: f.path, name, specName };
113
+ });
114
+ const lines = [
115
+ `/**`,
116
+ ` * Generated operation registry.`,
117
+ ` */`,
118
+ ``,
119
+ `import { OperationSpecRegistry } from '@contractspec/lib.contracts-spec';`,
120
+ ``
121
+ ];
122
+ for (const op of operationImports) {
123
+ const importPath = `./${op.path.replace(".ts", "")}`;
124
+ lines.push(`import { ${op.specName} } from '${importPath}';`);
125
+ }
126
+ lines.push(``);
127
+ lines.push(`export const operationRegistry = new OperationSpecRegistry();`);
128
+ lines.push(``);
129
+ for (const op of operationImports) {
130
+ lines.push(`operationRegistry.register(${op.specName});`);
131
+ }
132
+ lines.push(``);
133
+ return {
134
+ path: "registry.ts",
135
+ content: lines.join(`
136
+ `),
137
+ type: "registry"
138
+ };
139
+ }
140
+ function toPascalCase(str) {
141
+ return str.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
142
+ }
107
143
  // src/codegen/schema-gen.ts
108
144
  function generateSchema(schema, _options) {
109
145
  const fileName = `${toFileName2(schema.name)}.ts`;
@@ -184,42 +220,6 @@ function mapToZodType(tsType, optional) {
184
220
  }
185
221
  return optional ? `${zodType}.optional()` : zodType;
186
222
  }
187
- // src/codegen/registry-gen.ts
188
- function generateRegistry(operationFiles) {
189
- const operationImports = operationFiles.filter((f) => f.type === "operation").map((f) => {
190
- const name = f.path.replace(".ts", "").replace(/-/g, "_");
191
- const specName = toPascalCase(name) + "Spec";
192
- return { path: f.path, name, specName };
193
- });
194
- const lines = [
195
- `/**`,
196
- ` * Generated operation registry.`,
197
- ` */`,
198
- ``,
199
- `import { OperationSpecRegistry } from '@contractspec/lib.contracts-spec';`,
200
- ``
201
- ];
202
- for (const op of operationImports) {
203
- const importPath = `./${op.path.replace(".ts", "")}`;
204
- lines.push(`import { ${op.specName} } from '${importPath}';`);
205
- }
206
- lines.push(``);
207
- lines.push(`export const operationRegistry = new OperationSpecRegistry();`);
208
- lines.push(``);
209
- for (const op of operationImports) {
210
- lines.push(`operationRegistry.register(${op.specName});`);
211
- }
212
- lines.push(``);
213
- return {
214
- path: "registry.ts",
215
- content: lines.join(`
216
- `),
217
- type: "registry"
218
- };
219
- }
220
- function toPascalCase(str) {
221
- return str.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
222
- }
223
223
  export {
224
224
  generateSchemas,
225
225
  generateSchema,
@@ -317,101 +317,54 @@ class BaseExtractor {
317
317
  ctx.ir.schemas.push(schema);
318
318
  }
319
319
  }
320
- // src/extractors/nestjs/extractor.ts
320
+ // src/extractors/elysia/extractor.ts
321
321
  var PATTERNS = {
322
- controller: /@Controller\s*\(\s*['"`]([^'"`]*)['"`]\s*\)/g,
323
- route: /@(Get|Post|Put|Patch|Delete|Head|Options)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
324
- body: /@Body\s*\(\s*\)/g,
325
- param: /@Param\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
326
- query: /@Query\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
327
- dto: /class\s+(\w+(?:Dto|DTO|Request|Response|Input|Output))\s*\{/g,
328
- classValidator: /@(IsString|IsNumber|IsBoolean|IsArray|IsOptional|IsNotEmpty|Min|Max|Length|Matches)/g
322
+ route: /\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
323
+ tSchema: /body:\s*t\.\w+/g
329
324
  };
330
325
 
331
- class NestJsExtractor extends BaseExtractor {
332
- id = "nestjs";
333
- name = "NestJS Extractor";
334
- frameworks = ["nestjs"];
335
- priority = 20;
326
+ class ElysiaExtractor extends BaseExtractor {
327
+ id = "elysia";
328
+ name = "Elysia Extractor";
329
+ frameworks = ["elysia"];
330
+ priority = 15;
336
331
  async doExtract(ctx) {
337
332
  const { project, options, fs } = ctx;
338
333
  const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
339
334
  const files = await fs.glob(pattern, { cwd: project.rootPath });
340
335
  ctx.ir.stats.filesScanned = files.length;
341
336
  for (const file of files) {
342
- if (file.includes("node_modules") || file.includes(".spec.") || file.includes(".test.")) {
337
+ if (file.includes("node_modules") || file.includes(".test."))
343
338
  continue;
344
- }
345
339
  const fullPath = `${project.rootPath}/${file}`;
346
340
  const content = await fs.readFile(fullPath);
347
- await this.extractControllers(ctx, file, content);
348
- await this.extractDtos(ctx, file, content);
349
- }
350
- }
351
- async extractControllers(ctx, file, content) {
352
- const controllerMatches = [...content.matchAll(PATTERNS.controller)];
353
- for (const controllerMatch of controllerMatches) {
354
- const basePath = controllerMatch[1] || "";
355
- const controllerIndex = controllerMatch.index ?? 0;
356
- const afterDecorator = content.slice(controllerIndex);
357
- const classMatch = afterDecorator.match(/class\s+(\w+)/);
358
- const controllerName = classMatch?.[1] ?? "UnknownController";
359
- const nextController = content.indexOf("@Controller", controllerIndex + 1);
360
- const controllerBlock = nextController > 0 ? content.slice(controllerIndex, nextController) : content.slice(controllerIndex);
361
- const routeMatches = [...controllerBlock.matchAll(PATTERNS.route)];
362
- for (const routeMatch of routeMatches) {
363
- const method = routeMatch[1]?.toUpperCase();
364
- const routePath = routeMatch[2] || "";
365
- const fullPath = this.normalizePath(`/${basePath}/${routePath}`);
366
- const afterRoute = controllerBlock.slice(routeMatch.index ?? 0);
367
- const methodMatch = afterRoute.match(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*\w+(?:<[^>]+>)?)?\s*\{/);
368
- const handlerName = methodMatch?.[1] ?? "unknownHandler";
369
- const absoluteIndex = controllerIndex + (routeMatch.index ?? 0);
370
- const lineNumber = content.slice(0, absoluteIndex).split(`
371
- `).length;
372
- const hasBody = PATTERNS.body.test(afterRoute.slice(0, 200));
373
- const hasParams = PATTERNS.param.test(afterRoute.slice(0, 200));
374
- const hasQuery = PATTERNS.query.test(afterRoute.slice(0, 200));
375
- const endpoint = {
376
- id: this.generateEndpointId(method, fullPath, handlerName),
377
- method,
378
- path: fullPath,
379
- kind: this.methodToOpKind(method),
380
- handlerName,
381
- controllerName,
382
- source: this.createLocation(file, lineNumber, lineNumber + 10),
383
- confidence: this.createConfidence("medium", "decorator-hints"),
384
- frameworkMeta: {
385
- hasBody,
386
- hasParams,
387
- hasQuery
388
- }
389
- };
390
- this.addEndpoint(ctx, endpoint);
391
- }
341
+ if (!content.includes("elysia"))
342
+ continue;
343
+ await this.extractRoutes(ctx, file, content);
392
344
  }
393
345
  }
394
- async extractDtos(ctx, file, content) {
395
- const dtoMatches = [...content.matchAll(PATTERNS.dto)];
396
- for (const match of dtoMatches) {
397
- const name = match[1] ?? "UnknownDto";
346
+ async extractRoutes(ctx, file, content) {
347
+ const matches = [...content.matchAll(PATTERNS.route)];
348
+ for (const match of matches) {
349
+ const method = match[1]?.toUpperCase() ?? "GET";
350
+ const path = match[2] ?? "/";
398
351
  const index = match.index ?? 0;
399
352
  const lineNumber = content.slice(0, index).split(`
400
353
  `).length;
401
- const hasClassValidator = content.includes("class-validator") || content.includes("@IsString") || content.includes("@IsNumber");
402
- const schema = {
403
- id: this.generateSchemaId(name, file),
404
- name,
405
- schemaType: hasClassValidator ? "class-validator" : "typescript",
406
- source: this.createLocation(file, lineNumber, lineNumber + 20),
407
- confidence: this.createConfidence(hasClassValidator ? "high" : "medium", hasClassValidator ? "explicit-schema" : "inferred-types")
354
+ const afterMatch = content.slice(index, index + 500);
355
+ const hasTSchema = PATTERNS.tSchema.test(afterMatch);
356
+ const endpoint = {
357
+ id: this.generateEndpointId(method, path),
358
+ method,
359
+ path,
360
+ kind: this.methodToOpKind(method),
361
+ handlerName: "handler",
362
+ source: this.createLocation(file, lineNumber, lineNumber + 5),
363
+ confidence: this.createConfidence(hasTSchema ? "high" : "medium", hasTSchema ? "explicit-schema" : "decorator-hints")
408
364
  };
409
- this.addSchema(ctx, schema);
365
+ this.addEndpoint(ctx, endpoint);
410
366
  }
411
367
  }
412
- normalizePath(path) {
413
- return "/" + path.replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "");
414
- }
415
368
  }
416
369
  // src/extractors/express/extractor.ts
417
370
  var PATTERNS2 = {
@@ -560,119 +513,104 @@ class HonoExtractor extends BaseExtractor {
560
513
  }
561
514
  }
562
515
  }
563
- // src/extractors/elysia/extractor.ts
516
+ // src/extractors/nestjs/extractor.ts
564
517
  var PATTERNS5 = {
565
- route: /\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
566
- tSchema: /body:\s*t\.\w+/g
518
+ controller: /@Controller\s*\(\s*['"`]([^'"`]*)['"`]\s*\)/g,
519
+ route: /@(Get|Post|Put|Patch|Delete|Head|Options)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
520
+ body: /@Body\s*\(\s*\)/g,
521
+ param: /@Param\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
522
+ query: /@Query\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
523
+ dto: /class\s+(\w+(?:Dto|DTO|Request|Response|Input|Output))\s*\{/g,
524
+ classValidator: /@(IsString|IsNumber|IsBoolean|IsArray|IsOptional|IsNotEmpty|Min|Max|Length|Matches)/g
567
525
  };
568
526
 
569
- class ElysiaExtractor extends BaseExtractor {
570
- id = "elysia";
571
- name = "Elysia Extractor";
572
- frameworks = ["elysia"];
573
- priority = 15;
527
+ class NestJsExtractor extends BaseExtractor {
528
+ id = "nestjs";
529
+ name = "NestJS Extractor";
530
+ frameworks = ["nestjs"];
531
+ priority = 20;
574
532
  async doExtract(ctx) {
575
533
  const { project, options, fs } = ctx;
576
534
  const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
577
535
  const files = await fs.glob(pattern, { cwd: project.rootPath });
578
536
  ctx.ir.stats.filesScanned = files.length;
579
537
  for (const file of files) {
580
- if (file.includes("node_modules") || file.includes(".test."))
538
+ if (file.includes("node_modules") || file.includes(".spec.") || file.includes(".test.")) {
581
539
  continue;
540
+ }
582
541
  const fullPath = `${project.rootPath}/${file}`;
583
542
  const content = await fs.readFile(fullPath);
584
- if (!content.includes("elysia"))
585
- continue;
586
- await this.extractRoutes(ctx, file, content);
543
+ await this.extractControllers(ctx, file, content);
544
+ await this.extractDtos(ctx, file, content);
587
545
  }
588
546
  }
589
- async extractRoutes(ctx, file, content) {
590
- const matches = [...content.matchAll(PATTERNS5.route)];
591
- for (const match of matches) {
592
- const method = match[1]?.toUpperCase() ?? "GET";
593
- const path = match[2] ?? "/";
594
- const index = match.index ?? 0;
595
- const lineNumber = content.slice(0, index).split(`
547
+ async extractControllers(ctx, file, content) {
548
+ const controllerMatches = [...content.matchAll(PATTERNS5.controller)];
549
+ for (const controllerMatch of controllerMatches) {
550
+ const basePath = controllerMatch[1] || "";
551
+ const controllerIndex = controllerMatch.index ?? 0;
552
+ const afterDecorator = content.slice(controllerIndex);
553
+ const classMatch = afterDecorator.match(/class\s+(\w+)/);
554
+ const controllerName = classMatch?.[1] ?? "UnknownController";
555
+ const nextController = content.indexOf("@Controller", controllerIndex + 1);
556
+ const controllerBlock = nextController > 0 ? content.slice(controllerIndex, nextController) : content.slice(controllerIndex);
557
+ const routeMatches = [...controllerBlock.matchAll(PATTERNS5.route)];
558
+ for (const routeMatch of routeMatches) {
559
+ const method = routeMatch[1]?.toUpperCase();
560
+ const routePath = routeMatch[2] || "";
561
+ const fullPath = this.normalizePath(`/${basePath}/${routePath}`);
562
+ const afterRoute = controllerBlock.slice(routeMatch.index ?? 0);
563
+ const methodMatch = afterRoute.match(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*\w+(?:<[^>]+>)?)?\s*\{/);
564
+ const handlerName = methodMatch?.[1] ?? "unknownHandler";
565
+ const absoluteIndex = controllerIndex + (routeMatch.index ?? 0);
566
+ const lineNumber = content.slice(0, absoluteIndex).split(`
596
567
  `).length;
597
- const afterMatch = content.slice(index, index + 500);
598
- const hasTSchema = PATTERNS5.tSchema.test(afterMatch);
599
- const endpoint = {
600
- id: this.generateEndpointId(method, path),
601
- method,
602
- path,
603
- kind: this.methodToOpKind(method),
604
- handlerName: "handler",
605
- source: this.createLocation(file, lineNumber, lineNumber + 5),
606
- confidence: this.createConfidence(hasTSchema ? "high" : "medium", hasTSchema ? "explicit-schema" : "decorator-hints")
607
- };
608
- this.addEndpoint(ctx, endpoint);
609
- }
610
- }
611
- }
612
- // src/extractors/trpc/extractor.ts
613
- var PATTERNS6 = {
614
- procedure: /\.(query|mutation)\s*\(\s*(?:\{[^}]*\}|[^)]+)\)/gi,
615
- procedureName: /(\w+)\s*:\s*(?:publicProcedure|protectedProcedure|procedure)/g,
616
- zodInput: /\.input\s*\(\s*(\w+)/g,
617
- zodOutput: /\.output\s*\(\s*(\w+)/g
618
- };
619
-
620
- class TrpcExtractor extends BaseExtractor {
621
- id = "trpc";
622
- name = "tRPC Extractor";
623
- frameworks = ["trpc"];
624
- priority = 15;
625
- async doExtract(ctx) {
626
- const { project, options, fs } = ctx;
627
- const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
628
- const files = await fs.glob(pattern, { cwd: project.rootPath });
629
- ctx.ir.stats.filesScanned = files.length;
630
- for (const file of files) {
631
- if (file.includes("node_modules") || file.includes(".test."))
632
- continue;
633
- const fullPath = `${project.rootPath}/${file}`;
634
- const content = await fs.readFile(fullPath);
635
- if (!content.includes("trpc") && !content.includes("Procedure"))
636
- continue;
637
- await this.extractProcedures(ctx, file, content);
568
+ const hasBody = PATTERNS5.body.test(afterRoute.slice(0, 200));
569
+ const hasParams = PATTERNS5.param.test(afterRoute.slice(0, 200));
570
+ const hasQuery = PATTERNS5.query.test(afterRoute.slice(0, 200));
571
+ const endpoint = {
572
+ id: this.generateEndpointId(method, fullPath, handlerName),
573
+ method,
574
+ path: fullPath,
575
+ kind: this.methodToOpKind(method),
576
+ handlerName,
577
+ controllerName,
578
+ source: this.createLocation(file, lineNumber, lineNumber + 10),
579
+ confidence: this.createConfidence("medium", "decorator-hints"),
580
+ frameworkMeta: {
581
+ hasBody,
582
+ hasParams,
583
+ hasQuery
584
+ }
585
+ };
586
+ this.addEndpoint(ctx, endpoint);
587
+ }
638
588
  }
639
589
  }
640
- async extractProcedures(ctx, file, content) {
641
- const nameMatches = [...content.matchAll(PATTERNS6.procedureName)];
642
- for (const match of nameMatches) {
643
- const procedureName = match[1] ?? "unknownProcedure";
590
+ async extractDtos(ctx, file, content) {
591
+ const dtoMatches = [...content.matchAll(PATTERNS5.dto)];
592
+ for (const match of dtoMatches) {
593
+ const name = match[1] ?? "UnknownDto";
644
594
  const index = match.index ?? 0;
645
595
  const lineNumber = content.slice(0, index).split(`
646
596
  `).length;
647
- const afterMatch = content.slice(index, index + 500);
648
- const isQuery = afterMatch.includes(".query(");
649
- const isMutation = afterMatch.includes(".mutation(");
650
- if (!isQuery && !isMutation)
651
- continue;
652
- const hasZodInput = PATTERNS6.zodInput.test(afterMatch);
653
- const hasZodOutput = PATTERNS6.zodOutput.test(afterMatch);
654
- const hasSchema = hasZodInput || hasZodOutput;
655
- const method = isMutation ? "POST" : "GET";
656
- const endpoint = {
657
- id: `trpc.${procedureName}`,
658
- method,
659
- path: `/trpc/${procedureName}`,
660
- kind: isMutation ? "command" : "query",
661
- handlerName: procedureName,
662
- source: this.createLocation(file, lineNumber, lineNumber + 10),
663
- confidence: this.createConfidence(hasSchema ? "high" : "medium", hasSchema ? "explicit-schema" : "inferred-types"),
664
- frameworkMeta: {
665
- procedureType: isMutation ? "mutation" : "query",
666
- hasInput: hasZodInput,
667
- hasOutput: hasZodOutput
668
- }
597
+ const hasClassValidator = content.includes("class-validator") || content.includes("@IsString") || content.includes("@IsNumber");
598
+ const schema = {
599
+ id: this.generateSchemaId(name, file),
600
+ name,
601
+ schemaType: hasClassValidator ? "class-validator" : "typescript",
602
+ source: this.createLocation(file, lineNumber, lineNumber + 20),
603
+ confidence: this.createConfidence(hasClassValidator ? "high" : "medium", hasClassValidator ? "explicit-schema" : "inferred-types")
669
604
  };
670
- this.addEndpoint(ctx, endpoint);
605
+ this.addSchema(ctx, schema);
671
606
  }
672
607
  }
608
+ normalizePath(path) {
609
+ return "/" + path.replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "");
610
+ }
673
611
  }
674
612
  // src/extractors/next-api/extractor.ts
675
- var PATTERNS7 = {
613
+ var PATTERNS6 = {
676
614
  appRouterExport: /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/gi,
677
615
  pagesHandler: /export\s+default\s+(?:async\s+)?function/g,
678
616
  zodSchema: /z\.\w+\(/g
@@ -706,13 +644,13 @@ class NextApiExtractor extends BaseExtractor {
706
644
  async extractAppRoutes(ctx, file, content) {
707
645
  const pathMatch = file.match(/app\/api\/(.+)\/route\.ts$/);
708
646
  const routePath = pathMatch ? `/api/${pathMatch[1]}` : "/api";
709
- const matches = [...content.matchAll(PATTERNS7.appRouterExport)];
647
+ const matches = [...content.matchAll(PATTERNS6.appRouterExport)];
710
648
  for (const match of matches) {
711
649
  const method = match[1]?.toUpperCase() ?? "GET";
712
650
  const index = match.index ?? 0;
713
651
  const lineNumber = content.slice(0, index).split(`
714
652
  `).length;
715
- const hasZod = PATTERNS7.zodSchema.test(content);
653
+ const hasZod = PATTERNS6.zodSchema.test(content);
716
654
  const endpoint = {
717
655
  id: this.generateEndpointId(method, routePath),
718
656
  method,
@@ -729,10 +667,10 @@ class NextApiExtractor extends BaseExtractor {
729
667
  async extractPagesRoutes(ctx, file, content) {
730
668
  const pathMatch = file.match(/pages\/api\/(.+)\.ts$/);
731
669
  const routePath = pathMatch ? `/api/${pathMatch[1]}` : "/api";
732
- if (!PATTERNS7.pagesHandler.test(content))
670
+ if (!PATTERNS6.pagesHandler.test(content))
733
671
  return;
734
672
  const lineNumber = 1;
735
- const _hasZod = PATTERNS7.zodSchema.test(content);
673
+ const _hasZod = PATTERNS6.zodSchema.test(content);
736
674
  const methods = ["GET", "POST"];
737
675
  for (const method of methods) {
738
676
  const endpoint = {
@@ -749,6 +687,68 @@ class NextApiExtractor extends BaseExtractor {
749
687
  }
750
688
  }
751
689
  }
690
+ // src/extractors/trpc/extractor.ts
691
+ var PATTERNS7 = {
692
+ procedure: /\.(query|mutation)\s*\(\s*(?:\{[^}]*\}|[^)]+)\)/gi,
693
+ procedureName: /(\w+)\s*:\s*(?:publicProcedure|protectedProcedure|procedure)/g,
694
+ zodInput: /\.input\s*\(\s*(\w+)/g,
695
+ zodOutput: /\.output\s*\(\s*(\w+)/g
696
+ };
697
+
698
+ class TrpcExtractor extends BaseExtractor {
699
+ id = "trpc";
700
+ name = "tRPC Extractor";
701
+ frameworks = ["trpc"];
702
+ priority = 15;
703
+ async doExtract(ctx) {
704
+ const { project, options, fs } = ctx;
705
+ const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
706
+ const files = await fs.glob(pattern, { cwd: project.rootPath });
707
+ ctx.ir.stats.filesScanned = files.length;
708
+ for (const file of files) {
709
+ if (file.includes("node_modules") || file.includes(".test."))
710
+ continue;
711
+ const fullPath = `${project.rootPath}/${file}`;
712
+ const content = await fs.readFile(fullPath);
713
+ if (!content.includes("trpc") && !content.includes("Procedure"))
714
+ continue;
715
+ await this.extractProcedures(ctx, file, content);
716
+ }
717
+ }
718
+ async extractProcedures(ctx, file, content) {
719
+ const nameMatches = [...content.matchAll(PATTERNS7.procedureName)];
720
+ for (const match of nameMatches) {
721
+ const procedureName = match[1] ?? "unknownProcedure";
722
+ const index = match.index ?? 0;
723
+ const lineNumber = content.slice(0, index).split(`
724
+ `).length;
725
+ const afterMatch = content.slice(index, index + 500);
726
+ const isQuery = afterMatch.includes(".query(");
727
+ const isMutation = afterMatch.includes(".mutation(");
728
+ if (!isQuery && !isMutation)
729
+ continue;
730
+ const hasZodInput = PATTERNS7.zodInput.test(afterMatch);
731
+ const hasZodOutput = PATTERNS7.zodOutput.test(afterMatch);
732
+ const hasSchema = hasZodInput || hasZodOutput;
733
+ const method = isMutation ? "POST" : "GET";
734
+ const endpoint = {
735
+ id: `trpc.${procedureName}`,
736
+ method,
737
+ path: `/trpc/${procedureName}`,
738
+ kind: isMutation ? "command" : "query",
739
+ handlerName: procedureName,
740
+ source: this.createLocation(file, lineNumber, lineNumber + 10),
741
+ confidence: this.createConfidence(hasSchema ? "high" : "medium", hasSchema ? "explicit-schema" : "inferred-types"),
742
+ frameworkMeta: {
743
+ procedureType: isMutation ? "mutation" : "query",
744
+ hasInput: hasZodInput,
745
+ hasOutput: hasZodOutput
746
+ }
747
+ };
748
+ this.addEndpoint(ctx, endpoint);
749
+ }
750
+ }
751
+ }
752
752
  // src/extractors/zod/extractor.ts
753
753
  var PATTERNS8 = {
754
754
  zodSchema: /(?:export\s+)?const\s+(\w+)\s*=\s*z\.(?:object|string|number|boolean|array|enum|union|intersection|literal|tuple|record)/g,