ucn 3.0.0
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.
Potentially problematic release.
This version of ucn might be problematic. Click here for more details.
- package/.claude/skills/ucn/SKILL.md +77 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/cli/index.js +2437 -0
- package/core/discovery.js +513 -0
- package/core/imports.js +558 -0
- package/core/output.js +1274 -0
- package/core/parser.js +279 -0
- package/core/project.js +3261 -0
- package/index.js +52 -0
- package/languages/go.js +653 -0
- package/languages/index.js +267 -0
- package/languages/java.js +826 -0
- package/languages/javascript.js +1346 -0
- package/languages/python.js +667 -0
- package/languages/rust.js +950 -0
- package/languages/utils.js +457 -0
- package/package.json +42 -0
- package/test/fixtures/go/go.mod +3 -0
- package/test/fixtures/go/main.go +257 -0
- package/test/fixtures/go/service.go +187 -0
- package/test/fixtures/java/DataService.java +279 -0
- package/test/fixtures/java/Main.java +287 -0
- package/test/fixtures/java/Utils.java +199 -0
- package/test/fixtures/java/pom.xml +6 -0
- package/test/fixtures/javascript/main.js +109 -0
- package/test/fixtures/javascript/package.json +1 -0
- package/test/fixtures/javascript/service.js +88 -0
- package/test/fixtures/javascript/utils.js +67 -0
- package/test/fixtures/python/main.py +198 -0
- package/test/fixtures/python/pyproject.toml +3 -0
- package/test/fixtures/python/service.py +166 -0
- package/test/fixtures/python/utils.py +118 -0
- package/test/fixtures/rust/Cargo.toml +3 -0
- package/test/fixtures/rust/main.rs +253 -0
- package/test/fixtures/rust/service.rs +210 -0
- package/test/fixtures/rust/utils.rs +154 -0
- package/test/fixtures/typescript/main.ts +154 -0
- package/test/fixtures/typescript/package.json +1 -0
- package/test/fixtures/typescript/repository.ts +149 -0
- package/test/fixtures/typescript/types.ts +114 -0
- package/test/parser.test.js +3661 -0
- package/test/public-repos-test.js +477 -0
- package/test/systematic-test.js +619 -0
- package/ucn.js +8 -0
package/core/imports.js
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/imports.js - Import/export parsing for dependency tracking
|
|
3
|
+
*
|
|
4
|
+
* Extracts import and export statements from source files
|
|
5
|
+
* to build dependency graphs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { getParser, getLanguageModule } = require('../languages');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Import patterns by language
|
|
14
|
+
* @deprecated Use AST-based findImportsInCode() from language modules instead.
|
|
15
|
+
* Kept only as fallback for unsupported languages or when AST parsing fails.
|
|
16
|
+
*/
|
|
17
|
+
const IMPORT_PATTERNS = {
|
|
18
|
+
javascript: {
|
|
19
|
+
importDefault: /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
|
|
20
|
+
importNamed: /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g,
|
|
21
|
+
importNamespace: /import\s*\*\s*as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
|
|
22
|
+
require: /(?:const|let|var)\s+(?:\{[^}]+\}|(\w+))\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
23
|
+
exportNamed: /^\s*export\s+(?:async\s+)?(?:function|class|const|let|var|interface|type)\s+(\w+)/gm,
|
|
24
|
+
exportDefault: /^\s*export\s+default\s+(?:(?:async\s+)?(?:function|class)\s+)?(\w+)?/gm,
|
|
25
|
+
exportList: /^\s*export\s*\{([^}]+)\}/gm,
|
|
26
|
+
moduleExports: /^module\.exports\s*=\s*(?:\{([^}]+)\}|(\w+))/gm,
|
|
27
|
+
exportsNamed: /^exports\.(\w+)\s*=[^=]/gm,
|
|
28
|
+
importType: /import\s+type\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g,
|
|
29
|
+
importSideEffect: /import\s+['"]([^'"]+)['"]/g,
|
|
30
|
+
importDynamic: /(?:await\s+)?import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
31
|
+
reExportNamed: /^\s*export\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/gm,
|
|
32
|
+
reExportAll: /^\s*export\s*\*\s*from\s*['"]([^'"]+)['"]/gm
|
|
33
|
+
},
|
|
34
|
+
python: {
|
|
35
|
+
importModule: /^import\s+([\w.]+)(?:\s+as\s+(\w+))?/gm,
|
|
36
|
+
fromImport: /^from\s+([.\w]+)\s+import\s+(.+)/gm,
|
|
37
|
+
exportAll: /__all__\s*=\s*\[([^\]]+)\]/g
|
|
38
|
+
},
|
|
39
|
+
go: {
|
|
40
|
+
importSingle: /import\s+"([^"]+)"/g,
|
|
41
|
+
importBlock: /import\s*\(\s*([\s\S]*?)\s*\)/g,
|
|
42
|
+
exportedFunc: /^func\s+(?:\([^)]+\)\s+)?([A-Z]\w*)\s*\(/gm,
|
|
43
|
+
exportedType: /^type\s+([A-Z]\w*)\s+/gm
|
|
44
|
+
},
|
|
45
|
+
java: {
|
|
46
|
+
importStatement: /import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;/g,
|
|
47
|
+
exportedClass: /public\s+(?:abstract\s+)?(?:final\s+)?(?:class|interface|enum)\s+(\w+)/g
|
|
48
|
+
},
|
|
49
|
+
rust: {
|
|
50
|
+
useStatement: /^use\s+([^;]+);/gm,
|
|
51
|
+
modDecl: /^\s*mod\s+(\w+)\s*;/gm
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract imports from file content using AST
|
|
57
|
+
*
|
|
58
|
+
* @param {string} content - File content
|
|
59
|
+
* @param {string} language - Language name
|
|
60
|
+
* @returns {{ imports: Array<{ module: string, names: string[], type: string, line: number }> }}
|
|
61
|
+
*/
|
|
62
|
+
function extractImports(content, language) {
|
|
63
|
+
// Normalize language name for parser
|
|
64
|
+
const normalizedLang = (language === 'typescript' || language === 'tsx') ? 'javascript' : language;
|
|
65
|
+
|
|
66
|
+
// Try AST-based extraction first
|
|
67
|
+
const langModule = getLanguageModule(normalizedLang);
|
|
68
|
+
if (langModule && typeof langModule.findImportsInCode === 'function') {
|
|
69
|
+
try {
|
|
70
|
+
const parser = getParser(normalizedLang);
|
|
71
|
+
if (parser) {
|
|
72
|
+
const imports = langModule.findImportsInCode(content, parser);
|
|
73
|
+
return { imports };
|
|
74
|
+
}
|
|
75
|
+
} catch (e) {
|
|
76
|
+
// Fall through to regex-based extraction
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fallback to regex-based extraction (deprecated)
|
|
81
|
+
const imports = [];
|
|
82
|
+
if (language === 'javascript' || language === 'typescript' || language === 'tsx') {
|
|
83
|
+
extractJSImports(content, imports);
|
|
84
|
+
} else if (language === 'python') {
|
|
85
|
+
extractPythonImports(content, imports);
|
|
86
|
+
} else if (language === 'go') {
|
|
87
|
+
extractGoImports(content, imports);
|
|
88
|
+
} else if (language === 'java') {
|
|
89
|
+
extractJavaImports(content, imports);
|
|
90
|
+
} else if (language === 'rust') {
|
|
91
|
+
extractRustImports(content, imports);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { imports };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @deprecated Use AST-based findImportsInCode() from language modules.
|
|
99
|
+
*/
|
|
100
|
+
function extractJSImports(content, imports) {
|
|
101
|
+
const patterns = IMPORT_PATTERNS.javascript;
|
|
102
|
+
let match;
|
|
103
|
+
|
|
104
|
+
// Default imports
|
|
105
|
+
let regex = new RegExp(patterns.importDefault.source, 'g');
|
|
106
|
+
while ((match = regex.exec(content)) !== null) {
|
|
107
|
+
imports.push({ module: match[2], names: [match[1]], type: 'default' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Named imports
|
|
111
|
+
regex = new RegExp(patterns.importNamed.source, 'g');
|
|
112
|
+
while ((match = regex.exec(content)) !== null) {
|
|
113
|
+
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
|
|
114
|
+
imports.push({ module: match[2], names, type: 'named' });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Namespace imports
|
|
118
|
+
regex = new RegExp(patterns.importNamespace.source, 'g');
|
|
119
|
+
while ((match = regex.exec(content)) !== null) {
|
|
120
|
+
imports.push({ module: match[2], names: [match[1]], type: 'namespace' });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Require
|
|
124
|
+
regex = new RegExp(patterns.require.source, 'g');
|
|
125
|
+
while ((match = regex.exec(content)) !== null) {
|
|
126
|
+
imports.push({ module: match[2], names: match[1] ? [match[1]] : [], type: 'require' });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Type imports
|
|
130
|
+
regex = new RegExp(patterns.importType.source, 'g');
|
|
131
|
+
while ((match = regex.exec(content)) !== null) {
|
|
132
|
+
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
|
|
133
|
+
imports.push({ module: match[2], names, type: 'type' });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Side-effect imports
|
|
137
|
+
regex = new RegExp(patterns.importSideEffect.source, 'g');
|
|
138
|
+
while ((match = regex.exec(content)) !== null) {
|
|
139
|
+
const module = match[1];
|
|
140
|
+
if (!imports.some(i => i.module === module)) {
|
|
141
|
+
imports.push({ module, names: [], type: 'side-effect' });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Dynamic imports
|
|
146
|
+
regex = new RegExp(patterns.importDynamic.source, 'g');
|
|
147
|
+
while ((match = regex.exec(content)) !== null) {
|
|
148
|
+
const module = match[1];
|
|
149
|
+
if (!imports.some(i => i.module === module)) {
|
|
150
|
+
imports.push({ module, names: [], type: 'dynamic' });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Re-exports
|
|
155
|
+
regex = new RegExp(patterns.reExportNamed.source, 'gm');
|
|
156
|
+
while ((match = regex.exec(content)) !== null) {
|
|
157
|
+
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
|
|
158
|
+
imports.push({ module: match[2], names, type: 're-export' });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
regex = new RegExp(patterns.reExportAll.source, 'gm');
|
|
162
|
+
while ((match = regex.exec(content)) !== null) {
|
|
163
|
+
imports.push({ module: match[1], names: ['*'], type: 're-export-all' });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** @deprecated Use AST-based findImportsInCode() from language modules. */
|
|
168
|
+
function extractPythonImports(content, imports) {
|
|
169
|
+
const patterns = IMPORT_PATTERNS.python;
|
|
170
|
+
let match;
|
|
171
|
+
|
|
172
|
+
let regex = new RegExp(patterns.importModule.source, 'gm');
|
|
173
|
+
while ((match = regex.exec(content)) !== null) {
|
|
174
|
+
const moduleName = match[1];
|
|
175
|
+
const alias = match[2] || moduleName.split('.').pop();
|
|
176
|
+
imports.push({ module: moduleName, names: [alias], type: 'module' });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
regex = new RegExp(patterns.fromImport.source, 'gm');
|
|
180
|
+
while ((match = regex.exec(content)) !== null) {
|
|
181
|
+
const moduleName = match[1];
|
|
182
|
+
const importList = match[2].trim();
|
|
183
|
+
|
|
184
|
+
if (importList === '*') {
|
|
185
|
+
imports.push({ module: moduleName, names: ['*'], type: 'star' });
|
|
186
|
+
} else {
|
|
187
|
+
const names = importList.split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n && n !== '(');
|
|
188
|
+
imports.push({ module: moduleName, names, type: 'from' });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @deprecated Use AST-based findImportsInCode() from language modules. */
|
|
194
|
+
function extractGoImports(content, imports) {
|
|
195
|
+
const patterns = IMPORT_PATTERNS.go;
|
|
196
|
+
let match;
|
|
197
|
+
|
|
198
|
+
let regex = new RegExp(patterns.importSingle.source, 'g');
|
|
199
|
+
while ((match = regex.exec(content)) !== null) {
|
|
200
|
+
const pkg = match[1];
|
|
201
|
+
imports.push({ module: pkg, names: [path.basename(pkg)], type: 'single' });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
regex = new RegExp(patterns.importBlock.source, 'g');
|
|
205
|
+
while ((match = regex.exec(content)) !== null) {
|
|
206
|
+
const block = match[1];
|
|
207
|
+
const pkgMatches = block.matchAll(/"([^"]+)"/g);
|
|
208
|
+
for (const pkgMatch of pkgMatches) {
|
|
209
|
+
const pkg = pkgMatch[1];
|
|
210
|
+
imports.push({ module: pkg, names: [path.basename(pkg)], type: 'block' });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** @deprecated Use AST-based findImportsInCode() from language modules. */
|
|
216
|
+
function extractJavaImports(content, imports) {
|
|
217
|
+
const patterns = IMPORT_PATTERNS.java;
|
|
218
|
+
let match;
|
|
219
|
+
|
|
220
|
+
let regex = new RegExp(patterns.importStatement.source, 'g');
|
|
221
|
+
while ((match = regex.exec(content)) !== null) {
|
|
222
|
+
const fullImport = match[1];
|
|
223
|
+
const parts = fullImport.split('.');
|
|
224
|
+
const name = parts[parts.length - 1];
|
|
225
|
+
imports.push({ module: fullImport, names: name === '*' ? ['*'] : [name], type: 'import' });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** @deprecated Use AST-based findImportsInCode() from language modules. */
|
|
230
|
+
function extractRustImports(content, imports) {
|
|
231
|
+
const patterns = IMPORT_PATTERNS.rust;
|
|
232
|
+
let match;
|
|
233
|
+
|
|
234
|
+
let regex = new RegExp(patterns.useStatement.source, 'gm');
|
|
235
|
+
while ((match = regex.exec(content)) !== null) {
|
|
236
|
+
let raw = match[1].trim().split('{')[0].trim().split(' as ')[0].trim().replace(/::$/, '');
|
|
237
|
+
if (raw) {
|
|
238
|
+
imports.push({ module: raw, names: [], type: 'use' });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
regex = new RegExp(patterns.modDecl.source, 'gm');
|
|
243
|
+
while ((match = regex.exec(content)) !== null) {
|
|
244
|
+
imports.push({ module: `self::${match[1]}`, names: [match[1]], type: 'mod' });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Extract exports from file content using AST
|
|
250
|
+
*/
|
|
251
|
+
function extractExports(content, language) {
|
|
252
|
+
// Normalize language name for parser
|
|
253
|
+
const normalizedLang = (language === 'typescript' || language === 'tsx') ? 'javascript' : language;
|
|
254
|
+
|
|
255
|
+
// Try AST-based extraction first
|
|
256
|
+
const langModule = getLanguageModule(normalizedLang);
|
|
257
|
+
if (langModule && typeof langModule.findExportsInCode === 'function') {
|
|
258
|
+
try {
|
|
259
|
+
const parser = getParser(normalizedLang);
|
|
260
|
+
if (parser) {
|
|
261
|
+
const foundExports = langModule.findExportsInCode(content, parser);
|
|
262
|
+
return { exports: foundExports };
|
|
263
|
+
}
|
|
264
|
+
} catch (e) {
|
|
265
|
+
// Fall through to regex-based extraction
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Fallback to regex-based extraction (deprecated)
|
|
270
|
+
const foundExports = [];
|
|
271
|
+
if (language === 'javascript' || language === 'typescript' || language === 'tsx') {
|
|
272
|
+
extractJSExports(content, foundExports);
|
|
273
|
+
} else if (language === 'python') {
|
|
274
|
+
extractPythonExports(content, foundExports);
|
|
275
|
+
} else if (language === 'go') {
|
|
276
|
+
extractGoExports(content, foundExports);
|
|
277
|
+
} else if (language === 'java') {
|
|
278
|
+
extractJavaExports(content, foundExports);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { exports: foundExports };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** @deprecated Use AST-based findExportsInCode() from language modules. */
|
|
285
|
+
function extractJSExports(content, exports) {
|
|
286
|
+
const patterns = IMPORT_PATTERNS.javascript;
|
|
287
|
+
let match;
|
|
288
|
+
|
|
289
|
+
let regex = new RegExp(patterns.exportNamed.source, 'gm');
|
|
290
|
+
while ((match = regex.exec(content)) !== null) {
|
|
291
|
+
exports.push({ name: match[1], type: 'named' });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
regex = new RegExp(patterns.exportDefault.source, 'gm');
|
|
295
|
+
while ((match = regex.exec(content)) !== null) {
|
|
296
|
+
exports.push({ name: match[1] || 'default', type: 'default' });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
regex = new RegExp(patterns.exportList.source, 'gm');
|
|
300
|
+
while ((match = regex.exec(content)) !== null) {
|
|
301
|
+
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
|
|
302
|
+
for (const name of names) {
|
|
303
|
+
exports.push({ name, type: 'list' });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
regex = new RegExp(patterns.exportsNamed.source, 'gm');
|
|
308
|
+
while ((match = regex.exec(content)) !== null) {
|
|
309
|
+
exports.push({ name: match[1], type: 'commonjs-named' });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// module.exports = { a, b, c } or module.exports = identifier
|
|
313
|
+
regex = new RegExp(patterns.moduleExports.source, 'gm');
|
|
314
|
+
while ((match = regex.exec(content)) !== null) {
|
|
315
|
+
if (match[1]) {
|
|
316
|
+
// Object literal: module.exports = { a, b, c }
|
|
317
|
+
const names = match[1].split(',').map(n => n.trim().split(/\s*:\s*/)[0].trim()).filter(n => n && !n.includes('('));
|
|
318
|
+
for (const name of names) {
|
|
319
|
+
exports.push({ name, type: 'commonjs-object' });
|
|
320
|
+
}
|
|
321
|
+
} else if (match[2]) {
|
|
322
|
+
// Single identifier: module.exports = SomeClass
|
|
323
|
+
exports.push({ name: match[2], type: 'commonjs-default' });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** @deprecated Use AST-based findExportsInCode() from language modules. */
|
|
329
|
+
function extractPythonExports(content, exports) {
|
|
330
|
+
let match;
|
|
331
|
+
|
|
332
|
+
// Check for __all__
|
|
333
|
+
let regex = new RegExp(IMPORT_PATTERNS.python.exportAll.source, 'g');
|
|
334
|
+
while ((match = regex.exec(content)) !== null) {
|
|
335
|
+
const names = match[1].split(',').map(n => n.trim().replace(/['"]/g, '')).filter(n => n);
|
|
336
|
+
for (const name of names) {
|
|
337
|
+
exports.push({ name, type: 'explicit' });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// If no __all__, look for public names
|
|
342
|
+
if (exports.length === 0) {
|
|
343
|
+
const funcRegex = /^def\s+([a-zA-Z]\w*)\s*\(/gm;
|
|
344
|
+
while ((match = funcRegex.exec(content)) !== null) {
|
|
345
|
+
if (!match[1].startsWith('_')) {
|
|
346
|
+
exports.push({ name: match[1], type: 'function' });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const classRegex = /^class\s+([a-zA-Z]\w*)/gm;
|
|
351
|
+
while ((match = classRegex.exec(content)) !== null) {
|
|
352
|
+
if (!match[1].startsWith('_')) {
|
|
353
|
+
exports.push({ name: match[1], type: 'class' });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** @deprecated Use AST-based findExportsInCode() from language modules. */
|
|
360
|
+
function extractGoExports(content, exports) {
|
|
361
|
+
const patterns = IMPORT_PATTERNS.go;
|
|
362
|
+
let match;
|
|
363
|
+
|
|
364
|
+
let regex = new RegExp(patterns.exportedFunc.source, 'gm');
|
|
365
|
+
while ((match = regex.exec(content)) !== null) {
|
|
366
|
+
exports.push({ name: match[1], type: 'function' });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
regex = new RegExp(patterns.exportedType.source, 'gm');
|
|
370
|
+
while ((match = regex.exec(content)) !== null) {
|
|
371
|
+
exports.push({ name: match[1], type: 'type' });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** @deprecated Use AST-based findExportsInCode() from language modules. */
|
|
376
|
+
function extractJavaExports(content, exports) {
|
|
377
|
+
let match;
|
|
378
|
+
let regex = new RegExp(IMPORT_PATTERNS.java.exportedClass.source, 'g');
|
|
379
|
+
while ((match = regex.exec(content)) !== null) {
|
|
380
|
+
exports.push({ name: match[1], type: 'class' });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Cache for tsconfig lookups
|
|
385
|
+
const tsconfigCache = new Map();
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Resolve an import path to an actual file path
|
|
389
|
+
*
|
|
390
|
+
* @param {string} importPath - Import string
|
|
391
|
+
* @param {string} fromFile - File containing the import
|
|
392
|
+
* @param {object} config - Configuration { aliases, extensions, language, root }
|
|
393
|
+
* @returns {string|null} - Resolved absolute path or null if external
|
|
394
|
+
*/
|
|
395
|
+
function resolveImport(importPath, fromFile, config = {}) {
|
|
396
|
+
const fromDir = path.dirname(fromFile);
|
|
397
|
+
|
|
398
|
+
// Strip query strings (e.g., ?raw, ?url)
|
|
399
|
+
importPath = importPath.split('?')[0];
|
|
400
|
+
|
|
401
|
+
// External packages (not relative or alias)
|
|
402
|
+
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
|
403
|
+
// Check aliases
|
|
404
|
+
if (config.aliases) {
|
|
405
|
+
for (const [alias, target] of Object.entries(config.aliases)) {
|
|
406
|
+
if (importPath.startsWith(alias)) {
|
|
407
|
+
const relativePath = importPath.slice(alias.length);
|
|
408
|
+
const targetPath = path.join(config.root || fromDir, target, relativePath);
|
|
409
|
+
return resolveFilePath(targetPath, config.extensions || getExtensions(config.language));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Check tsconfig paths (JS/TS only)
|
|
415
|
+
if (config.language === 'javascript' || config.language === 'typescript' || config.language === 'tsx') {
|
|
416
|
+
const tsconfig = findTsConfig(fromDir, config.root);
|
|
417
|
+
if (tsconfig && tsconfig.compiledPaths) {
|
|
418
|
+
// Use pre-compiled regex patterns from cache
|
|
419
|
+
for (const { regex, targets } of tsconfig.compiledPaths) {
|
|
420
|
+
const match = importPath.match(regex);
|
|
421
|
+
if (match) {
|
|
422
|
+
for (const target of targets) {
|
|
423
|
+
const resolved = target.replace('*', match[1] || '');
|
|
424
|
+
const basePath = tsconfig.baseUrl || path.dirname(tsconfig.configPath);
|
|
425
|
+
const fullPath = path.join(basePath, resolved);
|
|
426
|
+
const result = resolveFilePath(fullPath, config.extensions || getExtensions(config.language));
|
|
427
|
+
if (result) return result;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return null; // External package
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Relative imports
|
|
438
|
+
const resolved = path.resolve(fromDir, importPath);
|
|
439
|
+
return resolveFilePath(resolved, config.extensions || getExtensions(config.language));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Try to resolve a path with various extensions
|
|
444
|
+
*/
|
|
445
|
+
function resolveFilePath(basePath, extensions) {
|
|
446
|
+
// Check exact path
|
|
447
|
+
if (fs.existsSync(basePath) && fs.statSync(basePath).isFile()) {
|
|
448
|
+
return basePath;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Try adding extensions
|
|
452
|
+
for (const ext of extensions) {
|
|
453
|
+
const withExt = basePath + ext;
|
|
454
|
+
if (fs.existsSync(withExt)) return withExt;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Try index files
|
|
458
|
+
for (const ext of extensions) {
|
|
459
|
+
const indexPath = path.join(basePath, 'index' + ext);
|
|
460
|
+
if (fs.existsSync(indexPath)) return indexPath;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Get file extensions for a language
|
|
468
|
+
*/
|
|
469
|
+
function getExtensions(language) {
|
|
470
|
+
switch (language) {
|
|
471
|
+
case 'javascript':
|
|
472
|
+
return ['.js', '.jsx', '.mjs', '.cjs'];
|
|
473
|
+
case 'typescript':
|
|
474
|
+
case 'tsx':
|
|
475
|
+
return ['.ts', '.tsx', '.js', '.jsx'];
|
|
476
|
+
case 'python':
|
|
477
|
+
return ['.py'];
|
|
478
|
+
case 'go':
|
|
479
|
+
return ['.go'];
|
|
480
|
+
case 'java':
|
|
481
|
+
return ['.java'];
|
|
482
|
+
case 'rust':
|
|
483
|
+
return ['.rs'];
|
|
484
|
+
default:
|
|
485
|
+
return ['.js', '.ts'];
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Find and load tsconfig.json
|
|
491
|
+
*/
|
|
492
|
+
function findTsConfig(fromDir, rootDir) {
|
|
493
|
+
const cacheKey = fromDir;
|
|
494
|
+
if (tsconfigCache.has(cacheKey)) {
|
|
495
|
+
return tsconfigCache.get(cacheKey);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
let currentDir = fromDir;
|
|
499
|
+
const normalizedRoot = rootDir ? path.resolve(rootDir) : null;
|
|
500
|
+
|
|
501
|
+
while (true) {
|
|
502
|
+
const tsconfigPath = path.join(currentDir, 'tsconfig.json');
|
|
503
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
504
|
+
try {
|
|
505
|
+
const content = fs.readFileSync(tsconfigPath, 'utf-8');
|
|
506
|
+
const cleanJson = stripJsonComments(content);
|
|
507
|
+
const config = JSON.parse(cleanJson);
|
|
508
|
+
|
|
509
|
+
const paths = config.compilerOptions?.paths || {};
|
|
510
|
+
// Pre-compile regex patterns for path aliases to avoid repeated compilation
|
|
511
|
+
const compiledPaths = Object.entries(paths).map(([pattern, targets]) => ({
|
|
512
|
+
pattern,
|
|
513
|
+
regex: new RegExp('^' + pattern.replace('*', '(.*)') + '$'),
|
|
514
|
+
targets
|
|
515
|
+
}));
|
|
516
|
+
|
|
517
|
+
const result = {
|
|
518
|
+
configPath: tsconfigPath,
|
|
519
|
+
baseUrl: config.compilerOptions?.baseUrl
|
|
520
|
+
? path.resolve(path.dirname(tsconfigPath), config.compilerOptions.baseUrl)
|
|
521
|
+
: null,
|
|
522
|
+
paths,
|
|
523
|
+
compiledPaths
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
tsconfigCache.set(cacheKey, result);
|
|
527
|
+
return result;
|
|
528
|
+
} catch (e) {
|
|
529
|
+
// Skip malformed tsconfig
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const parent = path.dirname(currentDir);
|
|
534
|
+
if (parent === currentDir) break;
|
|
535
|
+
if (normalizedRoot && !currentDir.startsWith(normalizedRoot)) break;
|
|
536
|
+
currentDir = parent;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
tsconfigCache.set(cacheKey, null);
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Strip JSON comments
|
|
545
|
+
*/
|
|
546
|
+
function stripJsonComments(content) {
|
|
547
|
+
return content
|
|
548
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
549
|
+
.replace(/\/\/.*/g, '')
|
|
550
|
+
.replace(/,(\s*[}\]])/g, '$1');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
module.exports = {
|
|
554
|
+
extractImports,
|
|
555
|
+
extractExports,
|
|
556
|
+
resolveImport,
|
|
557
|
+
IMPORT_PATTERNS
|
|
558
|
+
};
|