@getmikk/core 1.8.0 → 1.8.2
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-reader.ts +9 -9
- package/src/contract/lock-reader.ts +9 -6
- package/src/graph/dead-code-detector.ts +111 -52
- package/src/graph/graph-builder.ts +199 -61
- package/src/graph/impact-analyzer.ts +48 -16
- package/src/index.ts +1 -1
- package/src/parser/javascript/js-extractor.ts +22 -6
- package/src/parser/javascript/js-parser.ts +24 -17
- package/src/parser/javascript/js-resolver.ts +63 -22
- package/src/parser/parser-constants.ts +82 -0
- package/src/parser/tree-sitter/parser.ts +353 -69
- package/src/parser/types.ts +2 -0
- package/src/parser/typescript/ts-extractor.ts +17 -6
- package/src/parser/typescript/ts-parser.ts +22 -18
- package/src/parser/typescript/ts-resolver.ts +78 -45
- package/src/utils/fs.ts +64 -0
- package/src/utils/json.ts +27 -0
- package/tests/js-parser.test.ts +23 -3
- package/tests/tree-sitter-parser.test.ts +11 -3
|
@@ -7,11 +7,15 @@ import type { ParsedImport } from '../types.js'
|
|
|
7
7
|
* Handles:
|
|
8
8
|
* - Relative ESM imports: import './utils' → ./utils.js / ./utils/index.js / ...
|
|
9
9
|
* - CommonJS require(): require('./db') → same resolution order
|
|
10
|
-
* - Path aliases from jsconfig.json / tsconfig.json
|
|
10
|
+
* - Path aliases from jsconfig.json / tsconfig.json (all targets tried, not just first)
|
|
11
11
|
* - Mixed TS/JS projects: falls back to .ts/.tsx if no JS file matched
|
|
12
12
|
*
|
|
13
13
|
* Extension probe order: .js → .jsx → .mjs → .cjs → index.js → index.jsx →
|
|
14
14
|
* .ts → .tsx → index.ts → index.tsx
|
|
15
|
+
*
|
|
16
|
+
* Performance: allProjectFiles is converted to a Set internally for O(1) membership
|
|
17
|
+
* checks. Pass the same array every call — the Set is built per-call and is cheap
|
|
18
|
+
* for the sizes we deal with, or cache the Set externally and pass it directly.
|
|
15
19
|
*/
|
|
16
20
|
export class JavaScriptResolver {
|
|
17
21
|
constructor(
|
|
@@ -28,39 +32,77 @@ export class JavaScriptResolver {
|
|
|
28
32
|
) {
|
|
29
33
|
return { ...imp, resolvedPath: '' }
|
|
30
34
|
}
|
|
31
|
-
|
|
35
|
+
// Build Set once per resolve call. For large repos callers should cache this.
|
|
36
|
+
const fileSet = allProjectFiles.length > 0 ? new Set(allProjectFiles) : null
|
|
37
|
+
return { ...imp, resolvedPath: this.resolvePath(imp.source, fromFile, fileSet) }
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
resolveAll(imports: ParsedImport[], fromFile: string, allProjectFiles: string[] = []): ParsedImport[] {
|
|
35
|
-
|
|
41
|
+
// Build Set once for the entire batch — O(n) instead of O(n * imports * probes)
|
|
42
|
+
const fileSet = allProjectFiles.length > 0 ? new Set(allProjectFiles) : null
|
|
43
|
+
return imports.map(imp => {
|
|
44
|
+
if (
|
|
45
|
+
!imp.source.startsWith('.') &&
|
|
46
|
+
!imp.source.startsWith('/') &&
|
|
47
|
+
!this.matchesAlias(imp.source)
|
|
48
|
+
) {
|
|
49
|
+
return { ...imp, resolvedPath: '' }
|
|
50
|
+
}
|
|
51
|
+
return { ...imp, resolvedPath: this.resolvePath(imp.source, fromFile, fileSet) }
|
|
52
|
+
})
|
|
36
53
|
}
|
|
37
54
|
|
|
38
|
-
private resolvePath(source: string, fromFile: string,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// 1. Alias substitution
|
|
55
|
+
private resolvePath(source: string, fromFile: string, fileSet: Set<string> | null): string {
|
|
56
|
+
// 1. Alias substitution — try ALL targets in order, not just targets[0]
|
|
42
57
|
for (const [alias, targets] of Object.entries(this.aliases)) {
|
|
43
58
|
const prefix = alias.replace('/*', '')
|
|
44
59
|
if (source.startsWith(prefix)) {
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
for (const target of targets) {
|
|
61
|
+
const substituted = target.replace('/*', '') + source.slice(prefix.length)
|
|
62
|
+
const resolved = this.normalizePath(substituted, fromFile)
|
|
63
|
+
const found = this.probeExtensions(resolved, fileSet)
|
|
64
|
+
if (found) return found
|
|
65
|
+
}
|
|
66
|
+
// All alias targets failed — fall through to return unresolved indicator
|
|
67
|
+
// rather than silently returning a path that doesn't exist
|
|
68
|
+
return ''
|
|
47
69
|
}
|
|
48
70
|
}
|
|
49
71
|
|
|
50
|
-
// 2. Build absolute-like posix path
|
|
72
|
+
// 2. Build absolute-like posix path from relative source
|
|
73
|
+
const resolved = this.normalizePath(source, fromFile)
|
|
74
|
+
|
|
75
|
+
// 3. Already has a concrete extension — validate existence and return
|
|
76
|
+
const knownExts = ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx']
|
|
77
|
+
if (knownExts.some(e => resolved.endsWith(e))) {
|
|
78
|
+
// If we have a file list, validate the path exists in it
|
|
79
|
+
if (fileSet && !fileSet.has(resolved)) return ''
|
|
80
|
+
return resolved
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 4. Probe extensions in priority order
|
|
84
|
+
return this.probeExtensions(resolved, fileSet) ?? ''
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Normalize a source path to a posix-style project-relative path */
|
|
88
|
+
private normalizePath(source: string, fromFile: string): string {
|
|
51
89
|
let resolved: string
|
|
52
|
-
if (
|
|
53
|
-
resolved = path.posix.normalize(
|
|
90
|
+
if (source.startsWith('.')) {
|
|
91
|
+
resolved = path.posix.normalize(
|
|
92
|
+
path.posix.join(path.dirname(fromFile.replace(/\\/g, '/')), source)
|
|
93
|
+
)
|
|
54
94
|
} else {
|
|
55
|
-
resolved =
|
|
95
|
+
resolved = source
|
|
56
96
|
}
|
|
57
|
-
|
|
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
|
|
97
|
+
return resolved.replace(/\\/g, '/')
|
|
98
|
+
}
|
|
62
99
|
|
|
63
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Probe file extensions in priority order.
|
|
102
|
+
* Returns the first candidate that exists in the file set,
|
|
103
|
+
* or the first candidate if no file set is provided (legacy behaviour).
|
|
104
|
+
*/
|
|
105
|
+
private probeExtensions(resolved: string, fileSet: Set<string> | null): string | null {
|
|
64
106
|
const probeOrder = [
|
|
65
107
|
'.js', '.jsx', '.mjs', '.cjs',
|
|
66
108
|
'/index.js', '/index.jsx', '/index.mjs',
|
|
@@ -69,12 +111,11 @@ export class JavaScriptResolver {
|
|
|
69
111
|
]
|
|
70
112
|
for (const ext of probeOrder) {
|
|
71
113
|
const candidate = resolved + ext
|
|
72
|
-
if (
|
|
114
|
+
if (fileSet === null || fileSet.has(candidate)) {
|
|
73
115
|
return candidate
|
|
74
116
|
}
|
|
75
117
|
}
|
|
76
|
-
|
|
77
|
-
return resolved + '.js'
|
|
118
|
+
return null
|
|
78
119
|
}
|
|
79
120
|
|
|
80
121
|
private matchesAlias(source: string): boolean {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared parser utilities/constants.
|
|
3
|
+
* Keeps common thresholds and comment-stripping helpers in one place so
|
|
4
|
+
* JavaScript and TypeScript parsers behave consistently.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Files count threshold that signals a reasonably complete project scan. */
|
|
8
|
+
export const MIN_FILES_FOR_COMPLETE_SCAN = 10
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Remove JSON5-style comments while preserving string literals.
|
|
12
|
+
* Handles single/double/backtick strings plus escaped characters.
|
|
13
|
+
*/
|
|
14
|
+
export function stripJsonComments(raw: string): string {
|
|
15
|
+
let result = ''
|
|
16
|
+
let i = 0
|
|
17
|
+
let stringChar: string | null = null
|
|
18
|
+
let escaped = false
|
|
19
|
+
|
|
20
|
+
while (i < raw.length) {
|
|
21
|
+
const char = raw[i]
|
|
22
|
+
const next = i + 1 < raw.length ? raw[i + 1] : ''
|
|
23
|
+
|
|
24
|
+
if (stringChar) {
|
|
25
|
+
result += char
|
|
26
|
+
if (escaped) {
|
|
27
|
+
escaped = false
|
|
28
|
+
} else if (char === '\\') {
|
|
29
|
+
escaped = true
|
|
30
|
+
} else if (char === stringChar) {
|
|
31
|
+
stringChar = null
|
|
32
|
+
}
|
|
33
|
+
i += 1
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
38
|
+
stringChar = char
|
|
39
|
+
result += char
|
|
40
|
+
i += 1
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (char === '/' && next === '*') {
|
|
45
|
+
i += 2
|
|
46
|
+
while (i < raw.length) {
|
|
47
|
+
if (raw[i] === '*' && raw[i + 1] === '/') {
|
|
48
|
+
i += 2
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
i += 1
|
|
52
|
+
}
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (char === '/' && next === '/') {
|
|
57
|
+
i += 2
|
|
58
|
+
while (i < raw.length && raw[i] !== '\n' && raw[i] !== '\r') {
|
|
59
|
+
i += 1
|
|
60
|
+
}
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
result += char
|
|
65
|
+
i += 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse JSON config files while tolerating JSON5 comments.
|
|
73
|
+
* Falls back to the raw content if comment stripping breaks URLs.
|
|
74
|
+
*/
|
|
75
|
+
export function parseJsonWithComments<T = any>(raw: string): T {
|
|
76
|
+
const stripped = stripJsonComments(raw)
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(stripped)
|
|
79
|
+
} catch {
|
|
80
|
+
return JSON.parse(raw)
|
|
81
|
+
}
|
|
82
|
+
}
|