@contractspec/lib.source-extractors 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/browser/codegen/index.js +225 -0
  2. package/dist/browser/extractors/index.js +835 -0
  3. package/dist/browser/index.js +1215 -0
  4. package/dist/browser/types.js +0 -0
  5. package/dist/codegen/index.d.ts +9 -11
  6. package/dist/codegen/index.d.ts.map +1 -1
  7. package/dist/codegen/index.js +223 -14
  8. package/dist/codegen/operation-gen.d.ts +9 -8
  9. package/dist/codegen/operation-gen.d.ts.map +1 -1
  10. package/dist/codegen/registry-gen.d.ts +7 -6
  11. package/dist/codegen/registry-gen.d.ts.map +1 -1
  12. package/dist/codegen/schema-gen.d.ts +9 -8
  13. package/dist/codegen/schema-gen.d.ts.map +1 -1
  14. package/dist/codegen/types.d.ts +29 -32
  15. package/dist/codegen/types.d.ts.map +1 -1
  16. package/dist/detect.d.ts +19 -17
  17. package/dist/detect.d.ts.map +1 -1
  18. package/dist/extract.d.ts +10 -8
  19. package/dist/extract.d.ts.map +1 -1
  20. package/dist/extractors/base.d.ts +76 -75
  21. package/dist/extractors/base.d.ts.map +1 -1
  22. package/dist/extractors/elysia/extractor.d.ts +15 -12
  23. package/dist/extractors/elysia/extractor.d.ts.map +1 -1
  24. package/dist/extractors/express/extractor.d.ts +16 -12
  25. package/dist/extractors/express/extractor.d.ts.map +1 -1
  26. package/dist/extractors/fastify/extractor.d.ts +16 -12
  27. package/dist/extractors/fastify/extractor.d.ts.map +1 -1
  28. package/dist/extractors/hono/extractor.d.ts +15 -12
  29. package/dist/extractors/hono/extractor.d.ts.map +1 -1
  30. package/dist/extractors/index.d.ts +16 -17
  31. package/dist/extractors/index.d.ts.map +1 -1
  32. package/dist/extractors/index.js +834 -40
  33. package/dist/extractors/nestjs/extractor.d.ts +28 -23
  34. package/dist/extractors/nestjs/extractor.d.ts.map +1 -1
  35. package/dist/extractors/next-api/extractor.d.ts +16 -13
  36. package/dist/extractors/next-api/extractor.d.ts.map +1 -1
  37. package/dist/extractors/trpc/extractor.d.ts +16 -12
  38. package/dist/extractors/trpc/extractor.d.ts.map +1 -1
  39. package/dist/extractors/zod/extractor.d.ts +15 -13
  40. package/dist/extractors/zod/extractor.d.ts.map +1 -1
  41. package/dist/index.d.ts +30 -7
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +1215 -6
  44. package/dist/node/codegen/index.js +225 -0
  45. package/dist/node/extractors/index.js +835 -0
  46. package/dist/node/index.js +1215 -0
  47. package/dist/node/types.js +0 -0
  48. package/dist/registry.d.ts +69 -68
  49. package/dist/registry.d.ts.map +1 -1
  50. package/dist/types.d.ts +182 -185
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/types.js +1 -0
  53. package/package.json +60 -21
  54. package/dist/_virtual/_rolldown/runtime.js +0 -18
  55. package/dist/codegen/index.js.map +0 -1
  56. package/dist/codegen/operation-gen.js +0 -91
  57. package/dist/codegen/operation-gen.js.map +0 -1
  58. package/dist/codegen/registry-gen.js +0 -47
  59. package/dist/codegen/registry-gen.js.map +0 -1
  60. package/dist/codegen/schema-gen.js +0 -93
  61. package/dist/codegen/schema-gen.js.map +0 -1
  62. package/dist/detect.js +0 -177
  63. package/dist/detect.js.map +0 -1
  64. package/dist/extract.js +0 -125
  65. package/dist/extract.js.map +0 -1
  66. package/dist/extractors/base.js +0 -152
  67. package/dist/extractors/base.js.map +0 -1
  68. package/dist/extractors/elysia/extractor.js +0 -58
  69. package/dist/extractors/elysia/extractor.js.map +0 -1
  70. package/dist/extractors/express/extractor.js +0 -61
  71. package/dist/extractors/express/extractor.js.map +0 -1
  72. package/dist/extractors/fastify/extractor.js +0 -61
  73. package/dist/extractors/fastify/extractor.js.map +0 -1
  74. package/dist/extractors/hono/extractor.js +0 -57
  75. package/dist/extractors/hono/extractor.js.map +0 -1
  76. package/dist/extractors/index.js.map +0 -1
  77. package/dist/extractors/nestjs/extractor.js +0 -118
  78. package/dist/extractors/nestjs/extractor.js.map +0 -1
  79. package/dist/extractors/next-api/extractor.js +0 -80
  80. package/dist/extractors/next-api/extractor.js.map +0 -1
  81. package/dist/extractors/trpc/extractor.js +0 -71
  82. package/dist/extractors/trpc/extractor.js.map +0 -1
  83. package/dist/extractors/zod/extractor.js +0 -69
  84. package/dist/extractors/zod/extractor.js.map +0 -1
  85. package/dist/registry.js +0 -87
  86. package/dist/registry.js.map +0 -1
