@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 +58 -76
- package/dist/browser/codegen/index.js +36 -36
- package/dist/browser/extractors/index.js +167 -167
- package/dist/browser/index.js +203 -203
- package/dist/codegen/index.d.ts +1 -1
- package/dist/codegen/index.js +36 -36
- package/dist/codegen/schema-gen.d.ts +1 -1
- package/dist/extractors/index.d.ts +4 -4
- package/dist/extractors/index.js +167 -167
- package/dist/index.d.ts +3 -3
- package/dist/index.js +203 -203
- package/dist/node/codegen/index.js +36 -36
- package/dist/node/extractors/index.js +167 -167
- package/dist/node/index.js +203 -203
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,86 +1,68 @@
|
|
|
1
1
|
# @contractspec/lib.source-extractors
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Website: https://contractspec.io
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Extract contract candidates from TypeScript source code across multiple frameworks (NestJS, Express, Fastify, Hono, Elysia, tRPC, Next.js).**
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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/
|
|
320
|
+
// src/extractors/elysia/extractor.ts
|
|
321
321
|
var PATTERNS = {
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
332
|
-
id = "
|
|
333
|
-
name = "
|
|
334
|
-
frameworks = ["
|
|
335
|
-
priority =
|
|
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(".
|
|
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
|
-
|
|
348
|
-
|
|
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
|
|
395
|
-
const
|
|
396
|
-
for (const match of
|
|
397
|
-
const
|
|
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
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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.
|
|
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/
|
|
516
|
+
// src/extractors/nestjs/extractor.ts
|
|
564
517
|
var PATTERNS5 = {
|
|
565
|
-
|
|
566
|
-
|
|
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
|
|
570
|
-
id = "
|
|
571
|
-
name = "
|
|
572
|
-
frameworks = ["
|
|
573
|
-
priority =
|
|
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
|
-
|
|
585
|
-
|
|
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
|
|
590
|
-
const
|
|
591
|
-
for (const
|
|
592
|
-
const
|
|
593
|
-
const
|
|
594
|
-
const
|
|
595
|
-
const
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
|
641
|
-
const
|
|
642
|
-
for (const match of
|
|
643
|
-
const
|
|
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
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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 =
|
|
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 (!
|
|
670
|
+
if (!PATTERNS6.pagesHandler.test(content))
|
|
733
671
|
return;
|
|
734
672
|
const lineNumber = 1;
|
|
735
|
-
const _hasZod =
|
|
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,
|