@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,1215 @@
|
|
|
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/codegen/index.ts
|
|
13
|
+
var exports_codegen = {};
|
|
14
|
+
__export(exports_codegen, {
|
|
15
|
+
generateSchemas: () => generateSchemas,
|
|
16
|
+
generateSchema: () => generateSchema,
|
|
17
|
+
generateRegistry: () => generateRegistry,
|
|
18
|
+
generateOperations: () => generateOperations,
|
|
19
|
+
generateOperation: () => generateOperation
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// src/codegen/operation-gen.ts
|
|
23
|
+
function generateOperation(endpoint, options) {
|
|
24
|
+
const specName = toSpecName(endpoint);
|
|
25
|
+
const fileName = `${toFileName(endpoint)}.ts`;
|
|
26
|
+
const code = generateOperationCode(endpoint, specName, options);
|
|
27
|
+
return {
|
|
28
|
+
path: fileName,
|
|
29
|
+
content: code,
|
|
30
|
+
type: "operation"
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function generateOperations(ir, options) {
|
|
34
|
+
return ir.endpoints.map((endpoint) => generateOperation(endpoint, options));
|
|
35
|
+
}
|
|
36
|
+
function toSpecName(endpoint) {
|
|
37
|
+
const parts = endpoint.path.replace(/^\//, "").split("/").filter((p) => p && !p.startsWith(":") && !p.startsWith("{"));
|
|
38
|
+
const methodPart = endpoint.method.toLowerCase();
|
|
39
|
+
const pathPart = parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
40
|
+
return `${methodPart}${pathPart}Spec`;
|
|
41
|
+
}
|
|
42
|
+
function toFileName(endpoint) {
|
|
43
|
+
const parts = endpoint.path.replace(/^\//, "").split("/").filter((p) => p && !p.startsWith(":") && !p.startsWith("{"));
|
|
44
|
+
const methodPart = endpoint.method.toLowerCase();
|
|
45
|
+
const pathPart = parts.join("-");
|
|
46
|
+
return `${methodPart}-${pathPart}`.replace(/--+/g, "-");
|
|
47
|
+
}
|
|
48
|
+
function generateOperationCode(endpoint, specName, options) {
|
|
49
|
+
const isCommand = endpoint.kind === "command";
|
|
50
|
+
const defineFunc = isCommand ? "defineCommand" : "defineQuery";
|
|
51
|
+
const auth = options.defaultAuth ?? "user";
|
|
52
|
+
const owners = options.defaultOwners ?? ["team"];
|
|
53
|
+
const lines = [
|
|
54
|
+
`/**`,
|
|
55
|
+
` * ${endpoint.method} ${endpoint.path}`,
|
|
56
|
+
` *`,
|
|
57
|
+
` * Generated from: ${endpoint.source.file}:${endpoint.source.startLine}`,
|
|
58
|
+
` * Confidence: ${endpoint.confidence.level}`,
|
|
59
|
+
` */`,
|
|
60
|
+
``,
|
|
61
|
+
`import { ${defineFunc} } from '@contractspec/lib.contracts';`,
|
|
62
|
+
`import { fromZod } from '@contractspec/lib.schema';`,
|
|
63
|
+
`import { z } from 'zod';`,
|
|
64
|
+
``,
|
|
65
|
+
`// TODO: Define input schema based on extracted information`,
|
|
66
|
+
`const inputSchema = fromZod(z.object({`,
|
|
67
|
+
` // Add fields here`,
|
|
68
|
+
`}));`,
|
|
69
|
+
``,
|
|
70
|
+
`// TODO: Define output schema`,
|
|
71
|
+
`const outputSchema = fromZod(z.object({`,
|
|
72
|
+
` // Add fields here`,
|
|
73
|
+
`}));`,
|
|
74
|
+
``,
|
|
75
|
+
`export const ${specName} = ${defineFunc}({`,
|
|
76
|
+
` meta: {`,
|
|
77
|
+
` name: '${endpoint.handlerName ?? endpoint.id}',`,
|
|
78
|
+
` version: 1,`,
|
|
79
|
+
` stability: 'experimental',`,
|
|
80
|
+
` owners: ${JSON.stringify(owners)},`,
|
|
81
|
+
` goal: 'TODO: Describe the business goal',`,
|
|
82
|
+
` context: 'Generated from ${endpoint.source.file}',`,
|
|
83
|
+
` },`,
|
|
84
|
+
` io: {`,
|
|
85
|
+
` input: inputSchema,`,
|
|
86
|
+
` output: outputSchema,`,
|
|
87
|
+
` },`,
|
|
88
|
+
` policy: {`,
|
|
89
|
+
` auth: '${auth}',`,
|
|
90
|
+
` },`,
|
|
91
|
+
` transport: {`,
|
|
92
|
+
` rest: {`,
|
|
93
|
+
` method: '${endpoint.method}',`,
|
|
94
|
+
` path: '${endpoint.path}',`,
|
|
95
|
+
` },`,
|
|
96
|
+
` },`,
|
|
97
|
+
`});`,
|
|
98
|
+
``
|
|
99
|
+
];
|
|
100
|
+
return lines.join(`
|
|
101
|
+
`);
|
|
102
|
+
}
|
|
103
|
+
// src/codegen/schema-gen.ts
|
|
104
|
+
function generateSchema(schema, _options) {
|
|
105
|
+
const fileName = `${toFileName2(schema.name)}.ts`;
|
|
106
|
+
const code = generateSchemaCode(schema);
|
|
107
|
+
return {
|
|
108
|
+
path: `schemas/${fileName}`,
|
|
109
|
+
content: code,
|
|
110
|
+
type: "schema"
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function generateSchemas(ir, options) {
|
|
114
|
+
return ir.schemas.map((schema) => generateSchema(schema, options));
|
|
115
|
+
}
|
|
116
|
+
function toFileName2(name) {
|
|
117
|
+
return name.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
118
|
+
}
|
|
119
|
+
function generateSchemaCode(schema) {
|
|
120
|
+
const lines = [
|
|
121
|
+
`/**`,
|
|
122
|
+
` * ${schema.name}`,
|
|
123
|
+
` *`,
|
|
124
|
+
` * Generated from: ${schema.source.file}:${schema.source.startLine}`,
|
|
125
|
+
` * Schema type: ${schema.schemaType}`,
|
|
126
|
+
` * Confidence: ${schema.confidence.level}`,
|
|
127
|
+
` */`,
|
|
128
|
+
``,
|
|
129
|
+
`import { fromZod } from '@contractspec/lib.schema';`,
|
|
130
|
+
`import { z } from 'zod';`,
|
|
131
|
+
``
|
|
132
|
+
];
|
|
133
|
+
if (schema.rawDefinition && schema.schemaType === "zod") {
|
|
134
|
+
lines.push(`// Original definition from source:`);
|
|
135
|
+
lines.push(`// ${schema.rawDefinition.split(`
|
|
136
|
+
`)[0]}`);
|
|
137
|
+
lines.push(``);
|
|
138
|
+
}
|
|
139
|
+
lines.push(`export const ${schema.name}Schema = fromZod(z.object({`);
|
|
140
|
+
if (schema.fields && schema.fields.length > 0) {
|
|
141
|
+
for (const field of schema.fields) {
|
|
142
|
+
const zodType = mapToZodType(field.type, field.optional);
|
|
143
|
+
lines.push(` ${field.name}: ${zodType},`);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
lines.push(` // TODO: Define schema fields`);
|
|
147
|
+
}
|
|
148
|
+
lines.push(`}));`);
|
|
149
|
+
lines.push(``);
|
|
150
|
+
lines.push(`export type ${schema.name} = z.infer<typeof ${schema.name}Schema.zodSchema>;`);
|
|
151
|
+
lines.push(``);
|
|
152
|
+
return lines.join(`
|
|
153
|
+
`);
|
|
154
|
+
}
|
|
155
|
+
function mapToZodType(tsType, optional) {
|
|
156
|
+
let zodType;
|
|
157
|
+
switch (tsType.toLowerCase()) {
|
|
158
|
+
case "string":
|
|
159
|
+
zodType = "z.string()";
|
|
160
|
+
break;
|
|
161
|
+
case "number":
|
|
162
|
+
zodType = "z.number()";
|
|
163
|
+
break;
|
|
164
|
+
case "boolean":
|
|
165
|
+
zodType = "z.boolean()";
|
|
166
|
+
break;
|
|
167
|
+
case "date":
|
|
168
|
+
zodType = "z.date()";
|
|
169
|
+
break;
|
|
170
|
+
case "string[]":
|
|
171
|
+
case "array<string>":
|
|
172
|
+
zodType = "z.array(z.string())";
|
|
173
|
+
break;
|
|
174
|
+
case "number[]":
|
|
175
|
+
case "array<number>":
|
|
176
|
+
zodType = "z.array(z.number())";
|
|
177
|
+
break;
|
|
178
|
+
default:
|
|
179
|
+
zodType = "z.unknown()";
|
|
180
|
+
}
|
|
181
|
+
return optional ? `${zodType}.optional()` : zodType;
|
|
182
|
+
}
|
|
183
|
+
// src/codegen/registry-gen.ts
|
|
184
|
+
function generateRegistry(operationFiles) {
|
|
185
|
+
const operationImports = operationFiles.filter((f) => f.type === "operation").map((f) => {
|
|
186
|
+
const name = f.path.replace(".ts", "").replace(/-/g, "_");
|
|
187
|
+
const specName = toPascalCase(name) + "Spec";
|
|
188
|
+
return { path: f.path, name, specName };
|
|
189
|
+
});
|
|
190
|
+
const lines = [
|
|
191
|
+
`/**`,
|
|
192
|
+
` * Generated operation registry.`,
|
|
193
|
+
` */`,
|
|
194
|
+
``,
|
|
195
|
+
`import { OperationSpecRegistry } from '@contractspec/lib.contracts';`,
|
|
196
|
+
``
|
|
197
|
+
];
|
|
198
|
+
for (const op of operationImports) {
|
|
199
|
+
const importPath = `./${op.path.replace(".ts", "")}`;
|
|
200
|
+
lines.push(`import { ${op.specName} } from '${importPath}';`);
|
|
201
|
+
}
|
|
202
|
+
lines.push(``);
|
|
203
|
+
lines.push(`export const operationRegistry = new OperationSpecRegistry();`);
|
|
204
|
+
lines.push(``);
|
|
205
|
+
for (const op of operationImports) {
|
|
206
|
+
lines.push(`operationRegistry.register(${op.specName});`);
|
|
207
|
+
}
|
|
208
|
+
lines.push(``);
|
|
209
|
+
return {
|
|
210
|
+
path: "registry.ts",
|
|
211
|
+
content: lines.join(`
|
|
212
|
+
`),
|
|
213
|
+
type: "registry"
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function toPascalCase(str) {
|
|
217
|
+
return str.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
218
|
+
}
|
|
219
|
+
// src/extractors/index.ts
|
|
220
|
+
var exports_extractors = {};
|
|
221
|
+
__export(exports_extractors, {
|
|
222
|
+
registerAllExtractors: () => registerAllExtractors,
|
|
223
|
+
ZodSchemaExtractor: () => ZodSchemaExtractor,
|
|
224
|
+
TrpcExtractor: () => TrpcExtractor,
|
|
225
|
+
NextApiExtractor: () => NextApiExtractor,
|
|
226
|
+
NestJsExtractor: () => NestJsExtractor,
|
|
227
|
+
HonoExtractor: () => HonoExtractor,
|
|
228
|
+
FastifyExtractor: () => FastifyExtractor,
|
|
229
|
+
ExpressExtractor: () => ExpressExtractor,
|
|
230
|
+
ElysiaExtractor: () => ElysiaExtractor,
|
|
231
|
+
BaseExtractor: () => BaseExtractor
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// src/registry.ts
|
|
235
|
+
class ExtractorRegistry {
|
|
236
|
+
extractors = new Map;
|
|
237
|
+
register(extractor) {
|
|
238
|
+
this.extractors.set(extractor.id, extractor);
|
|
239
|
+
}
|
|
240
|
+
unregister(id) {
|
|
241
|
+
return this.extractors.delete(id);
|
|
242
|
+
}
|
|
243
|
+
get(id) {
|
|
244
|
+
return this.extractors.get(id);
|
|
245
|
+
}
|
|
246
|
+
getAll() {
|
|
247
|
+
return Array.from(this.extractors.values());
|
|
248
|
+
}
|
|
249
|
+
async findMatching(project) {
|
|
250
|
+
const matches = [];
|
|
251
|
+
for (const extractor of this.extractors.values()) {
|
|
252
|
+
try {
|
|
253
|
+
if (await extractor.detect(project)) {
|
|
254
|
+
matches.push(extractor);
|
|
255
|
+
}
|
|
256
|
+
} catch {}
|
|
257
|
+
}
|
|
258
|
+
return matches.sort((a, b) => b.priority - a.priority);
|
|
259
|
+
}
|
|
260
|
+
findByFramework(framework) {
|
|
261
|
+
const matches = [];
|
|
262
|
+
for (const extractor of this.extractors.values()) {
|
|
263
|
+
if (extractor.frameworks.includes(framework)) {
|
|
264
|
+
matches.push(extractor);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return matches.sort((a, b) => b.priority - a.priority);
|
|
268
|
+
}
|
|
269
|
+
findForFramework(framework) {
|
|
270
|
+
return this.findByFramework(framework);
|
|
271
|
+
}
|
|
272
|
+
hasExtractorFor(framework) {
|
|
273
|
+
for (const extractor of this.extractors.values()) {
|
|
274
|
+
if (extractor.frameworks.includes(framework) || extractor.id === framework) {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
getSupportedFrameworks() {
|
|
281
|
+
const frameworks = new Set;
|
|
282
|
+
for (const extractor of this.extractors.values()) {
|
|
283
|
+
frameworks.add(extractor.id);
|
|
284
|
+
for (const fw of extractor.frameworks) {
|
|
285
|
+
frameworks.add(fw);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return Array.from(frameworks);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
var extractorRegistry = new ExtractorRegistry;
|
|
292
|
+
function registerBuiltInExtractors() {}
|
|
293
|
+
|
|
294
|
+
// src/extract.ts
|
|
295
|
+
async function extractFromProject(project, options = {}) {
|
|
296
|
+
let extractors;
|
|
297
|
+
if (options.framework) {
|
|
298
|
+
extractors = extractorRegistry.findByFramework(options.framework);
|
|
299
|
+
if (extractors.length === 0) {
|
|
300
|
+
return {
|
|
301
|
+
success: false,
|
|
302
|
+
errors: [
|
|
303
|
+
{
|
|
304
|
+
code: "EXTRACTOR_NOT_FOUND",
|
|
305
|
+
message: `No extractor found for framework: ${options.framework}`,
|
|
306
|
+
recoverable: false
|
|
307
|
+
}
|
|
308
|
+
]
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
extractors = await extractorRegistry.findMatching(project);
|
|
313
|
+
if (extractors.length === 0) {
|
|
314
|
+
return {
|
|
315
|
+
success: false,
|
|
316
|
+
errors: [
|
|
317
|
+
{
|
|
318
|
+
code: "NO_FRAMEWORK_DETECTED",
|
|
319
|
+
message: "No supported framework detected in project",
|
|
320
|
+
recoverable: false
|
|
321
|
+
}
|
|
322
|
+
]
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const extractor = extractors[0];
|
|
327
|
+
if (!extractor) {
|
|
328
|
+
return {
|
|
329
|
+
success: false,
|
|
330
|
+
errors: [
|
|
331
|
+
{
|
|
332
|
+
code: "NO_EXTRACTOR",
|
|
333
|
+
message: "No extractor available",
|
|
334
|
+
recoverable: false
|
|
335
|
+
}
|
|
336
|
+
]
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const result = await extractor.extract(project, options);
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
function mergeIRs(irs) {
|
|
343
|
+
if (irs.length === 0) {
|
|
344
|
+
throw new Error("Cannot merge empty IR array");
|
|
345
|
+
}
|
|
346
|
+
if (irs.length === 1) {
|
|
347
|
+
if (!irs[0])
|
|
348
|
+
throw new Error("First IR is undefined");
|
|
349
|
+
return irs[0];
|
|
350
|
+
}
|
|
351
|
+
const first = irs[0];
|
|
352
|
+
if (!first)
|
|
353
|
+
throw new Error("First IR is undefined");
|
|
354
|
+
const merged = {
|
|
355
|
+
version: "1.0",
|
|
356
|
+
extractedAt: new Date().toISOString(),
|
|
357
|
+
project: first.project,
|
|
358
|
+
endpoints: [],
|
|
359
|
+
schemas: [],
|
|
360
|
+
errors: [],
|
|
361
|
+
events: [],
|
|
362
|
+
ambiguities: [],
|
|
363
|
+
stats: {
|
|
364
|
+
filesScanned: 0,
|
|
365
|
+
endpointsFound: 0,
|
|
366
|
+
schemasFound: 0,
|
|
367
|
+
errorsFound: 0,
|
|
368
|
+
eventsFound: 0,
|
|
369
|
+
ambiguitiesFound: 0,
|
|
370
|
+
highConfidence: 0,
|
|
371
|
+
mediumConfidence: 0,
|
|
372
|
+
lowConfidence: 0
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
for (const ir of irs) {
|
|
376
|
+
merged.endpoints.push(...ir.endpoints);
|
|
377
|
+
merged.schemas.push(...ir.schemas);
|
|
378
|
+
merged.errors.push(...ir.errors);
|
|
379
|
+
merged.events.push(...ir.events);
|
|
380
|
+
merged.ambiguities.push(...ir.ambiguities);
|
|
381
|
+
merged.stats.filesScanned += ir.stats.filesScanned;
|
|
382
|
+
merged.stats.endpointsFound += ir.stats.endpointsFound;
|
|
383
|
+
merged.stats.schemasFound += ir.stats.schemasFound;
|
|
384
|
+
merged.stats.errorsFound += ir.stats.errorsFound;
|
|
385
|
+
merged.stats.eventsFound += ir.stats.eventsFound;
|
|
386
|
+
merged.stats.ambiguitiesFound += ir.stats.ambiguitiesFound;
|
|
387
|
+
merged.stats.highConfidence += ir.stats.highConfidence;
|
|
388
|
+
merged.stats.mediumConfidence += ir.stats.mediumConfidence;
|
|
389
|
+
merged.stats.lowConfidence += ir.stats.lowConfidence;
|
|
390
|
+
}
|
|
391
|
+
return merged;
|
|
392
|
+
}
|
|
393
|
+
function createEmptyIR(project) {
|
|
394
|
+
return {
|
|
395
|
+
version: "1.0",
|
|
396
|
+
extractedAt: new Date().toISOString(),
|
|
397
|
+
project,
|
|
398
|
+
endpoints: [],
|
|
399
|
+
schemas: [],
|
|
400
|
+
errors: [],
|
|
401
|
+
events: [],
|
|
402
|
+
ambiguities: [],
|
|
403
|
+
stats: {
|
|
404
|
+
filesScanned: 0,
|
|
405
|
+
endpointsFound: 0,
|
|
406
|
+
schemasFound: 0,
|
|
407
|
+
errorsFound: 0,
|
|
408
|
+
eventsFound: 0,
|
|
409
|
+
ambiguitiesFound: 0,
|
|
410
|
+
highConfidence: 0,
|
|
411
|
+
mediumConfidence: 0,
|
|
412
|
+
lowConfidence: 0
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/extractors/base.ts
|
|
418
|
+
class BaseExtractor {
|
|
419
|
+
priority = 10;
|
|
420
|
+
fs;
|
|
421
|
+
setFs(fs) {
|
|
422
|
+
this.fs = fs;
|
|
423
|
+
}
|
|
424
|
+
async detect(project) {
|
|
425
|
+
return project.frameworks.some((fw) => this.frameworks.includes(fw.id));
|
|
426
|
+
}
|
|
427
|
+
async extract(project, options) {
|
|
428
|
+
if (!this.fs) {
|
|
429
|
+
return {
|
|
430
|
+
success: false,
|
|
431
|
+
errors: [
|
|
432
|
+
{
|
|
433
|
+
code: "NO_FS_ADAPTER",
|
|
434
|
+
message: "File system adapter not configured",
|
|
435
|
+
recoverable: false
|
|
436
|
+
}
|
|
437
|
+
]
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const ir = createEmptyIR(project);
|
|
441
|
+
const ctx = {
|
|
442
|
+
project,
|
|
443
|
+
options,
|
|
444
|
+
fs: this.fs,
|
|
445
|
+
ir
|
|
446
|
+
};
|
|
447
|
+
try {
|
|
448
|
+
await this.doExtract(ctx);
|
|
449
|
+
this.calculateStats(ir);
|
|
450
|
+
return { success: true, ir };
|
|
451
|
+
} catch (error) {
|
|
452
|
+
return {
|
|
453
|
+
success: false,
|
|
454
|
+
errors: [
|
|
455
|
+
{
|
|
456
|
+
code: "EXTRACTION_ERROR",
|
|
457
|
+
message: error instanceof Error ? error.message : String(error),
|
|
458
|
+
recoverable: false
|
|
459
|
+
}
|
|
460
|
+
]
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
calculateStats(ir) {
|
|
465
|
+
ir.stats.endpointsFound = ir.endpoints.length;
|
|
466
|
+
ir.stats.schemasFound = ir.schemas.length;
|
|
467
|
+
ir.stats.errorsFound = ir.errors.length;
|
|
468
|
+
ir.stats.eventsFound = ir.events.length;
|
|
469
|
+
ir.stats.ambiguitiesFound = ir.ambiguities.length;
|
|
470
|
+
const allItems = [
|
|
471
|
+
...ir.endpoints,
|
|
472
|
+
...ir.schemas,
|
|
473
|
+
...ir.errors,
|
|
474
|
+
...ir.events
|
|
475
|
+
];
|
|
476
|
+
for (const item of allItems) {
|
|
477
|
+
switch (item.confidence.level) {
|
|
478
|
+
case "high":
|
|
479
|
+
ir.stats.highConfidence++;
|
|
480
|
+
break;
|
|
481
|
+
case "medium":
|
|
482
|
+
ir.stats.mediumConfidence++;
|
|
483
|
+
break;
|
|
484
|
+
case "low":
|
|
485
|
+
case "ambiguous":
|
|
486
|
+
ir.stats.lowConfidence++;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
generateEndpointId(method, path, handlerName) {
|
|
492
|
+
const pathPart = path.replace(/^\//, "").replace(/\//g, ".").replace(/:/g, "").replace(/\{/g, "").replace(/\}/g, "");
|
|
493
|
+
const base = `${method.toLowerCase()}.${pathPart}`;
|
|
494
|
+
return handlerName ? `${base}.${handlerName}` : base;
|
|
495
|
+
}
|
|
496
|
+
generateSchemaId(name, file) {
|
|
497
|
+
const filePart = file.replace(/\.ts$/, "").replace(/\//g, ".").replace(/^\.+/, "");
|
|
498
|
+
return `${filePart}.${name}`;
|
|
499
|
+
}
|
|
500
|
+
methodToOpKind(method) {
|
|
501
|
+
switch (method) {
|
|
502
|
+
case "GET":
|
|
503
|
+
case "HEAD":
|
|
504
|
+
case "OPTIONS":
|
|
505
|
+
return "query";
|
|
506
|
+
default:
|
|
507
|
+
return "command";
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
createLocation(file, startLine, endLine) {
|
|
511
|
+
return { file, startLine, endLine };
|
|
512
|
+
}
|
|
513
|
+
createConfidence(level, ...reasons) {
|
|
514
|
+
return { level, reasons };
|
|
515
|
+
}
|
|
516
|
+
addEndpoint(ctx, endpoint) {
|
|
517
|
+
ctx.ir.endpoints.push(endpoint);
|
|
518
|
+
}
|
|
519
|
+
addSchema(ctx, schema) {
|
|
520
|
+
ctx.ir.schemas.push(schema);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// src/extractors/nestjs/extractor.ts
|
|
524
|
+
var PATTERNS = {
|
|
525
|
+
controller: /@Controller\s*\(\s*['"`]([^'"`]*)['"`]\s*\)/g,
|
|
526
|
+
route: /@(Get|Post|Put|Patch|Delete|Head|Options)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
|
|
527
|
+
body: /@Body\s*\(\s*\)/g,
|
|
528
|
+
param: /@Param\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
|
|
529
|
+
query: /@Query\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g,
|
|
530
|
+
dto: /class\s+(\w+(?:Dto|DTO|Request|Response|Input|Output))\s*\{/g,
|
|
531
|
+
classValidator: /@(IsString|IsNumber|IsBoolean|IsArray|IsOptional|IsNotEmpty|Min|Max|Length|Matches)/g
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
class NestJsExtractor extends BaseExtractor {
|
|
535
|
+
id = "nestjs";
|
|
536
|
+
name = "NestJS Extractor";
|
|
537
|
+
frameworks = ["nestjs"];
|
|
538
|
+
priority = 20;
|
|
539
|
+
async doExtract(ctx) {
|
|
540
|
+
const { project, options, fs } = ctx;
|
|
541
|
+
const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
|
|
542
|
+
const files = await fs.glob(pattern, { cwd: project.rootPath });
|
|
543
|
+
ctx.ir.stats.filesScanned = files.length;
|
|
544
|
+
for (const file of files) {
|
|
545
|
+
if (file.includes("node_modules") || file.includes(".spec.") || file.includes(".test.")) {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
const fullPath = `${project.rootPath}/${file}`;
|
|
549
|
+
const content = await fs.readFile(fullPath);
|
|
550
|
+
await this.extractControllers(ctx, file, content);
|
|
551
|
+
await this.extractDtos(ctx, file, content);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async extractControllers(ctx, file, content) {
|
|
555
|
+
const controllerMatches = [...content.matchAll(PATTERNS.controller)];
|
|
556
|
+
for (const controllerMatch of controllerMatches) {
|
|
557
|
+
const basePath = controllerMatch[1] || "";
|
|
558
|
+
const controllerIndex = controllerMatch.index ?? 0;
|
|
559
|
+
const afterDecorator = content.slice(controllerIndex);
|
|
560
|
+
const classMatch = afterDecorator.match(/class\s+(\w+)/);
|
|
561
|
+
const controllerName = classMatch?.[1] ?? "UnknownController";
|
|
562
|
+
const nextController = content.indexOf("@Controller", controllerIndex + 1);
|
|
563
|
+
const controllerBlock = nextController > 0 ? content.slice(controllerIndex, nextController) : content.slice(controllerIndex);
|
|
564
|
+
const routeMatches = [...controllerBlock.matchAll(PATTERNS.route)];
|
|
565
|
+
for (const routeMatch of routeMatches) {
|
|
566
|
+
const method = routeMatch[1]?.toUpperCase();
|
|
567
|
+
const routePath = routeMatch[2] || "";
|
|
568
|
+
const fullPath = this.normalizePath(`/${basePath}/${routePath}`);
|
|
569
|
+
const afterRoute = controllerBlock.slice(routeMatch.index ?? 0);
|
|
570
|
+
const methodMatch = afterRoute.match(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*\w+(?:<[^>]+>)?)?\s*\{/);
|
|
571
|
+
const handlerName = methodMatch?.[1] ?? "unknownHandler";
|
|
572
|
+
const absoluteIndex = controllerIndex + (routeMatch.index ?? 0);
|
|
573
|
+
const lineNumber = content.slice(0, absoluteIndex).split(`
|
|
574
|
+
`).length;
|
|
575
|
+
const hasBody = PATTERNS.body.test(afterRoute.slice(0, 200));
|
|
576
|
+
const hasParams = PATTERNS.param.test(afterRoute.slice(0, 200));
|
|
577
|
+
const hasQuery = PATTERNS.query.test(afterRoute.slice(0, 200));
|
|
578
|
+
const endpoint = {
|
|
579
|
+
id: this.generateEndpointId(method, fullPath, handlerName),
|
|
580
|
+
method,
|
|
581
|
+
path: fullPath,
|
|
582
|
+
kind: this.methodToOpKind(method),
|
|
583
|
+
handlerName,
|
|
584
|
+
controllerName,
|
|
585
|
+
source: this.createLocation(file, lineNumber, lineNumber + 10),
|
|
586
|
+
confidence: this.createConfidence("medium", "decorator-hints"),
|
|
587
|
+
frameworkMeta: {
|
|
588
|
+
hasBody,
|
|
589
|
+
hasParams,
|
|
590
|
+
hasQuery
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
this.addEndpoint(ctx, endpoint);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
async extractDtos(ctx, file, content) {
|
|
598
|
+
const dtoMatches = [...content.matchAll(PATTERNS.dto)];
|
|
599
|
+
for (const match of dtoMatches) {
|
|
600
|
+
const name = match[1] ?? "UnknownDto";
|
|
601
|
+
const index = match.index ?? 0;
|
|
602
|
+
const lineNumber = content.slice(0, index).split(`
|
|
603
|
+
`).length;
|
|
604
|
+
const hasClassValidator = content.includes("class-validator") || content.includes("@IsString") || content.includes("@IsNumber");
|
|
605
|
+
const schema = {
|
|
606
|
+
id: this.generateSchemaId(name, file),
|
|
607
|
+
name,
|
|
608
|
+
schemaType: hasClassValidator ? "class-validator" : "typescript",
|
|
609
|
+
source: this.createLocation(file, lineNumber, lineNumber + 20),
|
|
610
|
+
confidence: this.createConfidence(hasClassValidator ? "high" : "medium", hasClassValidator ? "explicit-schema" : "inferred-types")
|
|
611
|
+
};
|
|
612
|
+
this.addSchema(ctx, schema);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
normalizePath(path) {
|
|
616
|
+
return "/" + path.replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "");
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// src/extractors/express/extractor.ts
|
|
620
|
+
var PATTERNS2 = {
|
|
621
|
+
route: /(?:app|router)\.(get|post|put|patch|delete|head|options)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
622
|
+
routerUse: /(?:app|router)\.use\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
623
|
+
zodValidate: /validate\s*\(\s*(\w+)\s*\)/g
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
class ExpressExtractor extends BaseExtractor {
|
|
627
|
+
id = "express";
|
|
628
|
+
name = "Express Extractor";
|
|
629
|
+
frameworks = ["express"];
|
|
630
|
+
priority = 15;
|
|
631
|
+
async doExtract(ctx) {
|
|
632
|
+
const { project, options, fs } = ctx;
|
|
633
|
+
const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
|
|
634
|
+
const files = await fs.glob(pattern, { cwd: project.rootPath });
|
|
635
|
+
ctx.ir.stats.filesScanned = files.length;
|
|
636
|
+
for (const file of files) {
|
|
637
|
+
if (file.includes("node_modules") || file.includes(".test."))
|
|
638
|
+
continue;
|
|
639
|
+
const fullPath = `${project.rootPath}/${file}`;
|
|
640
|
+
const content = await fs.readFile(fullPath);
|
|
641
|
+
await this.extractRoutes(ctx, file, content);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
async extractRoutes(ctx, file, content) {
|
|
645
|
+
const matches = [...content.matchAll(PATTERNS2.route)];
|
|
646
|
+
for (const match of matches) {
|
|
647
|
+
const method = match[1]?.toUpperCase() ?? "GET";
|
|
648
|
+
const path = match[2] ?? "/";
|
|
649
|
+
const index = match.index ?? 0;
|
|
650
|
+
const lineNumber = content.slice(0, index).split(`
|
|
651
|
+
`).length;
|
|
652
|
+
const afterMatch = content.slice(index, index + 500);
|
|
653
|
+
const handlerMatch = afterMatch.match(/(?:async\s+)?(?:function\s+)?(\w+)|,\s*(\w+)\s*\)/);
|
|
654
|
+
const handlerName = handlerMatch?.[1] ?? handlerMatch?.[2] ?? "handler";
|
|
655
|
+
const hasZodValidation = PATTERNS2.zodValidate.test(afterMatch);
|
|
656
|
+
const endpoint = {
|
|
657
|
+
id: this.generateEndpointId(method, path, handlerName),
|
|
658
|
+
method,
|
|
659
|
+
path,
|
|
660
|
+
kind: this.methodToOpKind(method),
|
|
661
|
+
handlerName,
|
|
662
|
+
source: this.createLocation(file, lineNumber, lineNumber + 5),
|
|
663
|
+
confidence: this.createConfidence(hasZodValidation ? "high" : "medium", hasZodValidation ? "explicit-schema" : "decorator-hints")
|
|
664
|
+
};
|
|
665
|
+
this.addEndpoint(ctx, endpoint);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// src/extractors/fastify/extractor.ts
|
|
670
|
+
var PATTERNS3 = {
|
|
671
|
+
route: /(?:fastify|app|server)\.(get|post|put|patch|delete|head|options)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
672
|
+
schemaOption: /schema\s*:\s*\{/g,
|
|
673
|
+
bodySchema: /body\s*:\s*(\w+)/g,
|
|
674
|
+
responseSchema: /response\s*:\s*\{/g
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
class FastifyExtractor extends BaseExtractor {
|
|
678
|
+
id = "fastify";
|
|
679
|
+
name = "Fastify Extractor";
|
|
680
|
+
frameworks = ["fastify"];
|
|
681
|
+
priority = 15;
|
|
682
|
+
async doExtract(ctx) {
|
|
683
|
+
const { project, options, fs } = ctx;
|
|
684
|
+
const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
|
|
685
|
+
const files = await fs.glob(pattern, { cwd: project.rootPath });
|
|
686
|
+
ctx.ir.stats.filesScanned = files.length;
|
|
687
|
+
for (const file of files) {
|
|
688
|
+
if (file.includes("node_modules") || file.includes(".test."))
|
|
689
|
+
continue;
|
|
690
|
+
const fullPath = `${project.rootPath}/${file}`;
|
|
691
|
+
const content = await fs.readFile(fullPath);
|
|
692
|
+
await this.extractRoutes(ctx, file, content);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
async extractRoutes(ctx, file, content) {
|
|
696
|
+
const matches = [...content.matchAll(PATTERNS3.route)];
|
|
697
|
+
for (const match of matches) {
|
|
698
|
+
const method = match[1]?.toUpperCase() ?? "GET";
|
|
699
|
+
const path = match[2] ?? "/";
|
|
700
|
+
const index = match.index ?? 0;
|
|
701
|
+
const lineNumber = content.slice(0, index).split(`
|
|
702
|
+
`).length;
|
|
703
|
+
const afterMatch = content.slice(index, index + 1000);
|
|
704
|
+
const hasSchema = PATTERNS3.schemaOption.test(afterMatch);
|
|
705
|
+
const endpoint = {
|
|
706
|
+
id: this.generateEndpointId(method, path),
|
|
707
|
+
method,
|
|
708
|
+
path,
|
|
709
|
+
kind: this.methodToOpKind(method),
|
|
710
|
+
handlerName: "handler",
|
|
711
|
+
source: this.createLocation(file, lineNumber, lineNumber + 10),
|
|
712
|
+
confidence: this.createConfidence(hasSchema ? "high" : "medium", hasSchema ? "explicit-schema" : "decorator-hints"),
|
|
713
|
+
frameworkMeta: { hasSchema }
|
|
714
|
+
};
|
|
715
|
+
this.addEndpoint(ctx, endpoint);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// src/extractors/hono/extractor.ts
|
|
720
|
+
var PATTERNS4 = {
|
|
721
|
+
route: /(?:app|hono)\.(get|post|put|patch|delete|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
722
|
+
zodValidator: /zValidator\s*\(\s*['"`](\w+)['"`]\s*,\s*(\w+)/g
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
class HonoExtractor extends BaseExtractor {
|
|
726
|
+
id = "hono";
|
|
727
|
+
name = "Hono Extractor";
|
|
728
|
+
frameworks = ["hono"];
|
|
729
|
+
priority = 15;
|
|
730
|
+
async doExtract(ctx) {
|
|
731
|
+
const { project, options, fs } = ctx;
|
|
732
|
+
const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
|
|
733
|
+
const files = await fs.glob(pattern, { cwd: project.rootPath });
|
|
734
|
+
ctx.ir.stats.filesScanned = files.length;
|
|
735
|
+
for (const file of files) {
|
|
736
|
+
if (file.includes("node_modules") || file.includes(".test."))
|
|
737
|
+
continue;
|
|
738
|
+
const fullPath = `${project.rootPath}/${file}`;
|
|
739
|
+
const content = await fs.readFile(fullPath);
|
|
740
|
+
await this.extractRoutes(ctx, file, content);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
async extractRoutes(ctx, file, content) {
|
|
744
|
+
const matches = [...content.matchAll(PATTERNS4.route)];
|
|
745
|
+
for (const match of matches) {
|
|
746
|
+
const method = match[1]?.toUpperCase() ?? "GET";
|
|
747
|
+
const path = match[2] ?? "/";
|
|
748
|
+
const index = match.index ?? 0;
|
|
749
|
+
const lineNumber = content.slice(0, index).split(`
|
|
750
|
+
`).length;
|
|
751
|
+
const afterMatch = content.slice(index, index + 500);
|
|
752
|
+
const hasZodValidator = PATTERNS4.zodValidator.test(afterMatch);
|
|
753
|
+
const endpoint = {
|
|
754
|
+
id: this.generateEndpointId(method, path),
|
|
755
|
+
method,
|
|
756
|
+
path,
|
|
757
|
+
kind: this.methodToOpKind(method),
|
|
758
|
+
handlerName: "handler",
|
|
759
|
+
source: this.createLocation(file, lineNumber, lineNumber + 5),
|
|
760
|
+
confidence: this.createConfidence(hasZodValidator ? "high" : "medium", hasZodValidator ? "explicit-schema" : "decorator-hints")
|
|
761
|
+
};
|
|
762
|
+
this.addEndpoint(ctx, endpoint);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
// src/extractors/elysia/extractor.ts
|
|
767
|
+
var PATTERNS5 = {
|
|
768
|
+
route: /\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
769
|
+
tSchema: /body:\s*t\.\w+/g
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
class ElysiaExtractor extends BaseExtractor {
|
|
773
|
+
id = "elysia";
|
|
774
|
+
name = "Elysia Extractor";
|
|
775
|
+
frameworks = ["elysia"];
|
|
776
|
+
priority = 15;
|
|
777
|
+
async doExtract(ctx) {
|
|
778
|
+
const { project, options, fs } = ctx;
|
|
779
|
+
const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
|
|
780
|
+
const files = await fs.glob(pattern, { cwd: project.rootPath });
|
|
781
|
+
ctx.ir.stats.filesScanned = files.length;
|
|
782
|
+
for (const file of files) {
|
|
783
|
+
if (file.includes("node_modules") || file.includes(".test."))
|
|
784
|
+
continue;
|
|
785
|
+
const fullPath = `${project.rootPath}/${file}`;
|
|
786
|
+
const content = await fs.readFile(fullPath);
|
|
787
|
+
if (!content.includes("elysia"))
|
|
788
|
+
continue;
|
|
789
|
+
await this.extractRoutes(ctx, file, content);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
async extractRoutes(ctx, file, content) {
|
|
793
|
+
const matches = [...content.matchAll(PATTERNS5.route)];
|
|
794
|
+
for (const match of matches) {
|
|
795
|
+
const method = match[1]?.toUpperCase() ?? "GET";
|
|
796
|
+
const path = match[2] ?? "/";
|
|
797
|
+
const index = match.index ?? 0;
|
|
798
|
+
const lineNumber = content.slice(0, index).split(`
|
|
799
|
+
`).length;
|
|
800
|
+
const afterMatch = content.slice(index, index + 500);
|
|
801
|
+
const hasTSchema = PATTERNS5.tSchema.test(afterMatch);
|
|
802
|
+
const endpoint = {
|
|
803
|
+
id: this.generateEndpointId(method, path),
|
|
804
|
+
method,
|
|
805
|
+
path,
|
|
806
|
+
kind: this.methodToOpKind(method),
|
|
807
|
+
handlerName: "handler",
|
|
808
|
+
source: this.createLocation(file, lineNumber, lineNumber + 5),
|
|
809
|
+
confidence: this.createConfidence(hasTSchema ? "high" : "medium", hasTSchema ? "explicit-schema" : "decorator-hints")
|
|
810
|
+
};
|
|
811
|
+
this.addEndpoint(ctx, endpoint);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
// src/extractors/trpc/extractor.ts
|
|
816
|
+
var PATTERNS6 = {
|
|
817
|
+
procedure: /\.(query|mutation)\s*\(\s*(?:\{[^}]*\}|[^)]+)\)/gi,
|
|
818
|
+
procedureName: /(\w+)\s*:\s*(?:publicProcedure|protectedProcedure|procedure)/g,
|
|
819
|
+
zodInput: /\.input\s*\(\s*(\w+)/g,
|
|
820
|
+
zodOutput: /\.output\s*\(\s*(\w+)/g
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
class TrpcExtractor extends BaseExtractor {
|
|
824
|
+
id = "trpc";
|
|
825
|
+
name = "tRPC Extractor";
|
|
826
|
+
frameworks = ["trpc"];
|
|
827
|
+
priority = 15;
|
|
828
|
+
async doExtract(ctx) {
|
|
829
|
+
const { project, options, fs } = ctx;
|
|
830
|
+
const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
|
|
831
|
+
const files = await fs.glob(pattern, { cwd: project.rootPath });
|
|
832
|
+
ctx.ir.stats.filesScanned = files.length;
|
|
833
|
+
for (const file of files) {
|
|
834
|
+
if (file.includes("node_modules") || file.includes(".test."))
|
|
835
|
+
continue;
|
|
836
|
+
const fullPath = `${project.rootPath}/${file}`;
|
|
837
|
+
const content = await fs.readFile(fullPath);
|
|
838
|
+
if (!content.includes("trpc") && !content.includes("Procedure"))
|
|
839
|
+
continue;
|
|
840
|
+
await this.extractProcedures(ctx, file, content);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async extractProcedures(ctx, file, content) {
|
|
844
|
+
const nameMatches = [...content.matchAll(PATTERNS6.procedureName)];
|
|
845
|
+
for (const match of nameMatches) {
|
|
846
|
+
const procedureName = match[1] ?? "unknownProcedure";
|
|
847
|
+
const index = match.index ?? 0;
|
|
848
|
+
const lineNumber = content.slice(0, index).split(`
|
|
849
|
+
`).length;
|
|
850
|
+
const afterMatch = content.slice(index, index + 500);
|
|
851
|
+
const isQuery = afterMatch.includes(".query(");
|
|
852
|
+
const isMutation = afterMatch.includes(".mutation(");
|
|
853
|
+
if (!isQuery && !isMutation)
|
|
854
|
+
continue;
|
|
855
|
+
const hasZodInput = PATTERNS6.zodInput.test(afterMatch);
|
|
856
|
+
const hasZodOutput = PATTERNS6.zodOutput.test(afterMatch);
|
|
857
|
+
const hasSchema = hasZodInput || hasZodOutput;
|
|
858
|
+
const method = isMutation ? "POST" : "GET";
|
|
859
|
+
const endpoint = {
|
|
860
|
+
id: `trpc.${procedureName}`,
|
|
861
|
+
method,
|
|
862
|
+
path: `/trpc/${procedureName}`,
|
|
863
|
+
kind: isMutation ? "command" : "query",
|
|
864
|
+
handlerName: procedureName,
|
|
865
|
+
source: this.createLocation(file, lineNumber, lineNumber + 10),
|
|
866
|
+
confidence: this.createConfidence(hasSchema ? "high" : "medium", hasSchema ? "explicit-schema" : "inferred-types"),
|
|
867
|
+
frameworkMeta: {
|
|
868
|
+
procedureType: isMutation ? "mutation" : "query",
|
|
869
|
+
hasInput: hasZodInput,
|
|
870
|
+
hasOutput: hasZodOutput
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
this.addEndpoint(ctx, endpoint);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
// src/extractors/next-api/extractor.ts
|
|
878
|
+
var PATTERNS7 = {
|
|
879
|
+
appRouterExport: /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/gi,
|
|
880
|
+
pagesHandler: /export\s+default\s+(?:async\s+)?function/g,
|
|
881
|
+
zodSchema: /z\.\w+\(/g
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
class NextApiExtractor extends BaseExtractor {
|
|
885
|
+
id = "next-api";
|
|
886
|
+
name = "Next.js API Extractor";
|
|
887
|
+
frameworks = ["next-api"];
|
|
888
|
+
priority = 15;
|
|
889
|
+
async doExtract(ctx) {
|
|
890
|
+
const { project, fs } = ctx;
|
|
891
|
+
const appRoutes = await fs.glob("**/app/api/**/route.ts", {
|
|
892
|
+
cwd: project.rootPath
|
|
893
|
+
});
|
|
894
|
+
const pagesRoutes = await fs.glob("**/pages/api/**/*.ts", {
|
|
895
|
+
cwd: project.rootPath
|
|
896
|
+
});
|
|
897
|
+
const allRoutes = [...appRoutes, ...pagesRoutes];
|
|
898
|
+
ctx.ir.stats.filesScanned = allRoutes.length;
|
|
899
|
+
for (const file of allRoutes) {
|
|
900
|
+
const fullPath = `${project.rootPath}/${file}`;
|
|
901
|
+
const content = await fs.readFile(fullPath);
|
|
902
|
+
if (file.includes("/app/api/")) {
|
|
903
|
+
await this.extractAppRoutes(ctx, file, content);
|
|
904
|
+
} else {
|
|
905
|
+
await this.extractPagesRoutes(ctx, file, content);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
async extractAppRoutes(ctx, file, content) {
|
|
910
|
+
const pathMatch = file.match(/app\/api\/(.+)\/route\.ts$/);
|
|
911
|
+
const routePath = pathMatch ? `/api/${pathMatch[1]}` : "/api";
|
|
912
|
+
const matches = [...content.matchAll(PATTERNS7.appRouterExport)];
|
|
913
|
+
for (const match of matches) {
|
|
914
|
+
const method = match[1]?.toUpperCase() ?? "GET";
|
|
915
|
+
const index = match.index ?? 0;
|
|
916
|
+
const lineNumber = content.slice(0, index).split(`
|
|
917
|
+
`).length;
|
|
918
|
+
const hasZod = PATTERNS7.zodSchema.test(content);
|
|
919
|
+
const endpoint = {
|
|
920
|
+
id: this.generateEndpointId(method, routePath),
|
|
921
|
+
method,
|
|
922
|
+
path: routePath.replace(/\[(\w+)\]/g, ":$1"),
|
|
923
|
+
kind: this.methodToOpKind(method),
|
|
924
|
+
handlerName: method,
|
|
925
|
+
source: this.createLocation(file, lineNumber, lineNumber + 10),
|
|
926
|
+
confidence: this.createConfidence(hasZod ? "high" : "medium", hasZod ? "explicit-schema" : "inferred-types"),
|
|
927
|
+
frameworkMeta: { routeType: "app-router" }
|
|
928
|
+
};
|
|
929
|
+
this.addEndpoint(ctx, endpoint);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
async extractPagesRoutes(ctx, file, content) {
|
|
933
|
+
const pathMatch = file.match(/pages\/api\/(.+)\.ts$/);
|
|
934
|
+
const routePath = pathMatch ? `/api/${pathMatch[1]}` : "/api";
|
|
935
|
+
if (!PATTERNS7.pagesHandler.test(content))
|
|
936
|
+
return;
|
|
937
|
+
const lineNumber = 1;
|
|
938
|
+
const _hasZod = PATTERNS7.zodSchema.test(content);
|
|
939
|
+
const methods = ["GET", "POST"];
|
|
940
|
+
for (const method of methods) {
|
|
941
|
+
const endpoint = {
|
|
942
|
+
id: this.generateEndpointId(method, routePath),
|
|
943
|
+
method,
|
|
944
|
+
path: routePath.replace(/\[(\w+)\]/g, ":$1"),
|
|
945
|
+
kind: this.methodToOpKind(method),
|
|
946
|
+
handlerName: "handler",
|
|
947
|
+
source: this.createLocation(file, lineNumber, lineNumber + 20),
|
|
948
|
+
confidence: this.createConfidence("low", "naming-convention"),
|
|
949
|
+
frameworkMeta: { routeType: "pages-router" }
|
|
950
|
+
};
|
|
951
|
+
this.addEndpoint(ctx, endpoint);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// src/extractors/zod/extractor.ts
|
|
956
|
+
var PATTERNS8 = {
|
|
957
|
+
zodSchema: /(?:export\s+)?const\s+(\w+)\s*=\s*z\.(?:object|string|number|boolean|array|enum|union|intersection|literal|tuple|record)/g,
|
|
958
|
+
zodInfer: /type\s+(\w+)\s*=\s*z\.infer<typeof\s+(\w+)>/g
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
class ZodSchemaExtractor extends BaseExtractor {
|
|
962
|
+
id = "zod";
|
|
963
|
+
name = "Zod Schema Extractor";
|
|
964
|
+
frameworks = ["zod"];
|
|
965
|
+
priority = 5;
|
|
966
|
+
async detect() {
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
async doExtract(ctx) {
|
|
970
|
+
const { project, options, fs } = ctx;
|
|
971
|
+
const pattern = options.scope?.length ? options.scope.map((s) => `${s}/**/*.ts`).join(",") : "**/*.ts";
|
|
972
|
+
const files = await fs.glob(pattern, { cwd: project.rootPath });
|
|
973
|
+
ctx.ir.stats.filesScanned = files.length;
|
|
974
|
+
for (const file of files) {
|
|
975
|
+
if (file.includes("node_modules") || file.includes(".test."))
|
|
976
|
+
continue;
|
|
977
|
+
const fullPath = `${project.rootPath}/${file}`;
|
|
978
|
+
const content = await fs.readFile(fullPath);
|
|
979
|
+
if (!content.includes("z."))
|
|
980
|
+
continue;
|
|
981
|
+
await this.extractSchemas(ctx, file, content);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
async extractSchemas(ctx, file, content) {
|
|
985
|
+
const matches = [...content.matchAll(PATTERNS8.zodSchema)];
|
|
986
|
+
for (const match of matches) {
|
|
987
|
+
const name = match[1] ?? "unknownSchema";
|
|
988
|
+
const index = match.index ?? 0;
|
|
989
|
+
const lineNumber = content.slice(0, index).split(`
|
|
990
|
+
`).length;
|
|
991
|
+
let depth = 0;
|
|
992
|
+
let endIndex = index;
|
|
993
|
+
for (let i = index;i < content.length; i++) {
|
|
994
|
+
const char = content[i];
|
|
995
|
+
if (char === "(" || char === "{" || char === "[")
|
|
996
|
+
depth++;
|
|
997
|
+
if (char === ")" || char === "}" || char === "]")
|
|
998
|
+
depth--;
|
|
999
|
+
if (depth === 0 && (char === ";" || char === `
|
|
1000
|
+
`)) {
|
|
1001
|
+
endIndex = i;
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
const rawDefinition = content.slice(index, endIndex + 1);
|
|
1006
|
+
const endLineNumber = lineNumber + rawDefinition.split(`
|
|
1007
|
+
`).length - 1;
|
|
1008
|
+
const schema = {
|
|
1009
|
+
id: this.generateSchemaId(name, file),
|
|
1010
|
+
name,
|
|
1011
|
+
schemaType: "zod",
|
|
1012
|
+
rawDefinition,
|
|
1013
|
+
source: this.createLocation(file, lineNumber, endLineNumber),
|
|
1014
|
+
confidence: this.createConfidence("high", "explicit-schema")
|
|
1015
|
+
};
|
|
1016
|
+
this.addSchema(ctx, schema);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
// src/extractors/index.ts
|
|
1021
|
+
function registerAllExtractors() {
|
|
1022
|
+
extractorRegistry.register(new NestJsExtractor);
|
|
1023
|
+
extractorRegistry.register(new ExpressExtractor);
|
|
1024
|
+
extractorRegistry.register(new FastifyExtractor);
|
|
1025
|
+
extractorRegistry.register(new HonoExtractor);
|
|
1026
|
+
extractorRegistry.register(new ElysiaExtractor);
|
|
1027
|
+
extractorRegistry.register(new TrpcExtractor);
|
|
1028
|
+
extractorRegistry.register(new NextApiExtractor);
|
|
1029
|
+
extractorRegistry.register(new ZodSchemaExtractor);
|
|
1030
|
+
}
|
|
1031
|
+
// src/detect.ts
|
|
1032
|
+
var FRAMEWORK_RULES = [
|
|
1033
|
+
{
|
|
1034
|
+
id: "nestjs",
|
|
1035
|
+
name: "NestJS",
|
|
1036
|
+
packages: ["@nestjs/core", "@nestjs/common"],
|
|
1037
|
+
importPatterns: [/@nestjs\//]
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
id: "express",
|
|
1041
|
+
name: "Express",
|
|
1042
|
+
packages: ["express"],
|
|
1043
|
+
importPatterns: [/from ['"]express['"]/]
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
id: "fastify",
|
|
1047
|
+
name: "Fastify",
|
|
1048
|
+
packages: ["fastify"],
|
|
1049
|
+
importPatterns: [/from ['"]fastify['"]/]
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
id: "hono",
|
|
1053
|
+
name: "Hono",
|
|
1054
|
+
packages: ["hono"],
|
|
1055
|
+
importPatterns: [/from ['"]hono['"]/]
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
id: "elysia",
|
|
1059
|
+
name: "Elysia",
|
|
1060
|
+
packages: ["elysia"],
|
|
1061
|
+
importPatterns: [/from ['"]elysia['"]/]
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
id: "trpc",
|
|
1065
|
+
name: "tRPC",
|
|
1066
|
+
packages: ["@trpc/server"],
|
|
1067
|
+
importPatterns: [/@trpc\/server/]
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
id: "next-api",
|
|
1071
|
+
name: "Next.js API",
|
|
1072
|
+
packages: ["next"],
|
|
1073
|
+
filePatterns: [/app\/api\/.*\/route\.ts$/, /pages\/api\/.*\.ts$/]
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
id: "koa",
|
|
1077
|
+
name: "Koa",
|
|
1078
|
+
packages: ["koa", "@koa/router"],
|
|
1079
|
+
importPatterns: [/from ['"]koa['"]/]
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
id: "hapi",
|
|
1083
|
+
name: "Hapi",
|
|
1084
|
+
packages: ["@hapi/hapi"],
|
|
1085
|
+
importPatterns: [/@hapi\/hapi/]
|
|
1086
|
+
}
|
|
1087
|
+
];
|
|
1088
|
+
function detectFrameworksFromPackageJson(packageJson) {
|
|
1089
|
+
const allDeps = {
|
|
1090
|
+
...packageJson.dependencies,
|
|
1091
|
+
...packageJson.devDependencies,
|
|
1092
|
+
...packageJson.peerDependencies
|
|
1093
|
+
};
|
|
1094
|
+
const detected = [];
|
|
1095
|
+
for (const rule of FRAMEWORK_RULES) {
|
|
1096
|
+
for (const pkg of rule.packages) {
|
|
1097
|
+
if (pkg in allDeps) {
|
|
1098
|
+
detected.push({
|
|
1099
|
+
id: rule.id,
|
|
1100
|
+
name: rule.name,
|
|
1101
|
+
version: allDeps[pkg],
|
|
1102
|
+
confidence: "high"
|
|
1103
|
+
});
|
|
1104
|
+
break;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return detected;
|
|
1109
|
+
}
|
|
1110
|
+
function detectFrameworksFromCode(sourceCode) {
|
|
1111
|
+
const detected = [];
|
|
1112
|
+
const seenIds = new Set;
|
|
1113
|
+
for (const rule of FRAMEWORK_RULES) {
|
|
1114
|
+
if (!rule.importPatterns)
|
|
1115
|
+
continue;
|
|
1116
|
+
for (const pattern of rule.importPatterns) {
|
|
1117
|
+
if (pattern.test(sourceCode) && !seenIds.has(rule.id)) {
|
|
1118
|
+
detected.push({
|
|
1119
|
+
id: rule.id,
|
|
1120
|
+
name: rule.name,
|
|
1121
|
+
confidence: "medium"
|
|
1122
|
+
});
|
|
1123
|
+
seenIds.add(rule.id);
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return detected;
|
|
1129
|
+
}
|
|
1130
|
+
function detectFrameworksFromPaths(filePaths) {
|
|
1131
|
+
const detected = [];
|
|
1132
|
+
const seenIds = new Set;
|
|
1133
|
+
for (const rule of FRAMEWORK_RULES) {
|
|
1134
|
+
if (!rule.filePatterns)
|
|
1135
|
+
continue;
|
|
1136
|
+
for (const pattern of rule.filePatterns) {
|
|
1137
|
+
for (const filePath of filePaths) {
|
|
1138
|
+
if (pattern.test(filePath) && !seenIds.has(rule.id)) {
|
|
1139
|
+
detected.push({
|
|
1140
|
+
id: rule.id,
|
|
1141
|
+
name: rule.name,
|
|
1142
|
+
confidence: "medium"
|
|
1143
|
+
});
|
|
1144
|
+
seenIds.add(rule.id);
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return detected;
|
|
1151
|
+
}
|
|
1152
|
+
function mergeFrameworkDetections(...detections) {
|
|
1153
|
+
const byId = new Map;
|
|
1154
|
+
const confidenceOrder = {
|
|
1155
|
+
high: 3,
|
|
1156
|
+
medium: 2,
|
|
1157
|
+
low: 1,
|
|
1158
|
+
ambiguous: 0
|
|
1159
|
+
};
|
|
1160
|
+
for (const group of detections) {
|
|
1161
|
+
for (const fw of group) {
|
|
1162
|
+
const existing = byId.get(fw.id);
|
|
1163
|
+
if (!existing || confidenceOrder[fw.confidence] > confidenceOrder[existing.confidence]) {
|
|
1164
|
+
byId.set(fw.id, fw);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return Array.from(byId.values());
|
|
1169
|
+
}
|
|
1170
|
+
async function detectFramework(rootPath, options) {
|
|
1171
|
+
const project = {
|
|
1172
|
+
rootPath,
|
|
1173
|
+
frameworks: []
|
|
1174
|
+
};
|
|
1175
|
+
if (options?.readFile) {
|
|
1176
|
+
try {
|
|
1177
|
+
const packageJsonPath = `${rootPath}/package.json`;
|
|
1178
|
+
const content = await options.readFile(packageJsonPath);
|
|
1179
|
+
const packageJson = JSON.parse(content);
|
|
1180
|
+
project.packageJsonPath = packageJsonPath;
|
|
1181
|
+
project.frameworks = detectFrameworksFromPackageJson(packageJson);
|
|
1182
|
+
} catch {}
|
|
1183
|
+
}
|
|
1184
|
+
if (options?.readFile) {
|
|
1185
|
+
try {
|
|
1186
|
+
const tsConfigPath = `${rootPath}/tsconfig.json`;
|
|
1187
|
+
await options.readFile(tsConfigPath);
|
|
1188
|
+
project.tsConfigPath = tsConfigPath;
|
|
1189
|
+
} catch {}
|
|
1190
|
+
}
|
|
1191
|
+
return project;
|
|
1192
|
+
}
|
|
1193
|
+
function getSupportedFrameworks() {
|
|
1194
|
+
return FRAMEWORK_RULES.map((r) => r.id);
|
|
1195
|
+
}
|
|
1196
|
+
function isFrameworkSupported(id) {
|
|
1197
|
+
return FRAMEWORK_RULES.some((r) => r.id === id);
|
|
1198
|
+
}
|
|
1199
|
+
export {
|
|
1200
|
+
registerBuiltInExtractors,
|
|
1201
|
+
mergeIRs,
|
|
1202
|
+
mergeFrameworkDetections,
|
|
1203
|
+
isFrameworkSupported,
|
|
1204
|
+
getSupportedFrameworks,
|
|
1205
|
+
exports_extractors as extractors,
|
|
1206
|
+
extractorRegistry,
|
|
1207
|
+
extractFromProject,
|
|
1208
|
+
detectFrameworksFromPaths,
|
|
1209
|
+
detectFrameworksFromPackageJson,
|
|
1210
|
+
detectFrameworksFromCode,
|
|
1211
|
+
detectFramework,
|
|
1212
|
+
createEmptyIR,
|
|
1213
|
+
exports_codegen as codegen,
|
|
1214
|
+
ExtractorRegistry
|
|
1215
|
+
};
|