@getmikk/core 1.8.1 → 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.
@@ -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
- return { ...imp, resolvedPath: this.resolvePath(imp.source, fromFile, allProjectFiles) }
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
- return imports.map(imp => this.resolve(imp, fromFile, allProjectFiles))
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, allProjectFiles: string[]): string {
39
- let resolvedSource = source
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
- resolvedSource = targets[0].replace('/*', '') + source.slice(prefix.length)
46
- break
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 (resolvedSource.startsWith('.')) {
53
- resolved = path.posix.normalize(path.posix.join(path.dirname(fromFile), resolvedSource))
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 = resolvedSource
95
+ resolved = source
56
96
  }
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
97
+ return resolved.replace(/\\/g, '/')
98
+ }
62
99
 
63
- // 4. Probe extensions: prefer JS-family first, fall back to TS for mixed projects
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 (allProjectFiles.length === 0 || allProjectFiles.includes(candidate)) {
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
+ }