@getmikk/core 1.3.2 → 1.5.1
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/package.json +1 -1
- package/src/contract/contract-generator.ts +87 -8
- package/src/contract/lock-compiler.ts +174 -8
- package/src/contract/lock-reader.ts +269 -3
- package/src/contract/schema.ts +31 -0
- package/src/graph/cluster-detector.ts +286 -18
- package/src/graph/graph-builder.ts +2 -0
- package/src/graph/types.ts +2 -0
- package/src/index.ts +2 -1
- package/src/parser/boundary-checker.ts +74 -2
- package/src/parser/types.ts +11 -0
- package/src/parser/typescript/ts-extractor.ts +146 -8
- package/src/parser/typescript/ts-parser.ts +32 -5
- package/src/utils/fs.ts +586 -4
- package/tests/fs.test.ts +186 -0
- package/tests/helpers.ts +6 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
|
-
import type { ParsedFunction, ParsedClass, ParsedImport, ParsedExport, ParsedParam, ParsedGeneric } from '../types.js'
|
|
2
|
+
import type { ParsedFunction, ParsedClass, ParsedImport, ParsedExport, ParsedParam, ParsedGeneric, ParsedRoute } from '../types.js'
|
|
3
3
|
import { hashContent } from '../../hash/file-hasher.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -18,10 +18,18 @@ export class TypeScriptExtractor {
|
|
|
18
18
|
content,
|
|
19
19
|
ts.ScriptTarget.Latest,
|
|
20
20
|
true, // setParentNodes
|
|
21
|
-
|
|
21
|
+
this.inferScriptKind(filePath)
|
|
22
22
|
)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** Infer TypeScript ScriptKind from file extension (supports JS/JSX/TS/TSX) */
|
|
26
|
+
private inferScriptKind(filePath: string): ts.ScriptKind {
|
|
27
|
+
if (filePath.endsWith('.tsx')) return ts.ScriptKind.TSX
|
|
28
|
+
if (filePath.endsWith('.jsx')) return ts.ScriptKind.JSX
|
|
29
|
+
if (filePath.endsWith('.js') || filePath.endsWith('.mjs') || filePath.endsWith('.cjs')) return ts.ScriptKind.JS
|
|
30
|
+
return ts.ScriptKind.TS
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
/** Extract all top-level and variable-assigned functions */
|
|
26
34
|
extractFunctions(): ParsedFunction[] {
|
|
27
35
|
const functions: ParsedFunction[] = []
|
|
@@ -119,7 +127,7 @@ export class TypeScriptExtractor {
|
|
|
119
127
|
return false
|
|
120
128
|
}
|
|
121
129
|
|
|
122
|
-
/** Extract all import statements */
|
|
130
|
+
/** Extract all import statements (static and dynamic) */
|
|
123
131
|
extractImports(): ParsedImport[] {
|
|
124
132
|
const imports: ParsedImport[] = []
|
|
125
133
|
this.walkNode(this.sourceFile, (node) => {
|
|
@@ -128,6 +136,25 @@ export class TypeScriptExtractor {
|
|
|
128
136
|
if (parsed) imports.push(parsed)
|
|
129
137
|
}
|
|
130
138
|
})
|
|
139
|
+
|
|
140
|
+
// Also detect dynamic import() calls: await import('./path')
|
|
141
|
+
const walkDynamic = (n: ts.Node) => {
|
|
142
|
+
if (ts.isCallExpression(n) && n.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
143
|
+
const arg = n.arguments[0]
|
|
144
|
+
if (arg && ts.isStringLiteral(arg)) {
|
|
145
|
+
imports.push({
|
|
146
|
+
source: arg.text,
|
|
147
|
+
resolvedPath: '', // Filled in by resolver
|
|
148
|
+
names: [],
|
|
149
|
+
isDefault: false,
|
|
150
|
+
isDynamic: true,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
ts.forEachChild(n, walkDynamic)
|
|
155
|
+
}
|
|
156
|
+
ts.forEachChild(this.sourceFile, walkDynamic)
|
|
157
|
+
|
|
131
158
|
return imports
|
|
132
159
|
}
|
|
133
160
|
|
|
@@ -205,6 +232,83 @@ export class TypeScriptExtractor {
|
|
|
205
232
|
return exports
|
|
206
233
|
}
|
|
207
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Extract HTTP route registrations.
|
|
237
|
+
* Detects Express/Koa/Hono patterns like:
|
|
238
|
+
* router.get("/path", handler)
|
|
239
|
+
* app.post("/path", middleware, handler)
|
|
240
|
+
* app.use("/prefix", subrouter)
|
|
241
|
+
* router.use(middleware)
|
|
242
|
+
*/
|
|
243
|
+
extractRoutes(): ParsedRoute[] {
|
|
244
|
+
const routes: ParsedRoute[] = []
|
|
245
|
+
const HTTP_METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'all', 'use'])
|
|
246
|
+
// Only detect routes on receiver objects that look like routers/apps
|
|
247
|
+
const ROUTER_NAMES = new Set(['app', 'router', 'server', 'route', 'api', 'express'])
|
|
248
|
+
|
|
249
|
+
const walk = (node: ts.Node) => {
|
|
250
|
+
if (
|
|
251
|
+
ts.isCallExpression(node) &&
|
|
252
|
+
ts.isPropertyAccessExpression(node.expression)
|
|
253
|
+
) {
|
|
254
|
+
const methodName = node.expression.name.text.toLowerCase()
|
|
255
|
+
if (HTTP_METHODS.has(methodName)) {
|
|
256
|
+
// Check if the receiver is a known router/app-like identifier
|
|
257
|
+
const receiver = node.expression.expression
|
|
258
|
+
let receiverName = ''
|
|
259
|
+
if (ts.isIdentifier(receiver)) {
|
|
260
|
+
receiverName = receiver.text.toLowerCase()
|
|
261
|
+
} else if (ts.isPropertyAccessExpression(receiver) && ts.isIdentifier(receiver.expression)) {
|
|
262
|
+
receiverName = receiver.expression.text.toLowerCase()
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Skip if receiver doesn't look like a router (e.g. prisma.file.delete)
|
|
266
|
+
if (!ROUTER_NAMES.has(receiverName)) {
|
|
267
|
+
ts.forEachChild(node, walk)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const args = node.arguments
|
|
272
|
+
let routePath = ''
|
|
273
|
+
const middlewares: string[] = []
|
|
274
|
+
let handler = 'anonymous'
|
|
275
|
+
|
|
276
|
+
for (let i = 0; i < args.length; i++) {
|
|
277
|
+
const arg = args[i]
|
|
278
|
+
// First string literal is the route path
|
|
279
|
+
if (ts.isStringLiteral(arg) && !routePath) {
|
|
280
|
+
routePath = arg.text
|
|
281
|
+
} else if (ts.isIdentifier(arg)) {
|
|
282
|
+
// Last identifier is the handler; earlier ones are middleware
|
|
283
|
+
if (i === args.length - 1) {
|
|
284
|
+
handler = arg.text
|
|
285
|
+
} else {
|
|
286
|
+
middlewares.push(arg.text)
|
|
287
|
+
}
|
|
288
|
+
} else if (ts.isCallExpression(arg)) {
|
|
289
|
+
// e.g. upload.single("file") — middleware call
|
|
290
|
+
middlewares.push(arg.expression.getText(this.sourceFile))
|
|
291
|
+
} else if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
|
|
292
|
+
handler = 'anonymous'
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
routes.push({
|
|
297
|
+
method: methodName.toUpperCase(),
|
|
298
|
+
path: routePath || '*',
|
|
299
|
+
handler,
|
|
300
|
+
middlewares,
|
|
301
|
+
file: this.filePath,
|
|
302
|
+
line: this.getLineNumber(node.getStart()),
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
ts.forEachChild(node, walk)
|
|
307
|
+
}
|
|
308
|
+
ts.forEachChild(this.sourceFile, walk)
|
|
309
|
+
return routes
|
|
310
|
+
}
|
|
311
|
+
|
|
208
312
|
// ─── Private Helpers ──────────────────────────────────────
|
|
209
313
|
|
|
210
314
|
private parseFunctionDeclaration(node: ts.FunctionDeclaration): ParsedFunction {
|
|
@@ -286,7 +390,32 @@ export class TypeScriptExtractor {
|
|
|
286
390
|
const typeParameters = this.extractTypeParameters(node.typeParameters)
|
|
287
391
|
|
|
288
392
|
for (const member of node.members) {
|
|
289
|
-
if (ts.
|
|
393
|
+
if (ts.isConstructorDeclaration(member)) {
|
|
394
|
+
// Track class constructors as methods
|
|
395
|
+
const mStartLine = this.getLineNumber(member.getStart())
|
|
396
|
+
const mEndLine = this.getLineNumber(member.getEnd())
|
|
397
|
+
const params = this.extractParams(member.parameters)
|
|
398
|
+
const calls = this.extractCalls(member)
|
|
399
|
+
const bodyText = member.getText(this.sourceFile)
|
|
400
|
+
|
|
401
|
+
methods.push({
|
|
402
|
+
id: `fn:${this.filePath}:${name}.constructor`,
|
|
403
|
+
name: `${name}.constructor`,
|
|
404
|
+
file: this.filePath,
|
|
405
|
+
startLine: mStartLine,
|
|
406
|
+
endLine: mEndLine,
|
|
407
|
+
params,
|
|
408
|
+
returnType: name,
|
|
409
|
+
isExported: this.hasExportModifier(node),
|
|
410
|
+
isAsync: false,
|
|
411
|
+
calls,
|
|
412
|
+
hash: hashContent(bodyText),
|
|
413
|
+
purpose: this.extractPurpose(member),
|
|
414
|
+
edgeCasesHandled: this.extractEdgeCases(member),
|
|
415
|
+
errorHandling: this.extractErrorHandling(member),
|
|
416
|
+
detailedLines: this.extractDetailedLines(member),
|
|
417
|
+
})
|
|
418
|
+
} else if (ts.isMethodDeclaration(member) && member.name) {
|
|
290
419
|
const methodName = member.name.getText(this.sourceFile)
|
|
291
420
|
const mStartLine = this.getLineNumber(member.getStart())
|
|
292
421
|
const mEndLine = this.getLineNumber(member.getEnd())
|
|
@@ -373,7 +502,7 @@ export class TypeScriptExtractor {
|
|
|
373
502
|
}
|
|
374
503
|
}
|
|
375
504
|
|
|
376
|
-
/** Extract function/method call expressions from a node */
|
|
505
|
+
/** Extract function/method call expressions from a node (including new Foo()) */
|
|
377
506
|
private extractCalls(node: ts.Node): string[] {
|
|
378
507
|
const calls: string[] = []
|
|
379
508
|
const walkCalls = (n: ts.Node) => {
|
|
@@ -386,6 +515,15 @@ export class TypeScriptExtractor {
|
|
|
386
515
|
calls.push(callee.getText(this.sourceFile))
|
|
387
516
|
}
|
|
388
517
|
}
|
|
518
|
+
// Track constructor calls: new Foo(...) → "Foo"
|
|
519
|
+
if (ts.isNewExpression(n)) {
|
|
520
|
+
const callee = n.expression
|
|
521
|
+
if (ts.isIdentifier(callee)) {
|
|
522
|
+
calls.push(callee.text)
|
|
523
|
+
} else if (ts.isPropertyAccessExpression(callee)) {
|
|
524
|
+
calls.push(callee.getText(this.sourceFile))
|
|
525
|
+
}
|
|
526
|
+
}
|
|
389
527
|
ts.forEachChild(n, walkCalls)
|
|
390
528
|
}
|
|
391
529
|
ts.forEachChild(node, walkCalls)
|
|
@@ -414,9 +552,9 @@ export class TypeScriptExtractor {
|
|
|
414
552
|
if (clean) meaningfulLines.push(clean)
|
|
415
553
|
}
|
|
416
554
|
|
|
417
|
-
// Return the
|
|
418
|
-
//
|
|
419
|
-
return meaningfulLines.length > 0 ? meaningfulLines[
|
|
555
|
+
// Return the first meaningful line — in JSDoc, the first line is the summary.
|
|
556
|
+
// e.g. "Decrypt data using AES-256-GCM" (not a later detail line)
|
|
557
|
+
return meaningfulLines.length > 0 ? meaningfulLines[0].split('\n')[0].trim() : ''
|
|
420
558
|
}
|
|
421
559
|
|
|
422
560
|
/** Extract edge cases handled (if statements returning early) */
|
|
@@ -12,14 +12,41 @@ export class TypeScriptParser extends BaseParser {
|
|
|
12
12
|
/** Parse a single TypeScript file */
|
|
13
13
|
parse(filePath: string, content: string): ParsedFile {
|
|
14
14
|
const extractor = new TypeScriptExtractor(filePath, content)
|
|
15
|
+
const functions = extractor.extractFunctions()
|
|
16
|
+
const classes = extractor.extractClasses()
|
|
17
|
+
const generics = extractor.extractGenerics()
|
|
18
|
+
const imports = extractor.extractImports()
|
|
19
|
+
const exports = extractor.extractExports()
|
|
20
|
+
const routes = extractor.extractRoutes()
|
|
21
|
+
|
|
22
|
+
// Cross-reference: if a function/class/generic is named in an export { Name }
|
|
23
|
+
// or export default declaration, mark it as exported.
|
|
24
|
+
const exportedNames = new Set(exports.map(e => e.name))
|
|
25
|
+
for (const fn of functions) {
|
|
26
|
+
if (!fn.isExported && exportedNames.has(fn.name)) {
|
|
27
|
+
fn.isExported = true
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const cls of classes) {
|
|
31
|
+
if (!cls.isExported && exportedNames.has(cls.name)) {
|
|
32
|
+
cls.isExported = true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const gen of generics) {
|
|
36
|
+
if (!gen.isExported && exportedNames.has(gen.name)) {
|
|
37
|
+
gen.isExported = true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
15
41
|
return {
|
|
16
42
|
path: filePath,
|
|
17
43
|
language: 'typescript',
|
|
18
|
-
functions
|
|
19
|
-
classes
|
|
20
|
-
generics
|
|
21
|
-
imports
|
|
22
|
-
exports
|
|
44
|
+
functions,
|
|
45
|
+
classes,
|
|
46
|
+
generics,
|
|
47
|
+
imports,
|
|
48
|
+
exports,
|
|
49
|
+
routes,
|
|
23
50
|
hash: hashContent(content),
|
|
24
51
|
parsedAt: Date.now(),
|
|
25
52
|
}
|