entkapp 5.4.0 → 5.6.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.
- package/README.md +7 -1
- package/bin/cli.js +117 -58
- package/bin/cli.mjs +175 -0
- package/index.cjs +18 -0
- package/index.mjs +51 -0
- package/package.json +7 -6
- package/src/EngineContext.js +0 -6
- package/src/EngineContext.mjs +428 -0
- package/src/Initializer.mjs +82 -0
- package/src/analyzers/CodeSmellAnalyzer.mjs +106 -0
- package/src/analyzers/OxcAnalyzer.mjs +11 -0
- package/src/api/HeadlessAPI.js +31 -16
- package/src/api/HeadlessAPI.mjs +369 -0
- package/src/api/PluginSDK.mjs +135 -0
- package/src/ast/ASTAnalyzer.js +23 -97
- package/src/ast/ASTAnalyzer.mjs +742 -0
- package/src/ast/AdvancedAnalysis.mjs +586 -0
- package/src/ast/BarrelParser.mjs +230 -0
- package/src/ast/DeadCodeDetector.js +7 -5
- package/src/ast/DeadCodeDetector.mjs +92 -0
- package/src/ast/MagicDetector.js +43 -23
- package/src/ast/MagicDetector.mjs +203 -0
- package/src/ast/OxcAnalyzer.js +27 -190
- package/src/ast/OxcAnalyzer.mjs +188 -0
- package/src/ast/SecretScanner.mjs +374 -0
- package/src/healing/GitSandbox.mjs +82 -0
- package/src/healing/SelfHealer.mjs +48 -0
- package/src/index.js +41 -88
- package/src/index.mjs +1176 -0
- package/src/performance/GraphCache.js +0 -27
- package/src/performance/GraphCache.mjs +108 -0
- package/src/performance/SupplyChainGuard.mjs +92 -0
- package/src/performance/WorkerPool.js +4 -22
- package/src/performance/WorkerPool.mjs +132 -0
- package/src/performance/WorkerTaskRunner.js +0 -26
- package/src/performance/WorkerTaskRunner.mjs +144 -0
- package/src/plugins/BasePlugin.js +27 -16
- package/src/plugins/BasePlugin.mjs +240 -0
- package/src/plugins/PluginRegistry.js +0 -44
- package/src/plugins/PluginRegistry.mjs +203 -0
- package/src/plugins/ecosystems/BackendServices.mjs +197 -0
- package/src/plugins/ecosystems/GenericPlugins.mjs +142 -0
- package/src/plugins/ecosystems/ModernFrameworks.mjs +162 -0
- package/src/plugins/ecosystems/MorePlugins.mjs +562 -0
- package/src/plugins/ecosystems/NewPlugins.mjs +526 -0
- package/src/plugins/ecosystems/NextJsPlugin.mjs +45 -0
- package/src/plugins/ecosystems/PluginLoader.mjs +193 -0
- package/src/plugins/ecosystems/TypeScriptPlugin.mjs +56 -0
- package/src/plugins/ecosystems/UltimateBundle.js +0 -259
- package/src/plugins/ecosystems/UltimateBundle.mjs +1182 -0
- package/src/refractor/ImpactAnalyzer.mjs +92 -0
- package/src/refractor/SourceRewriter.mjs +86 -0
- package/src/refractor/TransactionManager.mjs +5 -0
- package/src/refractor/TypeIntegrity.mjs +4 -0
- package/src/resolution/BuildOrchestrator.mjs +46 -0
- package/src/resolution/CircularDetector.mjs +91 -0
- package/src/resolution/ConfigGenerator.mjs +83 -0
- package/src/resolution/ConfigLoader.js +26 -190
- package/src/resolution/ConfigLoader.mjs +94 -0
- package/src/resolution/DepencyResolver.mjs +66 -0
- package/src/resolution/DependencyFixer.mjs +88 -0
- package/src/resolution/DependencyProfiler.mjs +286 -0
- package/src/resolution/EntryPointDetector.mjs +134 -0
- package/src/resolution/FrameworkConfigParser.mjs +119 -0
- package/src/resolution/GraphAnalyzer.js +1 -65
- package/src/resolution/GraphAnalyzer.mjs +80 -0
- package/src/resolution/MigrationAnalyzer.mjs +60 -0
- package/src/resolution/PathMapper.js +0 -81
- package/src/resolution/PathMapper.mjs +82 -0
- package/src/resolution/TSConfigLoader.js +1 -3
- package/src/resolution/TSConfigLoader.mjs +162 -0
- package/src/resolution/WorkSpaceGraph.js +89 -260
- package/src/resolution/WorkSpaceGraph.mjs +183 -0
- package/src/resolution/WorkspaceDiagnostic.mjs +139 -0
- package/test.js +76 -0
- package/wrangler.toml +6 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lightweight Static Analysis Parser for Vite / Vitest / Framework Configurations.
|
|
6
|
+
* Extracts 'resolve.alias' and 'build.lib.entry' without executing the file.
|
|
7
|
+
* Version 5.4.0: Added poly-extension path resolution.
|
|
8
|
+
*/
|
|
9
|
+
export class FrameworkConfigParser {
|
|
10
|
+
constructor(context) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parses a Framework configuration file and extracts structural metadata.
|
|
16
|
+
* @param {string} content - Raw source code of the config file
|
|
17
|
+
* @param {string} filePath - Absolute path to the config file
|
|
18
|
+
* @returns {Object} { aliases: Map<string, string>, entries: Set<string> }
|
|
19
|
+
*/
|
|
20
|
+
parse(content, filePath) {
|
|
21
|
+
const results = {
|
|
22
|
+
aliases: new Map(),
|
|
23
|
+
entries: new Set()
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (!content) return results;
|
|
27
|
+
|
|
28
|
+
const configDir = path.dirname(filePath);
|
|
29
|
+
|
|
30
|
+
// 1. Extract Aliases from resolve: { alias: { ... } } or alias: [ ... ]
|
|
31
|
+
this._extractAliases(content, configDir, results.aliases);
|
|
32
|
+
|
|
33
|
+
// 2. Extract Entry Points from build: { lib: { entry: '...' } } or rollupOptions: { input: '...' }
|
|
34
|
+
this._extractEntries(content, configDir, results.entries);
|
|
35
|
+
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_extractAliases(content, configDir, aliasMap) {
|
|
40
|
+
const objAliasPatterns = [
|
|
41
|
+
/alias\s*:\s*\{([\s\S]*?)\}/g,
|
|
42
|
+
/resolve\s*:\s*\{[\s\S]*?alias\s*:\s*\{([\s\S]*?)\}/g
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
for (const pattern of objAliasPatterns) {
|
|
46
|
+
let match;
|
|
47
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
48
|
+
const inner = match[1];
|
|
49
|
+
const pairPattern = /(?:['"]?)(@?[a-zA-Z0-9_\-\/*]+)(?:['"]?)\s*:\s*(?:['"]([^'"]+)['"]|(path\.(?:resolve|join)\([\s\S]*?\)))/g;
|
|
50
|
+
let pair;
|
|
51
|
+
while ((pair = pairPattern.exec(inner)) !== null) {
|
|
52
|
+
const [_, key, stringVal, callVal] = pair;
|
|
53
|
+
aliasMap.set(key, this._resolveValue(stringVal || callVal, configDir));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const arrAliasPattern = /alias\s*:\s*\[([\s\S]*?)\]/;
|
|
59
|
+
const arrMatch = arrAliasPattern.exec(content);
|
|
60
|
+
if (arrMatch) {
|
|
61
|
+
const inner = arrMatch[1];
|
|
62
|
+
const findPattern = /\{\s*find\s*:\s*['"]([^'"]+)['"]\s*,\s*replacement\s*:\s*['"]([^'"]+)['"]\s*\}/g;
|
|
63
|
+
let findMatch;
|
|
64
|
+
while ((findMatch = findPattern.exec(inner)) !== null) {
|
|
65
|
+
const [_, key, value] = findMatch;
|
|
66
|
+
aliasMap.set(key, this._resolveValue(value, configDir));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_extractEntries(content, configDir, entrySet) {
|
|
72
|
+
const libEntryPattern = /entry\s*:\s*['"]([^'"]+)['"]/;
|
|
73
|
+
const libMatch = libEntryPattern.exec(content);
|
|
74
|
+
if (libMatch) {
|
|
75
|
+
entrySet.add(this._resolveValue(libMatch[1], configDir));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const inputPattern = /input\s*:\s*(?:['"]([^'"]+)['"]|\[([\s\S]*?)\])/;
|
|
79
|
+
const inputMatch = inputPattern.exec(content);
|
|
80
|
+
if (inputMatch) {
|
|
81
|
+
if (inputMatch[1]) {
|
|
82
|
+
entrySet.add(this._resolveValue(inputMatch[1], configDir));
|
|
83
|
+
} else if (inputMatch[2]) {
|
|
84
|
+
const paths = inputMatch[2].match(/['"]([^'"]+)['"]/g);
|
|
85
|
+
if (paths) {
|
|
86
|
+
paths.forEach(p => entrySet.add(this._resolveValue(p.replace(/['"]/g, ''), configDir)));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolves a path value and applies poly-extension fallback if the file doesn't exist.
|
|
94
|
+
*/
|
|
95
|
+
_resolveValue(val, configDir) {
|
|
96
|
+
const pathCallPattern = /path\.(?:resolve|join)\s*\(\s*(?:__dirname\s*,\s*)?['"]([^'"]+)['"]\s*\)/;
|
|
97
|
+
const pathMatch = pathCallPattern.exec(val);
|
|
98
|
+
if (pathMatch) {
|
|
99
|
+
val = pathMatch[1];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (val.startsWith('.') || val.startsWith('/') || /^[a-zA-Z]:/.test(val)) {
|
|
103
|
+
let resolved = path.resolve(configDir, val).replace(/\\/g, '/');
|
|
104
|
+
|
|
105
|
+
// UPGRADE: Poly-extension fallback
|
|
106
|
+
if (!fs.existsSync(resolved)) {
|
|
107
|
+
const base = resolved.replace(/\.[a-zA-Z0-9]+$/, '');
|
|
108
|
+
const extensions = ['.ts', '.mjs', '.tsx', '.jsx', '.mjs', '.cjs'];
|
|
109
|
+
for (const ext of extensions) {
|
|
110
|
+
if (fs.existsSync(base + ext)) {
|
|
111
|
+
return (base + ext).replace(/\\/g, '/');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return resolved;
|
|
116
|
+
}
|
|
117
|
+
return val;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -10,36 +10,6 @@ export class GraphAnalyzer {
|
|
|
10
10
|
constructor(context) {
|
|
11
11
|
this.context = context;
|
|
12
12
|
this.cwd = context.cwd;
|
|
13
|
-
// UPGRADE 5.4.3: Default Boundary Rules
|
|
14
|
-
this.boundaryRules = context.config?.boundaries || [
|
|
15
|
-
{ from: 'packages/shared-*', to: 'apps/*', allow: false, message: 'Shared packages must not import from applications.' },
|
|
16
|
-
{ from: '*', to: '**/internal/**', allow: false, message: 'Private internal utilities must not be imported externally.' }
|
|
17
|
-
];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* UPGRADE 5.4.3: Boundary Enforcement
|
|
22
|
-
* Checks if an import violates defined architectural boundaries.
|
|
23
|
-
*/
|
|
24
|
-
checkBoundaries(fromPath, toPath) {
|
|
25
|
-
const relFrom = path.relative(this.cwd, fromPath).replace(/\\/g, '/');
|
|
26
|
-
const relTo = path.relative(this.cwd, toPath).replace(/\\/g, '/');
|
|
27
|
-
|
|
28
|
-
for (const rule of this.boundaryRules) {
|
|
29
|
-
const fromMatch = this._globMatch(relFrom, rule.from);
|
|
30
|
-
const toMatch = this._globMatch(relTo, rule.to);
|
|
31
|
-
|
|
32
|
-
if (fromMatch && toMatch && !rule.allow) {
|
|
33
|
-
return { violated: true, message: rule.message };
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return { violated: false };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
_globMatch(str, pattern) {
|
|
40
|
-
if (pattern === '*') return true;
|
|
41
|
-
const regex = new RegExp('^' + pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*') + '$');
|
|
42
|
-
return regex.test(str);
|
|
43
13
|
}
|
|
44
14
|
|
|
45
15
|
/**
|
|
@@ -93,41 +63,7 @@ export class GraphAnalyzer {
|
|
|
93
63
|
}
|
|
94
64
|
}
|
|
95
65
|
|
|
96
|
-
|
|
97
|
-
const boundaryViolations = [];
|
|
98
|
-
|
|
99
|
-
for (const [filePath, node] of graph.entries()) {
|
|
100
|
-
if (!reachable.has(filePath)) continue;
|
|
101
|
-
|
|
102
|
-
// UPGRADE 5.4.3: Check boundaries for every import
|
|
103
|
-
for (const impPath of node.explicitImports) {
|
|
104
|
-
if (graph.has(impPath)) {
|
|
105
|
-
const violation = this.checkBoundaries(filePath, impPath);
|
|
106
|
-
if (violation.violated) {
|
|
107
|
-
boundaryViolations.push({
|
|
108
|
-
file: path.relative(this.cwd, filePath).replace(/\\/g, '/'),
|
|
109
|
-
target: path.relative(this.cwd, impPath).replace(/\\/g, '/'),
|
|
110
|
-
message: violation.message
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (node.localImportBindings) {
|
|
117
|
-
for (const [localName, meta] of node.localImportBindings.entries()) {
|
|
118
|
-
// Check if this local binding is used anywhere in the file
|
|
119
|
-
if (!node.instantiatedIdentifiers.has(localName)) {
|
|
120
|
-
unusedImports.push({
|
|
121
|
-
file: path.relative(this.cwd, filePath).replace(/\\/g, '/'),
|
|
122
|
-
specifier: meta.specifier,
|
|
123
|
-
symbol: meta.originalName === '*' ? 'Namespace' : meta.originalName
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return { deadFiles, zombieExports, unusedImports, boundaryViolations };
|
|
66
|
+
return { deadFiles, zombieExports };
|
|
131
67
|
}
|
|
132
68
|
|
|
133
69
|
_walk(filePath, reachable) {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* Dead Code & Zombie Export Hunter v5.1.0
|
|
6
|
+
* ============================================================================
|
|
7
|
+
* Identifies unused files and exported symbols that are never imported.
|
|
8
|
+
*/
|
|
9
|
+
export class GraphAnalyzer {
|
|
10
|
+
constructor(context) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
this.cwd = context.cwd;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Performs a full reachability analysis from entry points.
|
|
17
|
+
*/
|
|
18
|
+
async findDeadCode() {
|
|
19
|
+
const graph = this.context.projectGraph;
|
|
20
|
+
const reachable = new Set();
|
|
21
|
+
const entries = [];
|
|
22
|
+
|
|
23
|
+
// 1. Identify all confirmed entry points
|
|
24
|
+
for (const [filePath, node] of graph.entries()) {
|
|
25
|
+
if (node.isEntry) {
|
|
26
|
+
entries.push(filePath);
|
|
27
|
+
this._walk(filePath, reachable);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const deadFiles = [];
|
|
32
|
+
const zombieExports = [];
|
|
33
|
+
|
|
34
|
+
// 2. Find unreachable files
|
|
35
|
+
for (const filePath of graph.keys()) {
|
|
36
|
+
if (!reachable.has(filePath)) {
|
|
37
|
+
deadFiles.push(path.relative(this.cwd, filePath).replace(/\\/g, '/'));
|
|
38
|
+
} else {
|
|
39
|
+
// 3. Find Zombie Exports in reachable files
|
|
40
|
+
const node = graph.get(filePath);
|
|
41
|
+
if (node.isEntry) continue; // Skip entries as their exports are intended for external use
|
|
42
|
+
|
|
43
|
+
for (const [symbol, meta] of node.internalExports.entries()) {
|
|
44
|
+
if (symbol === 'default' || symbol === '*') continue;
|
|
45
|
+
|
|
46
|
+
let isUsed = false;
|
|
47
|
+
// Check if any other node imports this symbol
|
|
48
|
+
for (const [otherPath, otherNode] of graph.entries()) {
|
|
49
|
+
if (otherNode.importedSymbols && otherNode.importedSymbols.has(symbol)) {
|
|
50
|
+
// This is a simplified check; in reality, we'd check if it's imported FROM this file
|
|
51
|
+
isUsed = true;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!isUsed) {
|
|
57
|
+
zombieExports.push({
|
|
58
|
+
file: path.relative(this.cwd, filePath).replace(/\\/g, '/'),
|
|
59
|
+
symbol: symbol
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { deadFiles, zombieExports };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_walk(filePath, reachable) {
|
|
70
|
+
if (reachable.has(filePath)) return;
|
|
71
|
+
reachable.add(filePath);
|
|
72
|
+
|
|
73
|
+
const node = this.context.projectGraph.get(filePath);
|
|
74
|
+
if (!node) return;
|
|
75
|
+
|
|
76
|
+
for (const impPath of node.explicitImports) {
|
|
77
|
+
this._walk(impPath, reachable);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Migration Path Analyzer v5.1.0
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* Suggests modern alternatives for legacy tools and frameworks.
|
|
6
|
+
*/
|
|
7
|
+
export class MigrationAnalyzer {
|
|
8
|
+
constructor(context) {
|
|
9
|
+
this.context = context;
|
|
10
|
+
this.migrations = [
|
|
11
|
+
{
|
|
12
|
+
legacy: 'webpack',
|
|
13
|
+
modern: 'vite',
|
|
14
|
+
reason: 'Vite offers significantly faster HMR and build times using ES modules during development.'
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
legacy: 'jest',
|
|
18
|
+
modern: 'vitest',
|
|
19
|
+
reason: 'Vitest is faster, has native ESM support, and shares configuration with Vite.'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
legacy: 'moment',
|
|
23
|
+
modern: 'dayjs',
|
|
24
|
+
reason: 'Day.js is a 2KB alternative to Moment.js with the same modern API.'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
legacy: 'axios',
|
|
28
|
+
modern: 'fetch',
|
|
29
|
+
reason: 'Native fetch is now widely supported and requires no external dependency for simple use cases.'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
legacy: 'ts-node',
|
|
33
|
+
modern: 'tsx',
|
|
34
|
+
reason: 'tsx is faster and has better ESM support for running TypeScript files.'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
legacy: 'eslint-plugin-prettier',
|
|
38
|
+
modern: 'eslint-config-prettier',
|
|
39
|
+
reason: 'Running Prettier as an ESLint rule is slow. It is recommended to run them separately or use a config that turns off conflicting rules.'
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async analyze(activePlugins) {
|
|
45
|
+
const suggestions = [];
|
|
46
|
+
const activeNames = activePlugins.map(p => p.name);
|
|
47
|
+
|
|
48
|
+
for (const m of this.migrations) {
|
|
49
|
+
if (activeNames.includes(m.legacy)) {
|
|
50
|
+
suggestions.push({
|
|
51
|
+
from: m.legacy,
|
|
52
|
+
to: m.modern,
|
|
53
|
+
message: `Migration Suggestion: Consider moving from "${m.legacy}" to "${m.modern}". ${m.reason}`
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return suggestions;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -6,22 +6,15 @@ export class PathMapper {
|
|
|
6
6
|
constructor(context) {
|
|
7
7
|
this.context = context;
|
|
8
8
|
this.aliasMappers = []; // list of alias mapping functions
|
|
9
|
-
// UPGRADE: Raw alias patterns for fast "is this an alias?" checks without file-existence tests
|
|
10
|
-
this._aliasPatterns = []; // Array of { regex }
|
|
11
9
|
}
|
|
12
10
|
|
|
13
11
|
async loadMappings(tsconfigFilename = 'tsconfig.json') {
|
|
14
|
-
// UPGRADE: Reset on reload to avoid duplicate entries when called twice (workspace re-load)
|
|
15
|
-
this.aliasMappers = [];
|
|
16
|
-
this._aliasPatterns = [];
|
|
17
|
-
|
|
18
12
|
// Load root tsconfig
|
|
19
13
|
const loader = new TSConfigLoader(this.context.cwd);
|
|
20
14
|
const config = loader.load();
|
|
21
15
|
if (config) {
|
|
22
16
|
const mapper = loader.getAliasMapper(config);
|
|
23
17
|
this.aliasMappers.push(mapper);
|
|
24
|
-
this._collectAliasPatterns(config, loader);
|
|
25
18
|
if (this.context.verbose) console.log(`[PathMapper] Loaded root tsconfig aliases`);
|
|
26
19
|
}
|
|
27
20
|
|
|
@@ -33,86 +26,12 @@ export class PathMapper {
|
|
|
33
26
|
if (wsConfig) {
|
|
34
27
|
const wsMapper = wsLoader.getAliasMapper(wsConfig);
|
|
35
28
|
this.aliasMappers.push(wsMapper);
|
|
36
|
-
this._collectAliasPatterns(wsConfig, wsLoader);
|
|
37
29
|
if (this.context.verbose) console.log(`[PathMapper] Loaded workspace tsconfig aliases from ${root}`);
|
|
38
30
|
}
|
|
39
31
|
}
|
|
40
32
|
}
|
|
41
33
|
}
|
|
42
34
|
|
|
43
|
-
/**
|
|
44
|
-
* UPGRADE: Collect raw alias patterns from a parsed tsconfig so we can answer
|
|
45
|
-
* "is this specifier a tsconfig alias?" without needing the target file to exist on disk.
|
|
46
|
-
* Handles patterns like "@shared/*", "@idk/*", "~/*", etc.
|
|
47
|
-
* @param {Object} parsedConfig - Result of TSConfigLoader.load()
|
|
48
|
-
* @param {TSConfigLoader} loader
|
|
49
|
-
*/
|
|
50
|
-
_collectAliasPatterns(parsedConfig, loader) {
|
|
51
|
-
if (!parsedConfig || !parsedConfig.options) return;
|
|
52
|
-
const { paths, baseUrl } = parsedConfig.options;
|
|
53
|
-
|
|
54
|
-
if (paths) {
|
|
55
|
-
for (const pattern in paths) {
|
|
56
|
-
// Convert tsconfig glob pattern to a regex
|
|
57
|
-
// e.g. "@shared/*" -> /^@shared\// or "@shared" -> /^@shared$/
|
|
58
|
-
const escaped = pattern
|
|
59
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex special chars except *
|
|
60
|
-
.replace(/\\\*/g, '.*'); // replace escaped \* with .*
|
|
61
|
-
try {
|
|
62
|
-
this._aliasPatterns.push({ regex: new RegExp('^' + escaped + '$') });
|
|
63
|
-
// Also add a prefix variant so "@shared/foo/bar" matches "@shared/*"
|
|
64
|
-
const prefixEscaped = pattern.replace(/\*.*$/, '').replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
65
|
-
if (prefixEscaped && prefixEscaped !== escaped) {
|
|
66
|
-
this._aliasPatterns.push({ regex: new RegExp('^' + prefixEscaped) });
|
|
67
|
-
}
|
|
68
|
-
} catch (e) { /* ignore invalid regex */ }
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* UPGRADE: Returns true if the given specifier matches any tsconfig path alias pattern.
|
|
75
|
-
* This check does NOT require the target file to exist on disk.
|
|
76
|
-
* Used by the unlisted-dependency audit to skip tsconfig-aliased imports.
|
|
77
|
-
* @param {string} specifier
|
|
78
|
-
* @returns {boolean}
|
|
79
|
-
*/
|
|
80
|
-
isTsconfigAlias(specifier) {
|
|
81
|
-
if (!specifier || typeof specifier !== 'string') return false;
|
|
82
|
-
for (const { regex } of this._aliasPatterns) {
|
|
83
|
-
if (regex.test(specifier)) return true;
|
|
84
|
-
}
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* UPGRADE: Manually register a dynamic alias (e.g. from vite.config.js).
|
|
90
|
-
* @param {string} key - Alias key (e.g. "@shared")
|
|
91
|
-
* @param {string} target - Absolute path or relative path to resolve
|
|
92
|
-
*/
|
|
93
|
-
addAlias(key, target) {
|
|
94
|
-
if (!key || !target) return;
|
|
95
|
-
|
|
96
|
-
// 1. Add to aliasMappers for resolvePath()
|
|
97
|
-
const mapper = (p) => {
|
|
98
|
-
if (p === key) return target;
|
|
99
|
-
if (p.startsWith(key + '/')) {
|
|
100
|
-
return path.join(target, p.substring(key.length + 1)).replace(/\\/g, '/');
|
|
101
|
-
}
|
|
102
|
-
return p;
|
|
103
|
-
};
|
|
104
|
-
this.aliasMappers.push(mapper);
|
|
105
|
-
|
|
106
|
-
// 2. Add to _aliasPatterns for isTsconfigAlias() checks
|
|
107
|
-
const escaped = key.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
108
|
-
try {
|
|
109
|
-
this._aliasPatterns.push({ regex: new RegExp('^' + escaped + '$') });
|
|
110
|
-
this._aliasPatterns.push({ regex: new RegExp('^' + escaped + '/') });
|
|
111
|
-
} catch (e) {}
|
|
112
|
-
|
|
113
|
-
if (this.context.verbose) console.log(`[PathMapper] Added dynamic alias: ${key} -> ${target}`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
35
|
/**
|
|
117
36
|
* Resolves physical module paths on disk, translating modern .js imports
|
|
118
37
|
* back to their actual TypeScript source files.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { TSConfigLoader } from './TSConfigLoader.mjs';
|
|
4
|
+
|
|
5
|
+
export class PathMapper {
|
|
6
|
+
constructor(context) {
|
|
7
|
+
this.context = context;
|
|
8
|
+
this.aliasMappers = []; // list of alias mapping functions
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async loadMappings(tsconfigFilename = 'tsconfig.json') {
|
|
12
|
+
// Load root tsconfig
|
|
13
|
+
const loader = new TSConfigLoader(this.context.cwd);
|
|
14
|
+
const config = loader.load();
|
|
15
|
+
if (config) {
|
|
16
|
+
const mapper = loader.getAliasMapper(config);
|
|
17
|
+
this.aliasMappers.push(mapper);
|
|
18
|
+
if (this.context.verbose) console.log(`[PathMapper] Loaded root tsconfig aliases`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Load workspace tsconfigs if available
|
|
22
|
+
if (this.context.isWorkspaceEnabled && this.context.monorepoPackageRoots) {
|
|
23
|
+
for (const root of this.context.monorepoPackageRoots) {
|
|
24
|
+
const wsLoader = new TSConfigLoader(root);
|
|
25
|
+
const wsConfig = wsLoader.load();
|
|
26
|
+
if (wsConfig) {
|
|
27
|
+
const wsMapper = wsLoader.getAliasMapper(wsConfig);
|
|
28
|
+
this.aliasMappers.push(wsMapper);
|
|
29
|
+
if (this.context.verbose) console.log(`[PathMapper] Loaded workspace tsconfig aliases from ${root}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolves physical module paths on disk, translating modern .js imports
|
|
37
|
+
* back to their actual TypeScript source files.
|
|
38
|
+
* @param {string} p - The target module specifier or absolute path
|
|
39
|
+
*/
|
|
40
|
+
resolvePath(p) {
|
|
41
|
+
if (!p || typeof p !== 'string') return p;
|
|
42
|
+
|
|
43
|
+
let resolvedP = p;
|
|
44
|
+
|
|
45
|
+
// Try alias mappers first
|
|
46
|
+
for (const mapper of this.aliasMappers) {
|
|
47
|
+
const mapped = mapper(resolvedP);
|
|
48
|
+
if (mapped !== resolvedP && fs.existsSync(mapped)) {
|
|
49
|
+
resolvedP = mapped;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// FIX 1: If the import ends with .js, translate it to .ts for the search
|
|
55
|
+
if (resolvedP.endsWith('.mjs')) {
|
|
56
|
+
const tsPath = resolvedP.slice(0, -3) + '.ts';
|
|
57
|
+
if (fs.existsSync(tsPath)) return tsPath;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// FIX 2: If the import ends with .jsx, translate it to .tsx for the search
|
|
61
|
+
if (resolvedP.endsWith('.jsx')) {
|
|
62
|
+
const tsxPath = resolvedP.slice(0, -4) + '.tsx';
|
|
63
|
+
if (fs.existsSync(tsxPath)) return tsxPath;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// FIX 3: Support for directory imports (z.B. ./adapters -> ./adapters/index.ts)
|
|
67
|
+
try {
|
|
68
|
+
const stat = fs.statSync(resolvedP);
|
|
69
|
+
if (stat.isDirectory()) {
|
|
70
|
+
const extensions = ['.ts', '.tsx', '.mjs', '.jsx'];
|
|
71
|
+
for (const ext of extensions) {
|
|
72
|
+
const indexPath = path.join(resolvedP, `index${ext}`);
|
|
73
|
+
if (fs.existsSync(indexPath)) return indexPath;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// File does not exist or is not a directory, continue with default
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return resolvedP;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -109,9 +109,7 @@ export class TSConfigLoader {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
const { paths, baseUrl } = parsedConfig.options;
|
|
112
|
-
|
|
113
|
-
// If not, we fall back to targetDir
|
|
114
|
-
const base = baseUrl ? baseUrl : this.targetDir;
|
|
112
|
+
const base = baseUrl ? path.resolve(this.targetDir, baseUrl) : this.targetDir;
|
|
115
113
|
|
|
116
114
|
return (source) => {
|
|
117
115
|
// If no paths configured, still try baseUrl resolution
|