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