@@ -1,42 +1,836 @@
1
- import { __exportAll } from "../_virtual/_rolldown/runtime.js";
2
- import { extractorRegistry } from "../registry.js";
3
- import { BaseExtractor } from "./base.js";
4
- import { NestJsExtractor } from "./nestjs/extractor.js";
5
- import { ExpressExtractor } from "./express/extractor.js";
6
- import { FastifyExtractor } from "./fastify/extractor.js";
7
- import { HonoExtractor } from "./hono/extractor.js";
8
- import { ElysiaExtractor } from "./elysia/extractor.js";
9
- import { TrpcExtractor } from "./trpc/extractor.js";
10
- import { NextApiExtractor } from "./next-api/extractor.js";
11
- import { ZodSchemaExtractor } from "./zod/extractor.js";
12
-
13
- //#region src/extractors/index.ts
14
- var extractors_exports = /* @__PURE__ */ __exportAll({
15
- BaseExtractor: () => BaseExtractor,
16
- ElysiaExtractor: () => ElysiaExtractor,
17
- ExpressExtractor: () => ExpressExtractor,
18
- FastifyExtractor: () => FastifyExtractor,
19
- HonoExtractor: () => HonoExtractor,
20
- NestJsExtractor: () => NestJsExtractor,
21
- NextApiExtractor: () => NextApiExtractor,
22
- TrpcExtractor: () => TrpcExtractor,
23
- ZodSchemaExtractor: () => ZodSchemaExtractor,
24
- registerAllExtractors: () => registerAllExtractors
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true,
8
+ configurable: true,
9
+ set: (newValue) => all[name] = () => newValue
10
+ });
11
+ };
12
+
13
+ // src/extractors/index.ts
14
+ var exports_extractors = {};
15
+ __export(exports_extractors, {
16
+ registerAllExtractors: () => registerAllExtractors,
17
+ ZodSchemaExtractor: () => ZodSchemaExtractor,
18
+ TrpcExtractor: () => TrpcExtractor,
19
+ NextApiExtractor: () => NextApiExtractor,
20
+ NestJsExtractor: () => NestJsExtractor,
21
+ HonoExtractor: () => HonoExtractor,
22
+ FastifyExtractor: () => FastifyExtractor,
23
+ ExpressExtractor: () => ExpressExtractor,
24
+ ElysiaExtractor: () => ElysiaExtractor,
25
+ BaseExtractor: () => BaseExtractor
25
26
  });
