@getmikk/core 1.6.0 → 1.7.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/lock-compiler.ts +6 -5
- package/src/contract/lock-reader.ts +40 -26
- package/src/contract/schema.ts +2 -7
- package/src/index.ts +1 -1
- package/src/parser/index.ts +20 -1
- package/src/parser/javascript/js-extractor.ts +262 -0
- package/src/parser/javascript/js-parser.ts +92 -0
- package/src/parser/javascript/js-resolver.ts +83 -0
- package/src/parser/types.ts +1 -1
- package/src/parser/typescript/ts-extractor.ts +29 -25
- package/src/parser/typescript/ts-parser.ts +93 -14
- package/tests/contract.test.ts +1 -1
- package/tests/helpers.ts +0 -1
- package/tests/js-parser.test.ts +616 -0
- package/tests/ts-parser.test.ts +93 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
import type { ParsedImport } from '../types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* JavaScriptResolver — resolves JS/JSX/CJS import paths to project-relative files.
|
|
6
|
+
*
|
|
7
|
+
* Handles:
|
|
8
|
+
* - Relative ESM imports: import './utils' → ./utils.js / ./utils/index.js / ...
|
|
9
|
+
* - CommonJS require(): require('./db') → same resolution order
|
|
10
|
+
* - Path aliases from jsconfig.json / tsconfig.json
|
|
11
|
+
* - Mixed TS/JS projects: falls back to .ts/.tsx if no JS file matched
|
|
12
|
+
*
|
|
13
|
+
* Extension probe order: .js → .jsx → .mjs → .cjs → index.js → index.jsx →
|
|
14
|
+
* .ts → .tsx → index.ts → index.tsx
|
|
15
|
+
*/
|
|
16
|
+
export class JavaScriptResolver {
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly projectRoot: string,
|
|
19
|
+
private readonly aliases: Record<string, string[]> = {},
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
resolve(imp: ParsedImport, fromFile: string, allProjectFiles: string[] = []): ParsedImport {
|
|
23
|
+
// External packages (no ./ / alias prefix) — leave unresolved
|
|
24
|
+
if (
|
|
25
|
+
!imp.source.startsWith('.') &&
|
|
26
|
+
!imp.source.startsWith('/') &&
|
|
27
|
+
!this.matchesAlias(imp.source)
|
|
28
|
+
) {
|
|
29
|
+
return { ...imp, resolvedPath: '' }
|
|
30
|
+
}
|
|
31
|
+
return { ...imp, resolvedPath: this.resolvePath(imp.source, fromFile, allProjectFiles) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
resolveAll(imports: ParsedImport[], fromFile: string, allProjectFiles: string[] = []): ParsedImport[] {
|
|
35
|
+
return imports.map(imp => this.resolve(imp, fromFile, allProjectFiles))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private resolvePath(source: string, fromFile: string, allProjectFiles: string[]): string {
|
|
39
|
+
let resolvedSource = source
|
|
40
|
+
|
|
41
|
+
// 1. Alias substitution
|
|
42
|
+
for (const [alias, targets] of Object.entries(this.aliases)) {
|
|
43
|
+
const prefix = alias.replace('/*', '')
|
|
44
|
+
if (source.startsWith(prefix)) {
|
|
45
|
+
resolvedSource = targets[0].replace('/*', '') + source.slice(prefix.length)
|
|
46
|
+
break
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Build absolute-like posix path
|
|
51
|
+
let resolved: string
|
|
52
|
+
if (resolvedSource.startsWith('.')) {
|
|
53
|
+
resolved = path.posix.normalize(path.posix.join(path.dirname(fromFile), resolvedSource))
|
|
54
|
+
} else {
|
|
55
|
+
resolved = resolvedSource
|
|
56
|
+
}
|
|
57
|
+
resolved = resolved.replace(/\\/g, '/')
|
|
58
|
+
|
|
59
|
+
// 3. Already has a concrete extension — return as-is
|
|
60
|
+
const knownExts = ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx']
|
|
61
|
+
if (knownExts.some(e => resolved.endsWith(e))) return resolved
|
|
62
|
+
|
|
63
|
+
// 4. Probe extensions: prefer JS-family first, fall back to TS for mixed projects
|
|
64
|
+
const probeOrder = [
|
|
65
|
+
'.js', '.jsx', '.mjs', '.cjs',
|
|
66
|
+
'/index.js', '/index.jsx', '/index.mjs',
|
|
67
|
+
'.ts', '.tsx',
|
|
68
|
+
'/index.ts', '/index.tsx',
|
|
69
|
+
]
|
|
70
|
+
for (const ext of probeOrder) {
|
|
71
|
+
const candidate = resolved + ext
|
|
72
|
+
if (allProjectFiles.length === 0 || allProjectFiles.includes(candidate)) {
|
|
73
|
+
return candidate
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return resolved + '.js'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private matchesAlias(source: string): boolean {
|
|
81
|
+
return Object.keys(this.aliases).some(a => source.startsWith(a.replace('/*', '')))
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/parser/types.ts
CHANGED
|
@@ -89,7 +89,7 @@ export interface ParsedGeneric {
|
|
|
89
89
|
/** Everything extracted from a single file */
|
|
90
90
|
export interface ParsedFile {
|
|
91
91
|
path: string // "src/auth/verify.ts"
|
|
92
|
-
language: 'typescript' | 'python' | 'go'
|
|
92
|
+
language: 'typescript' | 'javascript' | 'python' | 'go'
|
|
93
93
|
functions: ParsedFunction[]
|
|
94
94
|
classes: ParsedClass[]
|
|
95
95
|
generics: ParsedGeneric[]
|
|
@@ -7,11 +7,11 @@ import { hashContent } from '../../hash/file-hasher.js'
|
|
|
7
7
|
* and extracts functions, classes, imports, exports and call relationships.
|
|
8
8
|
*/
|
|
9
9
|
export class TypeScriptExtractor {
|
|
10
|
-
|
|
10
|
+
protected readonly sourceFile: ts.SourceFile
|
|
11
11
|
|
|
12
12
|
constructor(
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
protected readonly filePath: string,
|
|
14
|
+
protected readonly content: string
|
|
15
15
|
) {
|
|
16
16
|
this.sourceFile = ts.createSourceFile(
|
|
17
17
|
filePath,
|
|
@@ -118,7 +118,7 @@ export class TypeScriptExtractor {
|
|
|
118
118
|
return generics
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
protected isVariableFunction(node: ts.VariableStatement): boolean {
|
|
122
122
|
for (const decl of node.declarationList.declarations) {
|
|
123
123
|
if (decl.initializer && (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))) {
|
|
124
124
|
return true
|
|
@@ -309,14 +309,14 @@ export class TypeScriptExtractor {
|
|
|
309
309
|
return routes
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
// ───
|
|
312
|
+
// ─── Protected Helpers ─────────────────────────────────────
|
|
313
313
|
|
|
314
|
-
|
|
314
|
+
protected parseFunctionDeclaration(node: ts.FunctionDeclaration): ParsedFunction {
|
|
315
315
|
const name = node.name!.text
|
|
316
316
|
const startLine = this.getLineNumber(node.getStart())
|
|
317
317
|
const endLine = this.getLineNumber(node.getEnd())
|
|
318
318
|
const params = this.extractParams(node.parameters)
|
|
319
|
-
const returnType = node.type ? node.type.getText(this.sourceFile) : 'void'
|
|
319
|
+
const returnType = normalizeTypeAnnotation(node.type ? node.type.getText(this.sourceFile) : 'void')
|
|
320
320
|
const isAsync = !!node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)
|
|
321
321
|
const isGenerator = !!node.asteriskToken
|
|
322
322
|
const typeParameters = this.extractTypeParameters(node.typeParameters)
|
|
@@ -344,7 +344,7 @@ export class TypeScriptExtractor {
|
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
|
|
347
|
+
protected parseVariableFunction(
|
|
348
348
|
stmt: ts.VariableStatement,
|
|
349
349
|
decl: ts.VariableDeclaration,
|
|
350
350
|
fn: ts.ArrowFunction | ts.FunctionExpression
|
|
@@ -353,7 +353,7 @@ export class TypeScriptExtractor {
|
|
|
353
353
|
const startLine = this.getLineNumber(stmt.getStart())
|
|
354
354
|
const endLine = this.getLineNumber(stmt.getEnd())
|
|
355
355
|
const params = this.extractParams(fn.parameters)
|
|
356
|
-
const returnType = fn.type ? fn.type.getText(this.sourceFile) : 'void'
|
|
356
|
+
const returnType = normalizeTypeAnnotation(fn.type ? fn.type.getText(this.sourceFile) : 'void')
|
|
357
357
|
const isAsync = !!fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)
|
|
358
358
|
const isGenerator = ts.isFunctionExpression(fn) && !!fn.asteriskToken
|
|
359
359
|
const typeParameters = this.extractTypeParameters(fn.typeParameters)
|
|
@@ -381,7 +381,7 @@ export class TypeScriptExtractor {
|
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
|
|
384
|
+
protected parseClass(node: ts.ClassDeclaration): ParsedClass {
|
|
385
385
|
const name = node.name!.text
|
|
386
386
|
const startLine = this.getLineNumber(node.getStart())
|
|
387
387
|
const endLine = this.getLineNumber(node.getEnd())
|
|
@@ -420,7 +420,7 @@ export class TypeScriptExtractor {
|
|
|
420
420
|
const mStartLine = this.getLineNumber(member.getStart())
|
|
421
421
|
const mEndLine = this.getLineNumber(member.getEnd())
|
|
422
422
|
const params = this.extractParams(member.parameters)
|
|
423
|
-
const returnType = member.type ? member.type.getText(this.sourceFile) : 'void'
|
|
423
|
+
const returnType = normalizeTypeAnnotation(member.type ? member.type.getText(this.sourceFile) : 'void')
|
|
424
424
|
const isAsync = !!member.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)
|
|
425
425
|
const isGenerator = !!member.asteriskToken
|
|
426
426
|
const methodTypeParams = this.extractTypeParameters(member.typeParameters)
|
|
@@ -465,7 +465,7 @@ export class TypeScriptExtractor {
|
|
|
465
465
|
}
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
-
|
|
468
|
+
protected parseImport(node: ts.ImportDeclaration): ParsedImport | null {
|
|
469
469
|
const source = (node.moduleSpecifier as ts.StringLiteral).text
|
|
470
470
|
const names: string[] = []
|
|
471
471
|
let isDefault = false
|
|
@@ -503,7 +503,7 @@ export class TypeScriptExtractor {
|
|
|
503
503
|
}
|
|
504
504
|
|
|
505
505
|
/** Extract function/method call expressions from a node (including new Foo()) */
|
|
506
|
-
|
|
506
|
+
protected extractCalls(node: ts.Node): string[] {
|
|
507
507
|
const calls: string[] = []
|
|
508
508
|
const walkCalls = (n: ts.Node) => {
|
|
509
509
|
if (ts.isCallExpression(n)) {
|
|
@@ -532,7 +532,7 @@ export class TypeScriptExtractor {
|
|
|
532
532
|
|
|
533
533
|
/** Extract the purpose from JSDoc comments or preceding single-line comments.
|
|
534
534
|
* Falls back to deriving a human-readable sentence from the function name. */
|
|
535
|
-
|
|
535
|
+
protected extractPurpose(node: ts.Node): string {
|
|
536
536
|
const fullText = this.sourceFile.getFullText()
|
|
537
537
|
const commentRanges = ts.getLeadingCommentRanges(fullText, node.getFullStart())
|
|
538
538
|
if (commentRanges && commentRanges.length > 0) {
|
|
@@ -563,7 +563,7 @@ export class TypeScriptExtractor {
|
|
|
563
563
|
}
|
|
564
564
|
|
|
565
565
|
/** Get the identifier name from common declaration node types */
|
|
566
|
-
|
|
566
|
+
protected getNodeName(node: ts.Node): string {
|
|
567
567
|
if (ts.isFunctionDeclaration(node) && node.name) return node.name.text
|
|
568
568
|
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
569
569
|
const parent = node.parent
|
|
@@ -580,7 +580,7 @@ export class TypeScriptExtractor {
|
|
|
580
580
|
}
|
|
581
581
|
|
|
582
582
|
/** Extract edge cases handled (if statements returning early) */
|
|
583
|
-
|
|
583
|
+
protected extractEdgeCases(node: ts.Node): string[] {
|
|
584
584
|
const edgeCases: string[] = []
|
|
585
585
|
const walkEdgeCases = (n: ts.Node) => {
|
|
586
586
|
if (ts.isIfStatement(n)) {
|
|
@@ -601,7 +601,7 @@ export class TypeScriptExtractor {
|
|
|
601
601
|
}
|
|
602
602
|
|
|
603
603
|
/** Extract try-catch blocks or explicit throw statements */
|
|
604
|
-
|
|
604
|
+
protected extractErrorHandling(node: ts.Node): { line: number, type: 'try-catch' | 'throw', detail: string }[] {
|
|
605
605
|
const errors: { line: number, type: 'try-catch' | 'throw', detail: string }[] = []
|
|
606
606
|
const walkErrors = (n: ts.Node) => {
|
|
607
607
|
if (ts.isTryStatement(n)) {
|
|
@@ -625,7 +625,7 @@ export class TypeScriptExtractor {
|
|
|
625
625
|
}
|
|
626
626
|
|
|
627
627
|
/** Extract detailed line block breakdowns */
|
|
628
|
-
|
|
628
|
+
protected extractDetailedLines(node: ts.Node): { startLine: number, endLine: number, blockType: string }[] {
|
|
629
629
|
const blocks: { startLine: number, endLine: number, blockType: string }[] = []
|
|
630
630
|
const walkBlocks = (n: ts.Node) => {
|
|
631
631
|
if (ts.isIfStatement(n) || ts.isSwitchStatement(n)) {
|
|
@@ -650,13 +650,13 @@ export class TypeScriptExtractor {
|
|
|
650
650
|
}
|
|
651
651
|
|
|
652
652
|
/** Extract type parameter names from a generic declaration */
|
|
653
|
-
|
|
653
|
+
protected extractTypeParameters(typeParams: ts.NodeArray<ts.TypeParameterDeclaration> | undefined): string[] {
|
|
654
654
|
if (!typeParams || typeParams.length === 0) return []
|
|
655
655
|
return typeParams.map(tp => tp.name.text)
|
|
656
656
|
}
|
|
657
657
|
|
|
658
658
|
/** Extract decorator names from a class declaration */
|
|
659
|
-
|
|
659
|
+
protected extractDecorators(node: ts.ClassDeclaration): string[] {
|
|
660
660
|
const decorators: string[] = []
|
|
661
661
|
const modifiers = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined
|
|
662
662
|
if (modifiers) {
|
|
@@ -674,28 +674,28 @@ export class TypeScriptExtractor {
|
|
|
674
674
|
}
|
|
675
675
|
|
|
676
676
|
/** Extract parameters from a function's parameter list */
|
|
677
|
-
|
|
677
|
+
protected extractParams(params: ts.NodeArray<ts.ParameterDeclaration>): ParsedParam[] {
|
|
678
678
|
return params.map((p) => ({
|
|
679
679
|
name: p.name.getText(this.sourceFile),
|
|
680
|
-
type: p.type ? p.type.getText(this.sourceFile) : 'any',
|
|
680
|
+
type: normalizeTypeAnnotation(p.type ? p.type.getText(this.sourceFile) : 'any'),
|
|
681
681
|
optional: !!p.questionToken || !!p.initializer,
|
|
682
682
|
defaultValue: p.initializer ? p.initializer.getText(this.sourceFile) : undefined,
|
|
683
683
|
}))
|
|
684
684
|
}
|
|
685
685
|
|
|
686
686
|
/** Check if a node has the 'export' modifier */
|
|
687
|
-
|
|
687
|
+
protected hasExportModifier(node: ts.Node): boolean {
|
|
688
688
|
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined
|
|
689
689
|
return !!modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)
|
|
690
690
|
}
|
|
691
691
|
|
|
692
692
|
/** Get 1-indexed line number from a character position */
|
|
693
|
-
|
|
693
|
+
protected getLineNumber(pos: number): number {
|
|
694
694
|
return this.sourceFile.getLineAndCharacterOfPosition(pos).line + 1
|
|
695
695
|
}
|
|
696
696
|
|
|
697
697
|
/** Walk the top-level children of a node (non-recursive — callbacks decide depth) */
|
|
698
|
-
|
|
698
|
+
protected walkNode(node: ts.Node, callback: (node: ts.Node) => void): void {
|
|
699
699
|
ts.forEachChild(node, (child) => {
|
|
700
700
|
callback(child)
|
|
701
701
|
})
|
|
@@ -712,6 +712,10 @@ export class TypeScriptExtractor {
|
|
|
712
712
|
* UserRepository → "User repository"
|
|
713
713
|
* parseFiles → "Parse files"
|
|
714
714
|
*/
|
|
715
|
+
function normalizeTypeAnnotation(type: string): string {
|
|
716
|
+
return type.replace(/\s*\n\s*/g, ' ').replace(/\s{2,}/g, ' ').trim()
|
|
717
|
+
}
|
|
718
|
+
|
|
715
719
|
function derivePurposeFromName(name: string): string {
|
|
716
720
|
if (!name || name === 'constructor') return ''
|
|
717
721
|
// Split on camelCase/PascalCase boundaries and underscores
|
|
@@ -71,30 +71,27 @@ export class TypeScriptParser extends BaseParser {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* Read compilerOptions.paths from
|
|
75
|
-
*
|
|
74
|
+
* Read compilerOptions.paths from tsconfig.json in projectRoot.
|
|
75
|
+
* Recursively follows "extends" chains (e.g. extends ./tsconfig.base.json,
|
|
76
|
+
* extends @tsconfig/node-lts/tsconfig.json) and merges paths.
|
|
77
|
+
*
|
|
78
|
+
* Handles:
|
|
79
|
+
* - extends with relative paths (./tsconfig.base.json)
|
|
80
|
+
* - extends with node_modules packages (@tsconfig/node-lts)
|
|
81
|
+
* - baseUrl prefix so aliases like "@/*" → ["src/*"] resolve correctly
|
|
82
|
+
* - JSON5-style comments (line and block comments)
|
|
76
83
|
*/
|
|
77
84
|
function loadTsConfigPaths(projectRoot: string): Record<string, string[]> {
|
|
78
85
|
const candidates = ['tsconfig.json', 'tsconfig.base.json']
|
|
79
86
|
for (const name of candidates) {
|
|
80
87
|
const tsConfigPath = path.join(projectRoot, name)
|
|
81
88
|
try {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
85
|
-
let tsConfig: any
|
|
86
|
-
try {
|
|
87
|
-
tsConfig = JSON.parse(stripped)
|
|
88
|
-
} catch {
|
|
89
|
-
// Stripping may have broken a URL (e.g. "https://...") — fall back to raw
|
|
90
|
-
tsConfig = JSON.parse(raw)
|
|
91
|
-
}
|
|
92
|
-
const options = tsConfig.compilerOptions ?? {}
|
|
89
|
+
const merged = loadTsConfigWithExtends(tsConfigPath, new Set())
|
|
90
|
+
const options = merged.compilerOptions ?? {}
|
|
93
91
|
const rawPaths: Record<string, string[]> = options.paths ?? {}
|
|
94
92
|
if (Object.keys(rawPaths).length === 0) continue
|
|
95
93
|
|
|
96
94
|
const baseUrl: string = options.baseUrl ?? '.'
|
|
97
|
-
// Prefix each target with baseUrl so relative resolution works
|
|
98
95
|
const resolved: Record<string, string[]> = {}
|
|
99
96
|
for (const [alias, targets] of Object.entries(rawPaths)) {
|
|
100
97
|
resolved[alias] = (targets as string[]).map(t =>
|
|
@@ -106,3 +103,85 @@ function loadTsConfigPaths(projectRoot: string): Record<string, string[]> {
|
|
|
106
103
|
}
|
|
107
104
|
return {}
|
|
108
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Recursively load a tsconfig, following the "extends" chain.
|
|
109
|
+
* Merges compilerOptions from parent → child (child wins on conflict).
|
|
110
|
+
* Prevents infinite loops via a visited set.
|
|
111
|
+
*/
|
|
112
|
+
function loadTsConfigWithExtends(configPath: string, visited: Set<string>): any {
|
|
113
|
+
const resolved = path.resolve(configPath)
|
|
114
|
+
if (visited.has(resolved)) return {}
|
|
115
|
+
visited.add(resolved)
|
|
116
|
+
|
|
117
|
+
let raw: string
|
|
118
|
+
try {
|
|
119
|
+
raw = fs.readFileSync(resolved, 'utf-8')
|
|
120
|
+
} catch {
|
|
121
|
+
return {}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Strip JSON5 comments
|
|
125
|
+
const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
126
|
+
let config: any
|
|
127
|
+
try {
|
|
128
|
+
config = JSON.parse(stripped)
|
|
129
|
+
} catch {
|
|
130
|
+
try { config = JSON.parse(raw) } catch { return {} }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!config.extends) return config
|
|
134
|
+
|
|
135
|
+
// Resolve the parent config path
|
|
136
|
+
const extendsValue = config.extends
|
|
137
|
+
let parentPath: string
|
|
138
|
+
|
|
139
|
+
if (extendsValue.startsWith('.')) {
|
|
140
|
+
// Relative path: ./tsconfig.base.json or ../tsconfig.json
|
|
141
|
+
parentPath = path.resolve(path.dirname(resolved), extendsValue)
|
|
142
|
+
// Add .json if missing
|
|
143
|
+
if (!parentPath.endsWith('.json')) parentPath += '.json'
|
|
144
|
+
} else {
|
|
145
|
+
// Node module: @tsconfig/node-lts or @tsconfig/node-lts/tsconfig.json
|
|
146
|
+
try {
|
|
147
|
+
// Try resolving as a node module from projectRoot
|
|
148
|
+
const projectRoot = path.dirname(resolved)
|
|
149
|
+
const modulePath = path.join(projectRoot, 'node_modules', extendsValue)
|
|
150
|
+
if (fs.existsSync(modulePath + '.json')) {
|
|
151
|
+
parentPath = modulePath + '.json'
|
|
152
|
+
} else if (fs.existsSync(path.join(modulePath, 'tsconfig.json'))) {
|
|
153
|
+
parentPath = path.join(modulePath, 'tsconfig.json')
|
|
154
|
+
} else if (fs.existsSync(modulePath)) {
|
|
155
|
+
parentPath = modulePath
|
|
156
|
+
} else {
|
|
157
|
+
// Can't resolve — skip extends
|
|
158
|
+
delete config.extends
|
|
159
|
+
return config
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
delete config.extends
|
|
163
|
+
return config
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Load parent recursively
|
|
168
|
+
const parent = loadTsConfigWithExtends(parentPath, visited)
|
|
169
|
+
|
|
170
|
+
// Merge: parent compilerOptions → child compilerOptions (child wins)
|
|
171
|
+
const merged = { ...config }
|
|
172
|
+
delete merged.extends
|
|
173
|
+
merged.compilerOptions = {
|
|
174
|
+
...(parent.compilerOptions ?? {}),
|
|
175
|
+
...(config.compilerOptions ?? {}),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Merge paths specifically (child paths override parent paths for same alias)
|
|
179
|
+
if (parent.compilerOptions?.paths || config.compilerOptions?.paths) {
|
|
180
|
+
merged.compilerOptions.paths = {
|
|
181
|
+
...(parent.compilerOptions?.paths ?? {}),
|
|
182
|
+
...(config.compilerOptions?.paths ?? {}),
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return merged
|
|
187
|
+
}
|
package/tests/contract.test.ts
CHANGED
|
@@ -76,7 +76,7 @@ describe('LockCompiler', () => {
|
|
|
76
76
|
const graph = new GraphBuilder().build(files)
|
|
77
77
|
const lock = compiler.compile(graph, contract, files)
|
|
78
78
|
|
|
79
|
-
expect(lock.version).toBe('1.
|
|
79
|
+
expect(lock.version).toBe('1.7.0')
|
|
80
80
|
expect(lock.syncState.status).toBe('clean')
|
|
81
81
|
expect(Object.keys(lock.functions).length).toBeGreaterThan(0)
|
|
82
82
|
expect(Object.keys(lock.modules).length).toBeGreaterThanOrEqual(1)
|