@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.
Files changed (71) hide show
  1. package/README.md +4 -4
  2. package/package.json +2 -1
  3. package/src/analysis/index.ts +9 -0
  4. package/src/analysis/taint-analysis.ts +419 -0
  5. package/src/analysis/type-flow.ts +247 -0
  6. package/src/cache/incremental-cache.ts +278 -0
  7. package/src/cache/index.ts +1 -0
  8. package/src/contract/contract-generator.ts +31 -3
  9. package/src/contract/contract-reader.ts +1 -0
  10. package/src/contract/lock-compiler.ts +125 -12
  11. package/src/contract/schema.ts +4 -0
  12. package/src/error-handler.ts +2 -1
  13. package/src/graph/cluster-detector.ts +2 -4
  14. package/src/graph/dead-code-detector.ts +303 -117
  15. package/src/graph/graph-builder.ts +21 -161
  16. package/src/graph/impact-analyzer.ts +1 -0
  17. package/src/graph/index.ts +2 -0
  18. package/src/graph/rich-function-index.ts +1080 -0
  19. package/src/graph/symbol-table.ts +252 -0
  20. package/src/hash/hash-store.ts +1 -0
  21. package/src/index.ts +4 -0
  22. package/src/parser/base-extractor.ts +19 -0
  23. package/src/parser/boundary-checker.ts +31 -12
  24. package/src/parser/error-recovery.ts +647 -0
  25. package/src/parser/function-body-extractor.ts +248 -0
  26. package/src/parser/go/go-extractor.ts +249 -676
  27. package/src/parser/index.ts +138 -295
  28. package/src/parser/language-registry.ts +57 -0
  29. package/src/parser/oxc-parser.ts +166 -28
  30. package/src/parser/oxc-resolver.ts +179 -11
  31. package/src/parser/parser-constants.ts +1 -0
  32. package/src/parser/rust/rust-extractor.ts +109 -0
  33. package/src/parser/tree-sitter/parser.ts +400 -66
  34. package/src/parser/tree-sitter/queries.ts +106 -10
  35. package/src/parser/types.ts +20 -1
  36. package/src/search/bm25.ts +21 -8
  37. package/src/search/direct-search.ts +472 -0
  38. package/src/search/embedding-provider.ts +249 -0
  39. package/src/search/index.ts +12 -0
  40. package/src/search/semantic-search.ts +435 -0
  41. package/src/security/index.ts +1 -0
  42. package/src/security/scanner.ts +342 -0
  43. package/src/utils/artifact-transaction.ts +1 -0
  44. package/src/utils/atomic-write.ts +1 -0
  45. package/src/utils/errors.ts +89 -4
  46. package/src/utils/fs.ts +150 -65
  47. package/src/utils/json.ts +1 -0
  48. package/src/utils/language-registry.ts +96 -5
  49. package/src/utils/minimatch.ts +49 -6
  50. package/src/utils/path.ts +26 -0
  51. package/tests/dead-code.test.ts +3 -2
  52. package/tests/direct-search.test.ts +435 -0
  53. package/tests/error-recovery.test.ts +143 -0
  54. package/tests/fixtures/simple-api/src/index.ts +1 -1
  55. package/tests/go-parser.test.ts +19 -335
  56. package/tests/js-parser.test.ts +18 -1089
  57. package/tests/language-registry-all.test.ts +276 -0
  58. package/tests/language-registry.test.ts +6 -4
  59. package/tests/parse-diagnostics.test.ts +9 -96
  60. package/tests/parser.test.ts +42 -771
  61. package/tests/polyglot-parser.test.ts +117 -0
  62. package/tests/rich-function-index.test.ts +703 -0
  63. package/tests/tree-sitter-parser.test.ts +108 -80
  64. package/tests/ts-parser.test.ts +8 -8
  65. package/tests/verification.test.ts +175 -0
  66. package/src/parser/base-parser.ts +0 -16
  67. package/src/parser/go/go-parser.ts +0 -43
  68. package/src/parser/javascript/js-extractor.ts +0 -278
  69. package/src/parser/javascript/js-parser.ts +0 -101
  70. package/src/parser/typescript/ts-extractor.ts +0 -447
  71. package/src/parser/typescript/ts-parser.ts +0 -36
@@ -1,5 +1,6 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1
2
  import path from 'node:path';
2
- import { BaseParser } from './base-parser.js';
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}`.toLowerCase();
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: 'any',
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
- // OxcParser
352
+ // TypescriptExtractor (OXC-based)
278
353
  // ---------------------------------------------------------------------------
279
- export class OxcParser extends BaseParser {
280
- public async parse(filePath: string, content: string): Promise<ParsedFile> {
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
- let ast: any;
285
- try {
286
- const { parseSync } = await import('oxc-parser');
287
- const result = parseSync(filePath, content, {
288
- sourceType: 'module',
289
- lang: isTS ? 'ts' : 'js',
290
- });
291
- ast = result.program;
292
- } catch {
293
- // Return empty file on parse error — never crash the pipeline
294
- return this.emptyFile(filePath, content, isTS);
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: 'void',
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
- if (member.type === 'MethodDefinition') {
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: 'any',
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
- // Re-export specifiers: export { foo, bar }
701
+ const source = node.source?.value;
702
+
602
703
  for (const spec of node.specifiers ?? []) {
603
- if (spec.exported?.name) {
604
- exports.push({ name: spec.exported.name, type: 'variable', file: normalizedFilePath });
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 outside projectRoot (node_modules, monorepo peer) → return ''
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
- * 3. On any error → return '' (unresolved, no false edges)
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 (!resolved.startsWith(this.normalizedRoot + '/') && resolved !== this.normalizedRoot) {
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 (resolved.startsWith(this.normalizedRoot + '/') || resolved === this.normalizedRoot) {
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
- return Promise.all(files.map(async file => ({
113
- ...file,
114
- imports: await Promise.all(file.imports.map(async imp => ({
115
- ...imp,
116
- resolvedPath: await this.resolve(imp.source, file.path),
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 {