26
- /**
27
- * Register all built-in extractors with the global registry.
28
- */
27
+
28
+ // src/registry.ts
29
+ class ExtractorRegistry {
30
+ extractors = new Map;
31
+ register(extractor) {
32
+ this.extractors.set(extractor.id, extractor);
33
+ }
34
+ unregister(id) {
35
+ return this.extractors.delete(id);
36
+ }
37
+ get(id) {
38
+ return this.extractors.get(id);
39
+ }
40
+ getAll() {
41
+ return Array.from(this.extractors.values());
42
+ }
43
+ async findMatching(project) {
44
+ const matches = [];
45
+ for (const extractor of this.extractors.values()) {
46
+ try {
47
+ if (await extractor.detect(project)) {
48
+ matches.push(extractor);
49
+ }
50
+ } catch {}
51
+ }
52
+ return matches.sort((a, b) => b.priority - a.priority);
53
+ }
54
+ findByFramework(framework) {
55
+ const matches = [];
56
+ for (const extractor of this.extractors.values()) {
57
+ if (extractor.frameworks.includes(framework)) {
58
+ matches.push(extractor);
59
+ }
60
+ }
61
+ return matches.sort((a, b) => b.priority - a.priority);
62
+ }
63
+ findForFramework(framework) {
64
+ return this.findByFramework(framework);
65
+ }
66
+ hasExtractorFor(framework) {
67
+ for (const extractor of this.extractors.values()) {
68
+ if (extractor.frameworks.includes(framework) || extractor.id === framework) {
69
+ return true;
70
+ }
71
+ }
72
+ return false;
73
+ }
74
+ getSupportedFrameworks() {
75
+ const frameworks = new Set;
76
+ for (const extractor of this.extractors.values()) {
77
+ frameworks.add(extractor.id);
78
+ for (const fw of extractor.frameworks) {
79
+ frameworks.add(fw);
80
+ }
81
+ }
82
+ return Array.from(frameworks);
83
+ }
84
+ }
85
+ var extractorRegistry = new ExtractorRegistry;
86
+ function registerBuiltInExtractors() {}
87
+
88
+ // src/extract.ts
89
+ async function extractFromProject(project, options = {}) {
90
+ let extractors;
91
+ if (options.framework) {
92
+ extractors = extractorRegistry.findByFramework(options.framework);
93
+ if (extractors.length === 0) {
94
+ return {
95
+ success: false,
96
+ errors: [
97
+ {
98
+ code: "EXTRACTOR_NOT_FOUND",
99
+ message: `No extractor found for framework: ${options.framework}`,
100
+ recoverable: false
101
+ }
102
+ ]
103
+ };
104
+ }
105
+ } else {
106
+ extractors = await extractorRegistry.findMatching(project);
107
+ if (extractors.length === 0) {
108
+ return {
109
+ success: false,
110
+ errors: [
111
+ {
112
+ code: "NO_FRAMEWORK_DETECTED",
113
+ message: "No supported framework detected in project",
114
+ recoverable: false
115
+ }
116
+ ]
117
+ };
118
+ }
119
+ }
120
+ const extractor = extractors[0];
121
+ if (!extractor) {
122
+ return {
123
+ success: false,
124
+ errors: [
125
+ {
126
+ code: "NO_EXTRACTOR",
127
+ message: "No extractor available",
128
+ recoverable: false
129
+ }
130
+ ]
131
+ };
132
+ }
133
+ const result = await extractor.extract(project, options);
134
+ return result;
135
+ }
136
+ function mergeIRs(irs) {
137
+ if (irs.length === 0) {
138
+ throw new Error("Cannot merge empty IR array");
139
+ }
140
+ if (irs.length === 1) {
141
+ if (!irs[0])
142
+ throw new Error("First IR is undefined");
143
+ return irs[0];
144
+ }
145
+ const first = irs[0];
146
+ if (!first)
147
+ throw new Error("First IR is undefined");
148
+ const merged = {
149
+ version: "1.0",
150
+ extractedAt: new Date().toISOString(),
151
+ project: first.project,
152
+ endpoints: [],
153
+ schemas: [],
154
+ errors: [],
155
+ events: [],
156
+ ambiguities: [],
157
+ stats: {
158
+ filesScanned: 0,
159
+ endpointsFound: 0,
160
+ schemasFound: 0,
161
+ errorsFound: 0,
162
+ eventsFound: 0,
163
+ ambiguitiesFound: 0,
164
+ highConfidence: 0,
165
+ mediumConfidence: 0,
166
+ lowConfidence: 0
167
+ }
168
+ };
169
+ for (const ir of irs) {
170
+ merged.endpoints.push(...ir.endpoints);
171
+ merged.schemas.push(...ir.schemas);
172
+ merged.errors.push(...ir.errors);
173
+ merged.events.push(...ir.events);
174
+ merged.ambiguities.push(...ir.ambiguities);
175
+ merged.stats.filesScanned += ir.stats.filesScanned;
176
+ merged.stats.endpointsFound += ir.stats.endpointsFound;
177
+ merged.stats.schemasFound += ir.stats.schemasFound;
178
+ merged.stats.errorsFound += ir.stats.errorsFound;
179
+ merged.stats.eventsFound += ir.stats.eventsFound;
180
+ merged.stats.ambiguitiesFound += ir.stats.ambiguitiesFound;
181
+ merged.stats.highConfidence += ir.stats.highConfidence;
182
+ merged.stats.mediumConfidence += ir.stats.mediumConfidence;
183
+ merged.stats.lowConfidence += ir.stats.lowConfidence;
184
+ }
185
+ return merged;
186
+ }
187
+ function createEmptyIR(project) {
188
+ return {
189
+ version: "1.0",
190
+ extractedAt: new Date().toISOString(),
191
+ project,
192
+ endpoints: [],
193
+ schemas: [],
194
+ errors: [],
195
+ events: [],
196
+ ambiguities: [],
197
+ stats: {
198
+ filesScanned: 0,
199
+ endpointsFound: 0,
200
+ schemasFound: 0,
201
+ errorsFound: 0,
202
+ eventsFound: 0,
203
+ ambiguitiesFound: 0,
204
+ highConfidence: 0,
205
+ mediumConfidence: 0,
206
+ lowConfidence: 0
207
+ }
208
+ };
209
+ }
210
+
211
+ // src/extractors/base.ts
212
+ class BaseExtractor {
213
+ priority = 10;
214
+ fs;
215
+ setFs(fs) {
216
+ this.fs = fs;
217
+ }
218
+ async detect(project) {
219
+ return project.frameworks.some((fw) => this.frameworks.includes(fw.id));
220
+ }
221
+ async extract(project, options) {
222
+ if (!this.fs) {
223
+ return {
224
+ success: false,
225
+ errors: [
226
+ {
227
+ code: "NO_FS_ADAPTER",
228
+ message: "File system adapter not configured",
229
+ recoverable: false
230
+ }
231
+ ]
232
+ };
233
+ }
234
+ const ir = createEmptyIR(project);
235
+ const ctx = {
236
+ project,
237
+ options,
238
+ fs: this.fs,
239
+ ir
240
+ };
241
+ try {
242
+ await this.doExtract(ctx);
243
+ this.calculateStats(ir);
244
+ return { success: true, ir };
245
+ } catch (error) {
246
+ return {
247
+ success: false,
248
+ errors: [
249
+ {
250
+ code: "EXTRACTION_ERROR",
251
+ message: error instanceof Error ? error.message : String(error),
252
+ recoverable: false
253
+ }
254
+ ]
255
+ };
256
+ }
257
+ }
258
+ calculateStats(ir) {
259
+ ir.stats.endpointsFound = ir.endpoints.length;
260
+ ir.stats.schemasFound = ir.schemas.length;
261
+ ir.stats.errorsFound = ir.errors.length;
262
+ ir.stats.eventsFound = ir.events.length;
263
+ ir.stats.ambiguitiesFound = ir.ambiguities.length;
264
+ const allItems = [
265
+ ...ir.endpoints,
266
+ ...ir.schemas,
267
+ ...ir.errors,
268
+ ...ir.events
269
+ ];
270
+ for (const item of allItems) {
271
+ switch (item.confidence.level) {
272
+ case "high":
273
+ ir.stats.highConfidence++;
274
+ break;
275
+ case "medium":
276
+ ir.stats.mediumConfidence++;
277
+ break;
278
+ case "low":
279
+ case "ambiguous":
280
+ ir.stats.lowConfidence++;
281
+ break;
282
+ }
283
+ }
284
+ }
285
+ generateEndpointId(method, path, handlerName) {
286
+ const pathPart = path.replace(/^\//, "").replace(/\//g, ".").replace(/:/g, "").replace(/\{/g, "").replace(/\}/g, "");
287
+ const base = `${method.toLowerCase()}.${pathPart}`;
288
+ return handlerName ? `${base}.${handlerName}` : base;
289
+ }
290
+ generateSchemaId(name, file) {
291
+ const filePart = file.replace(/\.ts$/, "").replace(/\//g, ".").replace(/^\.+/, "");
292
+ return `${filePart}.${name}`;
293
+ }
294
+ methodToOpKind(method) {
295
+ switch (method) {
296
+ case "GET":
297
+ case "HEAD":
298
+ case "OPTIONS":
299
+ return "query";
300
+ default:
301
+ return "command";
302
+ }
303
+ }
304
+ createLocation(file, startLine, endLine) {
305
+ return { file, startLine, endLine };
306
+ }
307
+ createConfidence(level, ...reasons) {
308
+ return { level, reasons };
309
+ }
310
+ addEndpoint(ctx, endpoint) {
311
+ ctx.ir.endpoints.push(endpoint);
312
+ }
313
+ addSchema(ctx, schema) {
314
+ ctx.ir.schemas.push(schema);
315
+ }
316
+ }
317
+ // src/extractors/nestjs/extractor.ts
318
+ var PATTERNS = {
319
+ controller: /@Controller\s*\(\s*['"`]([^'"`]*)['"`]\s*\)/g,
320
+ route: /@(Get|Post|Put|Patch|Delete|Head|Options)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
321
+ body: /@Body\s*\(\s*\)/g,
322
+ param: /@Param\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
323
+ query: /@Query\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
324
+ dto: /class\s+(\w+(?:Dto|DTO|Request|Response|Input|Output))\s*\{/g,
325
+ classValidator: /@(IsString|IsNumber|IsBoolean|IsArray|IsOptional|IsNotEmpty|Min|Max|Length|Matches)/g
326
+ };
327
+
328
+ class NestJsExtractor extends BaseExtractor {
329
+ id = "nestjs";
330
+ name = "NestJS Extractor";
331
+ frameworks = ["nestjs"];
332
+ priority = 20;
333
+ async doExtract(ctx) {
334
+ const { project, options, fs } = ctx;
335
+ const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
336
+ const files = await fs.glob(pattern, { cwd: project.rootPath });
337
+ ctx.ir.stats.filesScanned = files.length;
338
+ for (const file of files) {
339
+ if (file.includes("node_modules") || file.includes(".spec.") || file.includes(".test.")) {
340
+ continue;
341
+ }
342
+ const fullPath = `${project.rootPath}/${file}`;
343
+ const content = await fs.readFile(fullPath);
344
+ await this.extractControllers(ctx, file, content);
345
+ await this.extractDtos(ctx, file, content);
346
+ }
347
+ }
348
+ async extractControllers(ctx, file, content) {
349
+ const controllerMatches = [...content.matchAll(PATTERNS.controller)];
350
+ for (const controllerMatch of controllerMatches) {
351
+ const basePath = controllerMatch[1] || "";
352
+ const controllerIndex = controllerMatch.index ?? 0;
353
+ const afterDecorator = content.slice(controllerIndex);
354
+ const classMatch = afterDecorator.match(/class\s+(\w+)/);
355
+ const controllerName = classMatch?.[1] ?? "UnknownController";
356
+ const nextController = content.indexOf("@Controller", controllerIndex + 1);
357
+ const controllerBlock = nextController > 0 ? content.slice(controllerIndex, nextController) : content.slice(controllerIndex);
358
+ const routeMatches = [...controllerBlock.matchAll(PATTERNS.route)];
359
+ for (const routeMatch of routeMatches) {
360
+ const method = routeMatch[1]?.toUpperCase();
361
+ const routePath = routeMatch[2] || "";
362
+ const fullPath = this.normalizePath(`/${basePath}/${routePath}`);
363
+ const afterRoute = controllerBlock.slice(routeMatch.index ?? 0);
364
+ const methodMatch = afterRoute.match(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*\w+(?:<[^>]+>)?)?\s*\{/);
365
+ const handlerName = methodMatch?.[1] ?? "unknownHandler";
366
+ const absoluteIndex = controllerIndex + (routeMatch.index ?? 0);
367
+ const lineNumber = content.slice(0, absoluteIndex).split(`
368
+ `).length;
369
+ const hasBody = PATTERNS.body.test(afterRoute.slice(0, 200));
370
+ const hasParams = PATTERNS.param.test(afterRoute.slice(0, 200));
371
+ const hasQuery = PATTERNS.query.test(afterRoute.slice(0, 200));
372
+ const endpoint = {
373
+ id: this.generateEndpointId(method, fullPath, handlerName),
374
+ method,
375
+ path: fullPath,
376
+ kind: this.methodToOpKind(method),
377
+ handlerName,
378
+ controllerName,
379
+ source: this.createLocation(file, lineNumber, lineNumber + 10),
380
+ confidence: this.createConfidence("medium", "decorator-hints"),
381
+ frameworkMeta: {
382
+ hasBody,
383
+ hasParams,
384
+ hasQuery
385
+ }
386
+ };
387
+ this.addEndpoint(ctx, endpoint);
388
+ }
389
+ }
390
+ }
391
+ async extractDtos(ctx, file, content) {
392
+ const dtoMatches = [...content.matchAll(PATTERNS.dto)];
393
+ for (const match of dtoMatches) {
394
+ const name = match[1] ?? "UnknownDto";
395
+ const index = match.index ?? 0;
396
+ const lineNumber = content.slice(0, index).split(`
397
+ `).length;
398
+ const hasClassValidator = content.includes("class-validator") || content.includes("@IsString") || content.includes("@IsNumber");
399
+ const schema = {
400
+ id: this.generateSchemaId(name, file),
401
+ name,
402
+ schemaType: hasClassValidator ? "class-validator" : "typescript",
403
+ source: this.createLocation(file, lineNumber, lineNumber + 20),
404
+ confidence: this.createConfidence(hasClassValidator ? "high" : "medium", hasClassValidator ? "explicit-schema" : "inferred-types")
405
+ };
406
+ this.addSchema(ctx, schema);
407
+ }
408
+ }
409
+ normalizePath(path) {
410
+ return "/" + path.replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "");
411
+ }
412
+ }
413
+ // src/extractors/express/extractor.ts
414
+ var PATTERNS2 = {
415
+ route: /(?:app|router)\.(get|post|put|patch|delete|head|options)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
416
+ routerUse: /(?:app|router)\.use\s*\(\s*['"`]([^'"`]+)['"`]/gi,
417
+ zodValidate: /validate\s*\(\s*(\w+)\s*\)/g
418
+ };
419
+
420
+ class ExpressExtractor extends BaseExtractor {
421
+ id = "express";
422
+ name = "Express Extractor";
423
+ frameworks = ["express"];
424
+ priority = 15;
425
+ async doExtract(ctx) {
426
+ const { project, options, fs } = ctx;
427
+ const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
428
+ const files = await fs.glob(pattern, { cwd: project.rootPath });
429
+ ctx.ir.stats.filesScanned = files.length;
430
+ for (const file of files) {
431
+ if (file.includes("node_modules") || file.includes(".test."))
432
+ continue;
433
+ const fullPath = `${project.rootPath}/${file}`;
434
+ const content = await fs.readFile(fullPath);
435
+ await this.extractRoutes(ctx, file, content);
436
+ }
437
+ }
438
+ async extractRoutes(ctx, file, content) {
439
+ const matches = [...content.matchAll(PATTERNS2.route)];
440
+ for (const match of matches) {
441
+ const method = match[1]?.toUpperCase() ?? "GET";
442
+ const path = match[2] ?? "/";
443
+ const index = match.index ?? 0;
444
+ const lineNumber = content.slice(0, index).split(`
445
+ `).length;
446
+ const afterMatch = content.slice(index, index + 500);
447
+ const handlerMatch = afterMatch.match(/(?:async\s+)?(?:function\s+)?(\w+)|,\s*(\w+)\s*\)/);
448
+ const handlerName = handlerMatch?.[1] ?? handlerMatch?.[2] ?? "handler";
449
+ const hasZodValidation = PATTERNS2.zodValidate.test(afterMatch);
450
+ const endpoint = {
451
+ id: this.generateEndpointId(method, path, handlerName),
452
+ method,
453
+ path,
454
+ kind: this.methodToOpKind(method),
455
+ handlerName,
456
+ source: this.createLocation(file, lineNumber, lineNumber + 5),
457
+ confidence: this.createConfidence(hasZodValidation ? "high" : "medium", hasZodValidation ? "explicit-schema" : "decorator-hints")
458
+ };
459
+ this.addEndpoint(ctx, endpoint);
460
+ }
461
+ }
462
+ }
463
+ // src/extractors/fastify/extractor.ts
464
+ var PATTERNS3 = {
465
+ route: /(?:fastify|app|server)\.(get|post|put|patch|delete|head|options)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
466
+ schemaOption: /schema\s*:\s*\{/g,
467
+ bodySchema: /body\s*:\s*(\w+)/g,
468
+ responseSchema: /response\s*:\s*\{/g
469
+ };
470
+
471
+ class FastifyExtractor extends BaseExtractor {
472
+ id = "fastify";
473
+ name = "Fastify Extractor";
474
+ frameworks = ["fastify"];
475
+ priority = 15;
476
+ async doExtract(ctx) {
477
+ const { project, options, fs } = ctx;
478
+ const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
479
+ const files = await fs.glob(pattern, { cwd: project.rootPath });
480
+ ctx.ir.stats.filesScanned = files.length;
481
+ for (const file of files) {
482
+ if (file.includes("node_modules") || file.includes(".test."))
483
+ continue;
484
+ const fullPath = `${project.rootPath}/${file}`;
485
+ const content = await fs.readFile(fullPath);
486
+ await this.extractRoutes(ctx, file, content);
487
+ }
488
+ }
489
+ async extractRoutes(ctx, file, content) {
490
+ const matches = [...content.matchAll(PATTERNS3.route)];
491
+ for (const match of matches) {
492
+ const method = match[1]?.toUpperCase() ?? "GET";
493
+ const path = match[2] ?? "/";
494
+ const index = match.index ?? 0;
495
+ const lineNumber = content.slice(0, index).split(`
496
+ `).length;
497
+ const afterMatch = content.slice(index, index + 1000);
498
+ const hasSchema = PATTERNS3.schemaOption.test(afterMatch);
499
+ const endpoint = {
500
+ id: this.generateEndpointId(method, path),
501
+ method,
502
+ path,
503
+ kind: this.methodToOpKind(method),
504
+ handlerName: "handler",
505
+ source: this.createLocation(file, lineNumber, lineNumber + 10),
506
+ confidence: this.createConfidence(hasSchema ? "high" : "medium", hasSchema ? "explicit-schema" : "decorator-hints"),
507
+ frameworkMeta: { hasSchema }
508
+ };
509
+ this.addEndpoint(ctx, endpoint);
510
+ }
511
+ }
512
+ }
513
+ // src/extractors/hono/extractor.ts
514
+ var PATTERNS4 = {
515
+ route: /(?:app|hono)\.(get|post|put|patch|delete|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
516
+ zodValidator: /zValidator\s*\(\s*['"`](\w+)['"`]\s*,\s*(\w+)/g
517
+ };
518
+
519
+ class HonoExtractor extends BaseExtractor {
520
+ id = "hono";
521
+ name = "Hono Extractor";
522
+ frameworks = ["hono"];
523
+ priority = 15;
524
+ async doExtract(ctx) {
525
+ const { project, options, fs } = ctx;
526
+ const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
527
+ const files = await fs.glob(pattern, { cwd: project.rootPath });
528
+ ctx.ir.stats.filesScanned = files.length;
529
+ for (const file of files) {
530
+ if (file.includes("node_modules") || file.includes(".test."))
531
+ continue;
532
+ const fullPath = `${project.rootPath}/${file}`;
533
+ const content = await fs.readFile(fullPath);
534
+ await this.extractRoutes(ctx, file, content);
535
+ }
536
+ }
537
+ async extractRoutes(ctx, file, content) {
538
+ const matches = [...content.matchAll(PATTERNS4.route)];
539
+ for (const match of matches) {
540
+ const method = match[1]?.toUpperCase() ?? "GET";
541
+ const path = match[2] ?? "/";
542
+ const index = match.index ?? 0;
543
+ const lineNumber = content.slice(0, index).split(`
544
+ `).length;
545
+ const afterMatch = content.slice(index, index + 500);
546
+ const hasZodValidator = PATTERNS4.zodValidator.test(afterMatch);
547
+ const endpoint = {
548
+ id: this.generateEndpointId(method, path),
549
+ method,
550
+ path,
551
+ kind: this.methodToOpKind(method),
552
+ handlerName: "handler",
553
+ source: this.createLocation(file, lineNumber, lineNumber + 5),
554
+ confidence: this.createConfidence(hasZodValidator ? "high" : "medium", hasZodValidator ? "explicit-schema" : "decorator-hints")
555
+ };
556
+ this.addEndpoint(ctx, endpoint);
557
+ }
558
+ }
559
+ }
560
+ // src/extractors/elysia/extractor.ts
561
+ var PATTERNS5 = {
562
+ route: /\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
563
+ tSchema: /body:\s*t\.\w+/g
564
+ };
565
+
566
+ class ElysiaExtractor extends BaseExtractor {
567
+ id = "elysia";
568
+ name = "Elysia Extractor";
569
+ frameworks = ["elysia"];
570
+ priority = 15;
571
+ async doExtract(ctx) {
572
+ const { project, options, fs } = ctx;
573
+ const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
574
+ const files = await fs.glob(pattern, { cwd: project.rootPath });
575
+ ctx.ir.stats.filesScanned = files.length;
576
+ for (const file of files) {
577
+ if (file.includes("node_modules") || file.includes(".test."))
578
+ continue;
579
+ const fullPath = `${project.rootPath}/${file}`;
580
+ const content = await fs.readFile(fullPath);
581
+ if (!content.includes("elysia"))
582
+ continue;
583
+ await this.extractRoutes(ctx, file, content);
584
+ }
585
+ }
586
+ async extractRoutes(ctx, file, content) {
587
+ const matches = [...content.matchAll(PATTERNS5.route)];
588
+ for (const match of matches) {
589
+ const method = match[1]?.toUpperCase() ?? "GET";
590
+ const path = match[2] ?? "/";
591
+ const index = match.index ?? 0;
592
+ const lineNumber = content.slice(0, index).split(`
593
+ `).length;
594
+ const afterMatch = content.slice(index, index + 500);
595
+ const hasTSchema = PATTERNS5.tSchema.test(afterMatch);
596
+ const endpoint = {
597
+ id: this.generateEndpointId(method, path),
598
+ method,
599
+ path,
600
+ kind: this.methodToOpKind(method),
601
+ handlerName: "handler",
602
+ source: this.createLocation(file, lineNumber, lineNumber + 5),
603
+ confidence: this.createConfidence(hasTSchema ? "high" : "medium", hasTSchema ? "explicit-schema" : "decorator-hints")
604
+ };
605
+ this.addEndpoint(ctx, endpoint);
606
+ }
607
+ }
608
+ }
609
+ // src/extractors/trpc/extractor.ts
610
+ var PATTERNS6 = {
611
+ procedure: /\.(query|mutation)\s*\(\s*(?:\{[^}]*\}|[^)]+)\)/gi,
612
+ procedureName: /(\w+)\s*:\s*(?:publicProcedure|protectedProcedure|procedure)/g,
613
+ zodInput: /\.input\s*\(\s*(\w+)/g,
614
+ zodOutput: /\.output\s*\(\s*(\w+)/g
615
+ };
616
+
617
+ class TrpcExtractor extends BaseExtractor {
618
+ id = "trpc";
619
+ name = "tRPC Extractor";
620
+ frameworks = ["trpc"];
621
+ priority = 15;
622
+ async doExtract(ctx) {
623
+ const { project, options, fs } = ctx;
624
+ const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
625
+ const files = await fs.glob(pattern, { cwd: project.rootPath });
626
+ ctx.ir.stats.filesScanned = files.length;
627
+ for (const file of files) {
628
+ if (file.includes("node_modules") || file.includes(".test."))
629
+ continue;
630
+ const fullPath = `${project.rootPath}/${file}`;
631
+ const content = await fs.readFile(fullPath);
632
+ if (!content.includes("trpc") && !content.includes("Procedure"))
633
+ continue;
634
+ await this.extractProcedures(ctx, file, content);
635
+ }
636
+ }
637
+ async extractProcedures(ctx, file, content) {
638
+ const nameMatches = [...content.matchAll(PATTERNS6.procedureName)];
639
+ for (const match of nameMatches) {
640
+ const procedureName = match[1] ?? "unknownProcedure";
641
+ const index = match.index ?? 0;
642
+ const lineNumber = content.slice(0, index).split(`
643
+ `).length;
644
+ const afterMatch = content.slice(index, index + 500);
645
+ const isQuery = afterMatch.includes(".query(");
646
+ const isMutation = afterMatch.includes(".mutation(");
647
+ if (!isQuery && !isMutation)
648
+ continue;
649
+ const hasZodInput = PATTERNS6.zodInput.test(afterMatch);
650
+ const hasZodOutput = PATTERNS6.zodOutput.test(afterMatch);
651
+ const hasSchema = hasZodInput || hasZodOutput;
652
+ const method = isMutation ? "POST" : "GET";
653
+ const endpoint = {
654
+ id: `trpc.${procedureName}`,
655
+ method,
656
+ path: `/trpc/${procedureName}`,
657
+ kind: isMutation ? "command" : "query",
658
+ handlerName: procedureName,
659
+ source: this.createLocation(file, lineNumber, lineNumber + 10),
660
+ confidence: this.createConfidence(hasSchema ? "high" : "medium", hasSchema ? "explicit-schema" : "inferred-types"),
661
+ frameworkMeta: {
662
+ procedureType: isMutation ? "mutation" : "query",
663
+ hasInput: hasZodInput,
664
+ hasOutput: hasZodOutput
665
+ }
666
+ };
667
+ this.addEndpoint(ctx, endpoint);
668
+ }
669
+ }
670
+ }
671
+ // src/extractors/next-api/extractor.ts
672
+ var PATTERNS7 = {
673
+ appRouterExport: /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/gi,
674
+ pagesHandler: /export\s+default\s+(?:async\s+)?function/g,
675
+ zodSchema: /z\.\w+\(/g
676
+ };
677
+
678
+ class NextApiExtractor extends BaseExtractor {
679
+ id = "next-api";
680
+ name = "Next.js API Extractor";
681
+ frameworks = ["next-api"];
682
+ priority = 15;
683
+ async doExtract(ctx) {
684
+ const { project, fs } = ctx;
685
+ const appRoutes = await fs.glob("**/app/api/**/route.ts", {
686
+ cwd: project.rootPath
687
+ });
688
+ const pagesRoutes = await fs.glob("**/pages/api/**/*.ts", {
689
+ cwd: project.rootPath
690
+ });
691
+ const allRoutes = [...appRoutes, ...pagesRoutes];
692
+ ctx.ir.stats.filesScanned = allRoutes.length;
693
+ for (const file of allRoutes) {
694
+ const fullPath = `${project.rootPath}/${file}`;
695
+ const content = await fs.readFile(fullPath);
696
+ if (file.includes("/app/api/")) {
697
+ await this.extractAppRoutes(ctx, file, content);
698
+ } else {
699
+ await this.extractPagesRoutes(ctx, file, content);
700
+ }
701
+ }
702
+ }
703
+ async extractAppRoutes(ctx, file, content) {
704
+ const pathMatch = file.match(/app\/api\/(.+)\/route\.ts$/);
705
+ const routePath = pathMatch ? `/api/${pathMatch[1]}` : "/api";
706
+ const matches = [...content.matchAll(PATTERNS7.appRouterExport)];
707
+ for (const match of matches) {
708
+ const method = match[1]?.toUpperCase() ?? "GET";
709
+ const index = match.index ?? 0;
710
+ const lineNumber = content.slice(0, index).split(`
711
+ `).length;
712
+ const hasZod = PATTERNS7.zodSchema.test(content);
713
+ const endpoint = {
714
+ id: this.generateEndpointId(method, routePath),
715
+ method,
716
+ path: routePath.replace(/\[(\w+)\]/g, ":$1"),
717
+ kind: this.methodToOpKind(method),
718
+ handlerName: method,
719
+ source: this.createLocation(file, lineNumber, lineNumber + 10),
720
+ confidence: this.createConfidence(hasZod ? "high" : "medium", hasZod ? "explicit-schema" : "inferred-types"),
721
+ frameworkMeta: { routeType: "app-router" }
722
+ };
723
+ this.addEndpoint(ctx, endpoint);
724
+ }
725
+ }
726
+ async extractPagesRoutes(ctx, file, content) {
727
+ const pathMatch = file.match(/pages\/api\/(.+)\.ts$/);
728
+ const routePath = pathMatch ? `/api/${pathMatch[1]}` : "/api";
729
+ if (!PATTERNS7.pagesHandler.test(content))
730
+ return;
731
+ const lineNumber = 1;
732
+ const _hasZod = PATTERNS7.zodSchema.test(content);
733
+ const methods = ["GET", "POST"];
734
+ for (const method of methods) {
735
+ const endpoint = {
736
+ id: this.generateEndpointId(method, routePath),
737
+ method,
738
+ path: routePath.replace(/\[(\w+)\]/g, ":$1"),
739
+ kind: this.methodToOpKind(method),
740
+ handlerName: "handler",
741
+ source: this.createLocation(file, lineNumber, lineNumber + 20),
742
+ confidence: this.createConfidence("low", "naming-convention"),
743
+ frameworkMeta: { routeType: "pages-router" }
744
+ };
745
+ this.addEndpoint(ctx, endpoint);
746
+ }
747
+ }
748
+ }
749
+ // src/extractors/zod/extractor.ts
750
+ var PATTERNS8 = {
751
+ zodSchema: /(?:export\s+)?const\s+(\w+)\s*=\s*z\.(?:object|string|number|boolean|array|enum|union|intersection|literal|tuple|record)/g,
752
+ zodInfer: /type\s+(\w+)\s*=\s*z\.infer<typeof\s+(\w+)>/g
753
+ };
754
+
755
+ class ZodSchemaExtractor extends BaseExtractor {
756
+ id = "zod";
757
+ name = "Zod Schema Extractor";
758
+ frameworks = ["zod"];
759
+ priority = 5;
760
+ async detect() {
761
+ return true;
762
+ }
763
+ async doExtract(ctx) {
764
+ const { project, options, fs } = ctx;
765
+ const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
766
+ const files = await fs.glob(pattern, { cwd: project.rootPath });
767
+ ctx.ir.stats.filesScanned = files.length;
768
+ for (const file of files) {
769
+ if (file.includes("node_modules") || file.includes(".test."))
770
+ continue;
771
+ const fullPath = `${project.rootPath}/${file}`;
772
+ const content = await fs.readFile(fullPath);
773
+ if (!content.includes("z."))
774
+ continue;
775
+ await this.extractSchemas(ctx, file, content);
776
+ }
777
+ }
778
+ async extractSchemas(ctx, file, content) {
779
+ const matches = [...content.matchAll(PATTERNS8.zodSchema)];
780
+ for (const match of matches) {
781
+ const name = match[1] ?? "unknownSchema";
782
+ const index = match.index ?? 0;
783
+ const lineNumber = content.slice(0, index).split(`
784
+ `).length;
785
+ let depth = 0;
786
+ let endIndex = index;
787
+ for (let i = index;i < content.length; i++) {
788
+ const char = content[i];
789
+ if (char === "(" || char === "{" || char === "[")
790
+ depth++;
791
+ if (char === ")" || char === "}" || char === "]")
792
+ depth--;
793
+ if (depth === 0 && (char === ";" || char === `
794
+ `)) {
795
+ endIndex = i;
796
+ break;
797
+ }
798
+ }
799
+ const rawDefinition = content.slice(index, endIndex + 1);
800
+ const endLineNumber = lineNumber + rawDefinition.split(`
801
+ `).length - 1;
802
+ const schema = {
803
+ id: this.generateSchemaId(name, file),
804
+ name,
805
+ schemaType: "zod",
806
+ rawDefinition,
807
+ source: this.createLocation(file, lineNumber, endLineNumber),
808
+ confidence: this.createConfidence("high", "explicit-schema")
809
+ };
810
+ this.addSchema(ctx, schema);
811
+ }
812
+ }
813
+ }
814
+ // src/extractors/index.ts
29
815
  function registerAllExtractors() {
30
- extractorRegistry.register(new NestJsExtractor());
31
- extractorRegistry.register(new ExpressExtractor());
32
- extractorRegistry.register(new FastifyExtractor());
33
- extractorRegistry.register(new HonoExtractor());
34
- extractorRegistry.register(new ElysiaExtractor());
35
- extractorRegistry.register(new TrpcExtractor());
36
- extractorRegistry.register(new NextApiExtractor());
37
- extractorRegistry.register(new ZodSchemaExtractor());
38
- }
39
-
40
- //#endregion
41
- export { BaseExtractor, ElysiaExtractor, ExpressExtractor, FastifyExtractor, HonoExtractor, NestJsExtractor, NextApiExtractor, TrpcExtractor, ZodSchemaExtractor, extractors_exports, registerAllExtractors };
42
- //# sourceMappingURL=index.js.map
816
+ extractorRegistry.register(new NestJsExtractor);
817
+ extractorRegistry.register(new ExpressExtractor);
818
+ extractorRegistry.register(new FastifyExtractor);
819
+ extractorRegistry.register(new HonoExtractor);
820
+ extractorRegistry.register(new ElysiaExtractor);
821
+ extractorRegistry.register(new TrpcExtractor);
822
+ extractorRegistry.register(new NextApiExtractor);
823
+ extractorRegistry.register(new ZodSchemaExtractor);
824
+ }
825
+ export {
826
+ registerAllExtractors,
827
+ ZodSchemaExtractor,
828
+ TrpcExtractor,
829
+ NextApiExtractor,
830
+ NestJsExtractor,
831
+ HonoExtractor,
832
+ FastifyExtractor,
833
+ ExpressExtractor,
834
+ ElysiaExtractor,
835
+ BaseExtractor
836
+ };