@getmikk/core 2.0.13 → 2.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/package.json +2 -1
- package/src/analysis/index.ts +9 -0
- package/src/analysis/taint-analysis.ts +419 -0
- package/src/analysis/type-flow.ts +247 -0
- package/src/cache/incremental-cache.ts +278 -0
- package/src/cache/index.ts +1 -0
- package/src/contract/contract-generator.ts +31 -3
- package/src/contract/contract-reader.ts +1 -0
- package/src/contract/lock-compiler.ts +125 -12
- package/src/contract/schema.ts +4 -0
- package/src/error-handler.ts +2 -1
- package/src/graph/cluster-detector.ts +2 -4
- package/src/graph/dead-code-detector.ts +303 -117
- package/src/graph/graph-builder.ts +21 -161
- package/src/graph/impact-analyzer.ts +1 -0
- package/src/graph/index.ts +2 -0
- package/src/graph/rich-function-index.ts +1080 -0
- package/src/graph/symbol-table.ts +252 -0
- package/src/hash/hash-store.ts +1 -0
- package/src/index.ts +4 -0
- package/src/parser/base-extractor.ts +19 -0
- package/src/parser/boundary-checker.ts +31 -12
- package/src/parser/error-recovery.ts +647 -0
- package/src/parser/function-body-extractor.ts +248 -0
- package/src/parser/go/go-extractor.ts +249 -676
- package/src/parser/index.ts +138 -295
- package/src/parser/language-registry.ts +57 -0
- package/src/parser/oxc-parser.ts +166 -28
- package/src/parser/oxc-resolver.ts +179 -11
- package/src/parser/parser-constants.ts +1 -0
- package/src/parser/rust/rust-extractor.ts +109 -0
- package/src/parser/tree-sitter/parser.ts +400 -66
- package/src/parser/tree-sitter/queries.ts +106 -10
- package/src/parser/types.ts +20 -1
- package/src/search/bm25.ts +21 -8
- package/src/search/direct-search.ts +472 -0
- package/src/search/embedding-provider.ts +249 -0
- package/src/search/index.ts +12 -0
- package/src/search/semantic-search.ts +435 -0
- package/src/security/index.ts +1 -0
- package/src/security/scanner.ts +342 -0
- package/src/utils/artifact-transaction.ts +1 -0
- package/src/utils/atomic-write.ts +1 -0
- package/src/utils/errors.ts +89 -4
- package/src/utils/fs.ts +150 -65
- package/src/utils/json.ts +1 -0
- package/src/utils/language-registry.ts +96 -5
- package/src/utils/minimatch.ts +49 -6
- package/src/utils/path.ts +26 -0
- package/tests/dead-code.test.ts +3 -2
- package/tests/direct-search.test.ts +435 -0
- package/tests/error-recovery.test.ts +143 -0
- package/tests/fixtures/simple-api/src/index.ts +1 -1
- package/tests/go-parser.test.ts +19 -335
- package/tests/js-parser.test.ts +18 -1089
- package/tests/language-registry-all.test.ts +276 -0
- package/tests/language-registry.test.ts +6 -4
- package/tests/parse-diagnostics.test.ts +9 -96
- package/tests/parser.test.ts +42 -771
- package/tests/polyglot-parser.test.ts +117 -0
- package/tests/rich-function-index.test.ts +703 -0
- package/tests/tree-sitter-parser.test.ts +108 -80
- package/tests/ts-parser.test.ts +8 -8
- package/tests/verification.test.ts +175 -0
- package/src/parser/base-parser.ts +0 -16
- package/src/parser/go/go-parser.ts +0 -43
- package/src/parser/javascript/js-extractor.ts +0 -278
- package/src/parser/javascript/js-parser.ts +0 -101
- package/src/parser/typescript/ts-extractor.ts +0 -447
- package/src/parser/typescript/ts-parser.ts +0 -36
package/src/parser/oxc-parser.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
2
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
3
|
+
import { BaseExtractor } from './base-extractor.js';
|
|
3
4
|
import { OxcResolver } from './oxc-resolver.js';
|
|
4
5
|
import { hashContent } from '../hash/file-hasher.js';
|
|
5
6
|
import type {
|
|
@@ -12,8 +13,10 @@ import type {
|
|
|
12
13
|
ParsedParam,
|
|
13
14
|
CallExpression,
|
|
14
15
|
ParsedGeneric,
|
|
15
|
-
ParsedRoute
|
|
16
|
+
ParsedRoute,
|
|
17
|
+
ReExport
|
|
16
18
|
} from './types.js';
|
|
19
|
+
import { LanguageRegistry } from './language-registry.js';
|
|
17
20
|
|
|
18
21
|
// ---------------------------------------------------------------------------
|
|
19
22
|
// LineIndex — O(log n) byte-offset → 1-based line number
|
|
@@ -55,13 +58,13 @@ class LineIndex {
|
|
|
55
58
|
// ---------------------------------------------------------------------------
|
|
56
59
|
function makeAllocator(filePath: string): (prefix: string, name: string) => string {
|
|
57
60
|
const counter = new Map<string, number>();
|
|
58
|
-
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
61
|
+
const normalizedPath = filePath.replace(/\\/g, '/').toLowerCase();
|
|
59
62
|
return (prefix: string, name: string): string => {
|
|
60
63
|
const key = `${prefix}:${name}`;
|
|
61
64
|
const count = (counter.get(key) ?? 0) + 1;
|
|
62
65
|
counter.set(key, count);
|
|
63
66
|
const suffix = count === 1 ? '' : `#${count}`;
|
|
64
|
-
return `${prefix}:${normalizedPath}:${name}${suffix}
|
|
67
|
+
return `${prefix}:${normalizedPath}:${name}${suffix}`;
|
|
65
68
|
};
|
|
66
69
|
}
|
|
67
70
|
|
|
@@ -102,6 +105,23 @@ function resolvePropertyName(node: any): string | null {
|
|
|
102
105
|
return null;
|
|
103
106
|
}
|
|
104
107
|
|
|
108
|
+
function extractDecorators(node: any): string[] {
|
|
109
|
+
if (!node?.decorators?.length) return [];
|
|
110
|
+
return node.decorators.map((dec: any) => {
|
|
111
|
+
if (dec.expression?.type === 'Identifier') return dec.expression.name;
|
|
112
|
+
if (dec.expression?.type === 'CallExpression') {
|
|
113
|
+
const callee = dec.expression.callee;
|
|
114
|
+
if (callee?.type === 'Identifier') return callee.name;
|
|
115
|
+
if (callee?.type === 'MemberExpression') {
|
|
116
|
+
const obj = callee.object?.name ?? '';
|
|
117
|
+
const prop = callee.property?.name ?? '';
|
|
118
|
+
return obj ? `${obj}.${prop}` : prop;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return dec.expression?.name ?? dec.name ?? 'decorator';
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
105
125
|
function resolveObjectName(node: any): string | null {
|
|
106
126
|
if (!node) return null;
|
|
107
127
|
if (node.type === 'Identifier') return node.name;
|
|
@@ -249,6 +269,52 @@ function extractCalls(node: any, lineIndex: LineIndex): CallExpression[] {
|
|
|
249
269
|
// ---------------------------------------------------------------------------
|
|
250
270
|
// Parameter extraction
|
|
251
271
|
// ---------------------------------------------------------------------------
|
|
272
|
+
function extractTypeAnnotation(typeNode: any): string {
|
|
273
|
+
if (!typeNode) return 'any';
|
|
274
|
+
switch (typeNode.type) {
|
|
275
|
+
case 'TSStringKeyword': return 'string';
|
|
276
|
+
case 'TSNumberKeyword': return 'number';
|
|
277
|
+
case 'TSBooleanKeyword': return 'boolean';
|
|
278
|
+
case 'TSVoidKeyword': return 'void';
|
|
279
|
+
case 'TSNullKeyword': return 'null';
|
|
280
|
+
case 'TSUndefinedKeyword': return 'undefined';
|
|
281
|
+
case 'TSNeverKeyword': return 'never';
|
|
282
|
+
case 'TSAnyKeyword': return 'any';
|
|
283
|
+
case 'TSUnknownKeyword': return 'unknown';
|
|
284
|
+
case 'TSObjectKeyword': return 'object';
|
|
285
|
+
case 'TSSymbolKeyword': return 'symbol';
|
|
286
|
+
case 'TSBigIntKeyword': return 'bigint';
|
|
287
|
+
case 'TSTypeReference':
|
|
288
|
+
return typeNode.typeName?.name ?? 'unknown';
|
|
289
|
+
case 'TSArrayType':
|
|
290
|
+
return `${extractTypeAnnotation(typeNode.elementType)}[]`;
|
|
291
|
+
case 'TSTupleType':
|
|
292
|
+
return 'tuple';
|
|
293
|
+
case 'TSUnionType':
|
|
294
|
+
return (typeNode.types ?? []).map((t: any) => extractTypeAnnotation(t)).join(' | ');
|
|
295
|
+
case 'TSIntersectionType':
|
|
296
|
+
return (typeNode.types ?? []).map((t: any) => extractTypeAnnotation(t)).join(' & ');
|
|
297
|
+
case 'TSFunctionType':
|
|
298
|
+
return 'Function';
|
|
299
|
+
case 'TSConstructorType':
|
|
300
|
+
return 'new (...args: any[]) => any';
|
|
301
|
+
case 'TSParenthesizedType':
|
|
302
|
+
return extractTypeAnnotation(typeNode.typeAnnotation);
|
|
303
|
+
case 'TSConditionalType':
|
|
304
|
+
return 'conditional';
|
|
305
|
+
case 'TSMappedType':
|
|
306
|
+
return 'mapped';
|
|
307
|
+
case 'TSIndexedAccessType':
|
|
308
|
+
return 'indexed';
|
|
309
|
+
case 'TSLiteralType':
|
|
310
|
+
return String(typeNode.literal?.value ?? typeNode.literal?.raw ?? 'literal');
|
|
311
|
+
case 'Identifier':
|
|
312
|
+
return typeNode.name ?? 'any';
|
|
313
|
+
default:
|
|
314
|
+
return 'any';
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
252
318
|
function extractParams(params: any[]): ParsedParam[] {
|
|
253
319
|
return params.map(p => {
|
|
254
320
|
const normalized = normalizeParamNode(p);
|
|
@@ -256,15 +322,24 @@ function extractParams(params: any[]): ParsedParam[] {
|
|
|
256
322
|
const name = describeParamPattern(pattern);
|
|
257
323
|
const optional = !!normalized?.optional || pattern?.type === 'AssignmentPattern' || pattern?.type === 'RestElement';
|
|
258
324
|
const hasDefault = pattern?.type === 'AssignmentPattern' || normalized?.defaultValue != null || normalized?.initializer != null;
|
|
325
|
+
const typeAnnotation = normalized?.typeAnnotation ?? pattern?.typeAnnotation;
|
|
259
326
|
return {
|
|
260
327
|
name,
|
|
261
|
-
type:
|
|
328
|
+
type: extractTypeAnnotation(typeAnnotation),
|
|
262
329
|
optional,
|
|
263
330
|
defaultValue: hasDefault ? 'default' : undefined,
|
|
264
331
|
};
|
|
265
332
|
});
|
|
266
333
|
}
|
|
267
334
|
|
|
335
|
+
function extractReturnType(returnType: any): string {
|
|
336
|
+
if (!returnType) return 'void';
|
|
337
|
+
if (returnType.typeAnnotation) {
|
|
338
|
+
return extractTypeAnnotation(returnType.typeAnnotation);
|
|
339
|
+
}
|
|
340
|
+
return extractTypeAnnotation(returnType);
|
|
341
|
+
}
|
|
342
|
+
|
|
268
343
|
// ---------------------------------------------------------------------------
|
|
269
344
|
// Span helper
|
|
270
345
|
// ---------------------------------------------------------------------------
|
|
@@ -274,25 +349,28 @@ function getSpan(node: any): { start: number; end: number } {
|
|
|
274
349
|
}
|
|
275
350
|
|
|
276
351
|
// ---------------------------------------------------------------------------
|
|
277
|
-
//
|
|
352
|
+
// TypescriptExtractor (OXC-based)
|
|
278
353
|
// ---------------------------------------------------------------------------
|
|
279
|
-
export class
|
|
280
|
-
public async
|
|
354
|
+
export class TypescriptExtractor extends BaseExtractor {
|
|
355
|
+
public async extract(filePath: string, content: string): Promise<ParsedFile> {
|
|
281
356
|
const ext = path.extname(filePath).toLowerCase();
|
|
282
357
|
const isTS = ['.ts', '.tsx', '.mts', '.cts'].includes(ext);
|
|
283
358
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
359
|
+
let ast: any;
|
|
360
|
+
try {
|
|
361
|
+
const { parseSync } = await import('oxc-parser');
|
|
362
|
+
const lang = ext === '.jsx' ? 'jsx' : ext === '.tsx' ? 'tsx' : isTS ? 'ts' : 'js';
|
|
363
|
+
const result = parseSync(filePath, content, {
|
|
364
|
+
sourceType: 'module',
|
|
365
|
+
lang: lang,
|
|
366
|
+
});
|
|
367
|
+
ast = result.program;
|
|
368
|
+
} catch (err) {
|
|
369
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
370
|
+
const preview = content.substring(0, 100).replace(/\n/g, ' ');
|
|
371
|
+
console.warn(`[mikk] Parse error in ${filePath}: ${error} (content: "${preview}...")`);
|
|
372
|
+
return this.emptyFile(filePath, content, isTS);
|
|
373
|
+
}
|
|
296
374
|
|
|
297
375
|
const lineIndex = new LineIndex(content);
|
|
298
376
|
const allocateId = makeAllocator(filePath);
|
|
@@ -304,6 +382,7 @@ export class OxcParser extends BaseParser {
|
|
|
304
382
|
const generics: ParsedGeneric[] = [];
|
|
305
383
|
const imports: ParsedImport[] = [];
|
|
306
384
|
const exports: ParsedExport[] = [];
|
|
385
|
+
const reexports: ReExport[] = [];
|
|
307
386
|
const moduleCalls: CallExpression[] = [];
|
|
308
387
|
const routes: ParsedRoute[] = [];
|
|
309
388
|
|
|
@@ -316,18 +395,31 @@ export class OxcParser extends BaseParser {
|
|
|
316
395
|
case 'ImportDeclaration': {
|
|
317
396
|
if (node.importKind === 'type') break;
|
|
318
397
|
const names: string[] = [];
|
|
398
|
+
const specifiers: Array<{ imported: string; local: string }> = [];
|
|
319
399
|
let isDefault = false;
|
|
320
400
|
for (const spec of node.specifiers ?? []) {
|
|
321
401
|
if (spec.importKind === 'type') continue;
|
|
322
402
|
if (spec.type === 'ImportDefaultSpecifier') {
|
|
323
403
|
isDefault = true;
|
|
404
|
+
if (spec.local?.name) names.push(spec.local.name);
|
|
405
|
+
} else if (spec.type === 'ImportSpecifier' || spec.type === 'ImportNamedSpecifier') {
|
|
406
|
+
const imported = spec.imported?.name ?? spec.local?.name ?? '';
|
|
407
|
+
const local = spec.local?.name ?? '';
|
|
408
|
+
if (local) {
|
|
409
|
+
names.push(local);
|
|
410
|
+
if (imported !== local) {
|
|
411
|
+
specifiers.push({ imported, local });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
} else if (spec.type === 'ImportNamespaceSpecifier') {
|
|
415
|
+
if (spec.local?.name) names.push(spec.local.name);
|
|
324
416
|
}
|
|
325
|
-
if (spec.local?.name) names.push(spec.local.name);
|
|
326
417
|
}
|
|
327
418
|
imports.push({
|
|
328
419
|
source: node.source.value,
|
|
329
420
|
resolvedPath: '',
|
|
330
421
|
names,
|
|
422
|
+
specifiers: specifiers.length > 0 ? specifiers : undefined,
|
|
331
423
|
isDefault,
|
|
332
424
|
isDynamic: false,
|
|
333
425
|
});
|
|
@@ -355,6 +447,7 @@ export class OxcParser extends BaseParser {
|
|
|
355
447
|
const name = node.id.name;
|
|
356
448
|
const span = getSpan(node);
|
|
357
449
|
const exported = isDirectlyExported(parent);
|
|
450
|
+
const decorators = extractDecorators(node);
|
|
358
451
|
functions.push({
|
|
359
452
|
id: allocateId('fn', name),
|
|
360
453
|
name,
|
|
@@ -362,7 +455,7 @@ export class OxcParser extends BaseParser {
|
|
|
362
455
|
startLine: lineIndex.getLine(span.start),
|
|
363
456
|
endLine: lineIndex.getLine(span.end),
|
|
364
457
|
params: extractParams(node.params?.items ?? node.params ?? []),
|
|
365
|
-
returnType:
|
|
458
|
+
returnType: extractReturnType(node.returnType ?? node.signature?.returnType),
|
|
366
459
|
isExported: exported,
|
|
367
460
|
isAsync: !!node.async,
|
|
368
461
|
calls: extractCalls(node.body ?? node, lineIndex),
|
|
@@ -371,6 +464,7 @@ export class OxcParser extends BaseParser {
|
|
|
371
464
|
edgeCasesHandled: [],
|
|
372
465
|
errorHandling: [],
|
|
373
466
|
detailedLines: [],
|
|
467
|
+
decorators: decorators.length > 0 ? decorators : undefined,
|
|
374
468
|
});
|
|
375
469
|
if (exported) exports.push({ name, type: 'function', file: normalizedFilePath });
|
|
376
470
|
break;
|
|
@@ -382,6 +476,7 @@ export class OxcParser extends BaseParser {
|
|
|
382
476
|
const name = node.id.name;
|
|
383
477
|
const span = getSpan(node);
|
|
384
478
|
const exported = isDirectlyExported(parent);
|
|
479
|
+
const decorators = extractDecorators(node);
|
|
385
480
|
const methods: ParsedFunction[] = [];
|
|
386
481
|
const properties: ParsedVariable[] = [];
|
|
387
482
|
|
|
@@ -394,7 +489,9 @@ export class OxcParser extends BaseParser {
|
|
|
394
489
|
null;
|
|
395
490
|
if (!mName) continue;
|
|
396
491
|
|
|
397
|
-
|
|
492
|
+
const memberDecorators = extractDecorators(member);
|
|
493
|
+
const isMethod = member.type === 'MethodDefinition';
|
|
494
|
+
if (isMethod) {
|
|
398
495
|
const value = member.value;
|
|
399
496
|
const mSpan = getSpan(member);
|
|
400
497
|
methods.push({
|
|
@@ -404,7 +501,7 @@ export class OxcParser extends BaseParser {
|
|
|
404
501
|
startLine: lineIndex.getLine(mSpan.start),
|
|
405
502
|
endLine: lineIndex.getLine(mSpan.end),
|
|
406
503
|
params: extractParams(value?.params?.items ?? value?.params ?? []),
|
|
407
|
-
returnType:
|
|
504
|
+
returnType: extractReturnType(value?.returnType ?? value?.signature?.returnType),
|
|
408
505
|
isExported: exported,
|
|
409
506
|
isAsync: !!value?.async,
|
|
410
507
|
calls: extractCalls(value?.body ?? value ?? {}, lineIndex),
|
|
@@ -413,6 +510,7 @@ export class OxcParser extends BaseParser {
|
|
|
413
510
|
edgeCasesHandled: [],
|
|
414
511
|
errorHandling: [],
|
|
415
512
|
detailedLines: [],
|
|
513
|
+
decorators: memberDecorators.length > 0 ? memberDecorators : undefined,
|
|
416
514
|
});
|
|
417
515
|
} else {
|
|
418
516
|
// PropertyDefinition
|
|
@@ -425,6 +523,7 @@ export class OxcParser extends BaseParser {
|
|
|
425
523
|
line: lineIndex.getLine(pSpan.start),
|
|
426
524
|
isExported: false,
|
|
427
525
|
isStatic: !!member.static,
|
|
526
|
+
decorators: memberDecorators.length > 0 ? memberDecorators : undefined,
|
|
428
527
|
};
|
|
429
528
|
properties.push(propertyNode);
|
|
430
529
|
variables.push(propertyNode);
|
|
@@ -444,6 +543,7 @@ export class OxcParser extends BaseParser {
|
|
|
444
543
|
isExported: exported,
|
|
445
544
|
hash: hashContent(JSON.stringify(node.body ?? {})),
|
|
446
545
|
purpose: '',
|
|
546
|
+
decorators: decorators.length > 0 ? decorators : undefined,
|
|
447
547
|
});
|
|
448
548
|
if (exported) exports.push({ name, type: 'class', file: normalizedFilePath });
|
|
449
549
|
break;
|
|
@@ -598,13 +698,21 @@ export class OxcParser extends BaseParser {
|
|
|
598
698
|
|
|
599
699
|
// ── Named Exports ──────────────────────────────────────────
|
|
600
700
|
case 'ExportNamedDeclaration': {
|
|
601
|
-
|
|
701
|
+
const source = node.source?.value;
|
|
702
|
+
|
|
602
703
|
for (const spec of node.specifiers ?? []) {
|
|
603
|
-
|
|
604
|
-
|
|
704
|
+
const exportedName = spec.exported?.name ?? spec.local?.name;
|
|
705
|
+
if (!exportedName) continue;
|
|
706
|
+
|
|
707
|
+
if (source) {
|
|
708
|
+
reexports.push({
|
|
709
|
+
name: exportedName,
|
|
710
|
+
source: source,
|
|
711
|
+
});
|
|
712
|
+
} else {
|
|
713
|
+
exports.push({ name: exportedName, type: 'variable', file: normalizedFilePath });
|
|
605
714
|
}
|
|
606
715
|
}
|
|
607
|
-
// Declaration is handled by the declaration's own case with parent context
|
|
608
716
|
break;
|
|
609
717
|
}
|
|
610
718
|
|
|
@@ -674,6 +782,7 @@ export class OxcParser extends BaseParser {
|
|
|
674
782
|
generics,
|
|
675
783
|
imports,
|
|
676
784
|
exports,
|
|
785
|
+
reexports,
|
|
677
786
|
routes,
|
|
678
787
|
calls: moduleCalls,
|
|
679
788
|
hash: hashContent(content),
|
|
@@ -707,3 +816,32 @@ export class OxcParser extends BaseParser {
|
|
|
707
816
|
};
|
|
708
817
|
}
|
|
709
818
|
}
|
|
819
|
+
|
|
820
|
+
// Register in the global registry
|
|
821
|
+
LanguageRegistry.getInstance().register({
|
|
822
|
+
name: 'typescript',
|
|
823
|
+
extensions: ['.ts', '.tsx', '.mts', '.cts'],
|
|
824
|
+
treeSitterGrammar: '',
|
|
825
|
+
extractor: new TypescriptExtractor(),
|
|
826
|
+
semanticFeatures: {
|
|
827
|
+
hasTypeSystem: true,
|
|
828
|
+
hasGenerics: true,
|
|
829
|
+
hasMacros: false,
|
|
830
|
+
hasAnnotations: false,
|
|
831
|
+
hasPatternMatching: false
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
LanguageRegistry.getInstance().register({
|
|
836
|
+
name: 'javascript',
|
|
837
|
+
extensions: ['.js', '.jsx', '.mjs', '.cjs'],
|
|
838
|
+
treeSitterGrammar: '',
|
|
839
|
+
extractor: new TypescriptExtractor(), // OXC handles both
|
|
840
|
+
semanticFeatures: {
|
|
841
|
+
hasTypeSystem: false,
|
|
842
|
+
hasGenerics: false,
|
|
843
|
+
hasMacros: false,
|
|
844
|
+
hasAnnotations: false,
|
|
845
|
+
hasPatternMatching: false
|
|
846
|
+
}
|
|
847
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
2
|
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import fs from 'node:fs';
|
|
@@ -9,9 +10,10 @@ import type { ParsedFile } from './types.js';
|
|
|
9
10
|
*
|
|
10
11
|
* Resolution strategy:
|
|
11
12
|
* 1. If the resolved path is inside projectRoot → return project-relative posix path
|
|
12
|
-
* 2. If the resolved path is
|
|
13
|
+
* 2. If the resolved path is in a monorepo workspace → return full path
|
|
14
|
+
* 3. If the resolved path is outside projectRoot (node_modules) → return ''
|
|
13
15
|
* (external deps produce no graph edges; they're not in our file set)
|
|
14
|
-
*
|
|
16
|
+
* 4. On any error → return '' (unresolved, no false edges)
|
|
15
17
|
*
|
|
16
18
|
* All returned paths use forward slashes and are ABSOLUTE, matching what
|
|
17
19
|
* parseFiles passes to parse(). graph-builder only creates import edges when
|
|
@@ -21,12 +23,140 @@ import type { ParsedFile } from './types.js';
|
|
|
21
23
|
export class OxcResolver {
|
|
22
24
|
private resolver: any;
|
|
23
25
|
private readonly normalizedRoot: string;
|
|
26
|
+
private workspaceRoots: string[] | null = null;
|
|
24
27
|
|
|
25
28
|
|
|
26
29
|
constructor(private readonly projectRoot: string) {
|
|
27
30
|
this.normalizedRoot = path.resolve(projectRoot).replace(/\\/g, '/');
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Detect all workspace roots in a monorepo setup.
|
|
35
|
+
* Supports: npm workspaces, pnpm workspaces, Turborepo
|
|
36
|
+
*/
|
|
37
|
+
private detectWorkspaceRoots(): string[] {
|
|
38
|
+
if (this.workspaceRoots !== null) {
|
|
39
|
+
return this.workspaceRoots;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const roots: Set<string> = new Set([this.normalizedRoot]);
|
|
43
|
+
const projectDir = path.dirname(this.normalizedRoot);
|
|
44
|
+
|
|
45
|
+
// Check for pnpm-workspace.yaml (pnpm workspaces)
|
|
46
|
+
const pnpmWorkspace = path.join(projectDir, 'pnpm-workspace.yaml');
|
|
47
|
+
if (fs.existsSync(pnpmWorkspace)) {
|
|
48
|
+
try {
|
|
49
|
+
const content = fs.readFileSync(pnpmWorkspace, 'utf-8');
|
|
50
|
+
const packageMatch = content.match(/packages:\s*\n([\s\S]*?)(?:\n\n|\n[^ ]|$)/);
|
|
51
|
+
if (packageMatch) {
|
|
52
|
+
const packagesPattern = packageMatch[1];
|
|
53
|
+
for (const line of packagesPattern.split('\n')) {
|
|
54
|
+
const trimmed = line.trim().replace(/^-\s*/, '').replace(/^'\s*/, '').replace(/^\s*/, '');
|
|
55
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
56
|
+
const wsPath = path.resolve(projectDir, trimmed.replace(/\/\*$/, '')).replace(/\\/g, '/');
|
|
57
|
+
roots.add(wsPath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch { /* ignore */ }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check for package.json with workspaces field (npm/yarn workspaces)
|
|
65
|
+
const pkgJson = path.join(projectDir, 'package.json');
|
|
66
|
+
if (fs.existsSync(pkgJson)) {
|
|
67
|
+
try {
|
|
68
|
+
const content = fs.readFileSync(pkgJson, 'utf-8');
|
|
69
|
+
const pkg = JSON.parse(content);
|
|
70
|
+
const workspaces = pkg.workspaces;
|
|
71
|
+
if (workspaces) {
|
|
72
|
+
const patterns = Array.isArray(workspaces) ? workspaces : (workspaces.packages || []);
|
|
73
|
+
for (const pattern of patterns) {
|
|
74
|
+
if (typeof pattern === 'string' && !pattern.includes('*')) {
|
|
75
|
+
const wsPath = path.resolve(projectDir, pattern).replace(/\\/g, '/');
|
|
76
|
+
roots.add(wsPath);
|
|
77
|
+
} else if (typeof pattern === 'string') {
|
|
78
|
+
// Glob pattern - find matching directories
|
|
79
|
+
const basePattern = pattern.replace(/\/\*$/, '');
|
|
80
|
+
const basePath = path.resolve(projectDir, basePattern);
|
|
81
|
+
if (fs.existsSync(basePath)) {
|
|
82
|
+
try {
|
|
83
|
+
const entries = fs.readdirSync(basePath);
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const entryPath = path.join(basePath, entry);
|
|
86
|
+
const stat = fs.statSync(entryPath);
|
|
87
|
+
if (stat.isDirectory()) {
|
|
88
|
+
const pkgJsonPath = path.join(entryPath, 'package.json');
|
|
89
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
90
|
+
roots.add(entryPath.replace(/\\/g, '/'));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch { /* ignore */ }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch { /* ignore */ }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check for turbo.json (Turborepo)
|
|
103
|
+
const turboJson = path.join(projectDir, 'turbo.json');
|
|
104
|
+
if (fs.existsSync(turboJson)) {
|
|
105
|
+
try {
|
|
106
|
+
const content = fs.readFileSync(turboJson, 'utf-8');
|
|
107
|
+
const turbo = JSON.parse(content);
|
|
108
|
+
if (turbo.pipeline) {
|
|
109
|
+
for (const task of Object.keys(turbo.pipeline)) {
|
|
110
|
+
const pipeline = turbo.pipeline[task];
|
|
111
|
+
if (pipeline?.dependsOn) {
|
|
112
|
+
for (const dep of pipeline.dependsOn) {
|
|
113
|
+
if (typeof dep === 'string' && dep.startsWith('^')) {
|
|
114
|
+
// Workspace dependency
|
|
115
|
+
const wsName = dep.slice(1).replace(/^.*:/, '');
|
|
116
|
+
roots.add(path.resolve(projectDir, 'apps', wsName).replace(/\\/g, '/'));
|
|
117
|
+
roots.add(path.resolve(projectDir, 'packages', wsName).replace(/\\/g, '/'));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch { /* ignore */ }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.workspaceRoots = [...roots];
|
|
127
|
+
return this.workspaceRoots;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if a resolved path is within an accepted workspace root.
|
|
132
|
+
*/
|
|
133
|
+
private isInAcceptedWorkspace(resolvedPath: string): boolean {
|
|
134
|
+
if (resolvedPath.startsWith(this.normalizedRoot + '/') || resolvedPath === this.normalizedRoot) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check workspace roots for monorepo packages
|
|
139
|
+
const workspaces = this.detectWorkspaceRoots();
|
|
140
|
+
for (const wsRoot of workspaces) {
|
|
141
|
+
if (resolvedPath.startsWith(wsRoot + '/')) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Accept node_modules within workspace packages (but not external)
|
|
147
|
+
const nodeModulesIdx = resolvedPath.indexOf('/node_modules/');
|
|
148
|
+
if (nodeModulesIdx > 0) {
|
|
149
|
+
const prefix = resolvedPath.slice(0, nodeModulesIdx);
|
|
150
|
+
for (const wsRoot of workspaces) {
|
|
151
|
+
if (prefix.startsWith(wsRoot)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
30
160
|
private async ensureResolver() {
|
|
31
161
|
if (this.resolver) return;
|
|
32
162
|
const { ResolverFactory } = await import('oxc-resolver');
|
|
@@ -68,7 +198,7 @@ export class OxcResolver {
|
|
|
68
198
|
|
|
69
199
|
const resolved = result.path.replace(/\\/g, '/');
|
|
70
200
|
|
|
71
|
-
if (!
|
|
201
|
+
if (!this.isInAcceptedWorkspace(resolved)) {
|
|
72
202
|
return '';
|
|
73
203
|
}
|
|
74
204
|
|
|
@@ -97,7 +227,7 @@ export class OxcResolver {
|
|
|
97
227
|
const resolved = path.resolve(baseDir, candidate).replace(/\\/g, '/');
|
|
98
228
|
|
|
99
229
|
if (fs.existsSync(resolved)) {
|
|
100
|
-
if (
|
|
230
|
+
if (this.isInAcceptedWorkspace(resolved)) {
|
|
101
231
|
return resolved;
|
|
102
232
|
}
|
|
103
233
|
}
|
|
@@ -109,13 +239,51 @@ export class OxcResolver {
|
|
|
109
239
|
|
|
110
240
|
/** Resolve all imports for a batch of files in one pass */
|
|
111
241
|
public async resolveBatch(files: ParsedFile[]): Promise<ParsedFile[]> {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
242
|
+
const fileByPath = new Map<string, ParsedFile>();
|
|
243
|
+
for (const file of files) {
|
|
244
|
+
fileByPath.set(file.path, file);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const resolveWithReexports = async (source: string, fromFile: string, visited: Set<string> = new Set()): Promise<string> => {
|
|
248
|
+
if (visited.has(fromFile)) return '';
|
|
249
|
+
visited.add(fromFile);
|
|
250
|
+
|
|
251
|
+
const resolved = await this.resolve(source, fromFile);
|
|
252
|
+
if (!resolved) return '';
|
|
253
|
+
|
|
254
|
+
const targetFile = fileByPath.get(resolved);
|
|
255
|
+
if (!targetFile) return resolved;
|
|
256
|
+
|
|
257
|
+
for (const re of targetFile.reexports ?? []) {
|
|
258
|
+
if (re.sourceResolved) {
|
|
259
|
+
const chain = await resolveWithReexports(re.source, resolved, visited);
|
|
260
|
+
if (chain) return chain;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return resolved;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const resolvedFiles: ParsedFile[] = [];
|
|
268
|
+
for (const file of files) {
|
|
269
|
+
const resolvedImports = await Promise.all(file.imports.map(async imp => {
|
|
270
|
+
const resolvedPath = await resolveWithReexports(imp.source, file.path);
|
|
271
|
+
return { ...imp, resolvedPath };
|
|
272
|
+
}));
|
|
273
|
+
|
|
274
|
+
const resolvedReexports = await Promise.all((file.reexports ?? []).map(async re => {
|
|
275
|
+
const sourceResolved = await this.resolve(re.source, file.path);
|
|
276
|
+
return { ...re, sourceResolved };
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
resolvedFiles.push({
|
|
280
|
+
...file,
|
|
281
|
+
imports: resolvedImports,
|
|
282
|
+
reexports: resolvedReexports,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return resolvedFiles;
|
|
119
287
|
}
|
|
120
288
|
}
|
|
121
289
|
|
|
@@ -72,6 +72,7 @@ export function stripJsonComments(raw: string): string {
|
|
|
72
72
|
* Parse JSON config files while tolerating JSON5 comments.
|
|
73
73
|
* Falls back to the raw content if comment stripping breaks URLs.
|
|
74
74
|
*/
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
76
|
export function parseJsonWithComments<T = any>(raw: string): T {
|
|
76
77
|
const stripped = stripJsonComments(raw)
|
|
77
78
|
try {
|