entkapp 5.3.1 → 5.4.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 +1 -7
- package/bin/cli.js +58 -117
- package/package.json +1 -1
- package/src/EngineContext.js +6 -0
- package/src/ast/ASTAnalyzer.js +94 -6
- package/src/ast/MagicDetector.js +23 -43
- package/src/ast/OxcAnalyzer.js +190 -27
- package/src/index.js +87 -4
- package/src/performance/GraphCache.js +27 -0
- package/src/performance/WorkerPool.js +22 -4
- package/src/performance/WorkerTaskRunner.js +26 -0
- package/src/plugins/BasePlugin.js +16 -27
- package/src/plugins/PluginRegistry.js +44 -0
- package/src/plugins/ecosystems/UltimateBundle.js +259 -0
- package/src/resolution/ConfigLoader.js +190 -26
- package/src/resolution/FrameworkConfigParser.js +119 -0
- package/src/resolution/GraphAnalyzer.js +65 -1
- package/src/resolution/PathMapper.js +81 -0
- package/src/resolution/TSConfigLoader.js +3 -1
- package/src/resolution/WorkSpaceGraph.js +260 -89
package/src/ast/OxcAnalyzer.js
CHANGED
|
@@ -2,13 +2,18 @@ import path from 'path';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Production-Grade Native Rust AST Parser Bridge (OXC)
|
|
5
|
-
*
|
|
5
|
+
* Optimized for Monorepos and Windows environments.
|
|
6
|
+
* Implements the same semantic logic as ASTAnalyzer but using the high-performance OXC parser.
|
|
6
7
|
*/
|
|
7
8
|
export class OxcAnalyzer {
|
|
8
9
|
constructor(context) {
|
|
9
10
|
this.context = context;
|
|
10
11
|
this.oxc = null;
|
|
11
12
|
this.isAvailable = false;
|
|
13
|
+
this.scopeStack = [];
|
|
14
|
+
this.currentScope = null;
|
|
15
|
+
this.scopeCounter = 0;
|
|
16
|
+
this.pass = 1;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
async init() {
|
|
@@ -35,7 +40,6 @@ export class OxcAnalyzer {
|
|
|
35
40
|
|
|
36
41
|
/**
|
|
37
42
|
* WINDOWS FIX: Robust path normalization for OXC.
|
|
38
|
-
* Replaces backslashes with forward slashes to prevent escape sequence errors.
|
|
39
43
|
*/
|
|
40
44
|
normalizePath(filePath) {
|
|
41
45
|
if (!filePath) return filePath;
|
|
@@ -64,13 +68,10 @@ export class OxcAnalyzer {
|
|
|
64
68
|
lang: "typescript"
|
|
65
69
|
});
|
|
66
70
|
} catch (e) {
|
|
67
|
-
// Fallback with normalized path if the options object fails
|
|
68
71
|
try {
|
|
69
72
|
result = this.oxc.parseSync(normalizedPath, cleanContent);
|
|
70
73
|
} catch (innerErr) {
|
|
71
|
-
if (this.context.verbose) {
|
|
72
|
-
console.error(`[OXC-ERROR] Native parse failed for: ${normalizedPath}`);
|
|
73
|
-
}
|
|
74
|
+
if (this.context.verbose) console.error(`[OXC-ERROR] Native parse failed for: ${normalizedPath}`);
|
|
74
75
|
return false;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
@@ -79,11 +80,8 @@ export class OxcAnalyzer {
|
|
|
79
80
|
try {
|
|
80
81
|
parsedResult = typeof result === 'string' ? JSON.parse(result) : JSON.parse(JSON.stringify(result));
|
|
81
82
|
} catch (err) {
|
|
82
|
-
if (result && typeof result === 'object')
|
|
83
|
-
|
|
84
|
-
} else {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
83
|
+
if (result && typeof result === 'object') parsedResult = result;
|
|
84
|
+
else return false;
|
|
87
85
|
}
|
|
88
86
|
|
|
89
87
|
let programRoot = null;
|
|
@@ -93,24 +91,35 @@ export class OxcAnalyzer {
|
|
|
93
91
|
else if (parsedResult.type === 'Program' || parsedResult.body) programRoot = parsedResult;
|
|
94
92
|
}
|
|
95
93
|
|
|
96
|
-
// --- VALIDATION CHECK ---
|
|
97
|
-
// If the file has content but OXC returns an empty body, it's a silent failure.
|
|
98
94
|
if (cleanContent.trim().length > 0 && (!programRoot || !programRoot.body || programRoot.body.length === 0)) {
|
|
99
|
-
if (this.context.verbose) {
|
|
100
|
-
console.warn(`[OXC-WARNING] Silent failure detected for ${normalizedPath}. Body is empty despite content. Triggering Fallback...`);
|
|
101
|
-
}
|
|
102
|
-
return false; // This triggers the fallback to TypeScript in the engine
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (!programRoot || !programRoot.body) {
|
|
95
|
+
if (this.context.verbose) console.warn(`[OXC-WARNING] Silent failure detected for ${normalizedPath}. Triggering Fallback...`);
|
|
106
96
|
return false;
|
|
107
97
|
}
|
|
108
98
|
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
if (!programRoot || !programRoot.body) return false;
|
|
100
|
+
|
|
101
|
+
fileNode.ast = programRoot;
|
|
102
|
+
fileNode.symbolTable = new Map();
|
|
103
|
+
|
|
104
|
+
// --- TWO-PASS ANALYSIS ---
|
|
111
105
|
|
|
112
|
-
|
|
113
|
-
this.
|
|
106
|
+
// Pass 1: Declarations
|
|
107
|
+
this.currentScope = { symbols: new Map(), parent: null, children: [] };
|
|
108
|
+
this.scopeStack = [this.currentScope];
|
|
109
|
+
this.scopeCounter = 0;
|
|
110
|
+
this.pass = 1;
|
|
111
|
+
this.walkOxcAst(programRoot, fileNode, cleanContent);
|
|
112
|
+
|
|
113
|
+
// Pass 2: References
|
|
114
|
+
if (this.scopeStack.length > 0) {
|
|
115
|
+
this.currentScope = this.scopeStack[0];
|
|
116
|
+
this.scopeCounter = 0;
|
|
117
|
+
this.pass = 2;
|
|
118
|
+
this.walkOxcAst(programRoot, fileNode, cleanContent);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.scopeStack = [];
|
|
122
|
+
this.currentScope = null;
|
|
114
123
|
|
|
115
124
|
return true;
|
|
116
125
|
} catch (e) {
|
|
@@ -118,9 +127,163 @@ export class OxcAnalyzer {
|
|
|
118
127
|
}
|
|
119
128
|
}
|
|
120
129
|
|
|
121
|
-
|
|
122
|
-
|
|
130
|
+
pushScope() {
|
|
131
|
+
if (this.pass === 1) {
|
|
132
|
+
const newScope = { symbols: new Map(), parent: this.currentScope, children: [] };
|
|
133
|
+
if (this.currentScope) this.currentScope.children.push(newScope);
|
|
134
|
+
this.scopeStack.push(newScope);
|
|
135
|
+
this.currentScope = newScope;
|
|
136
|
+
} else {
|
|
137
|
+
if (this.currentScope && this.currentScope.children) {
|
|
138
|
+
const nextScope = this.currentScope.children[this.scopeCounter++];
|
|
139
|
+
if (nextScope) {
|
|
140
|
+
this.scopeStack.push(nextScope);
|
|
141
|
+
this.currentScope = nextScope;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
popScope() {
|
|
148
|
+
this.scopeStack.pop();
|
|
149
|
+
this.currentScope = this.scopeStack[this.scopeStack.length - 1];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
walkOxcAst(node, fileNode, content) {
|
|
123
153
|
if (!node) return;
|
|
124
|
-
|
|
154
|
+
|
|
155
|
+
const isScopeNode = node.type === 'BlockStatement' || node.type === 'FunctionDeclaration' ||
|
|
156
|
+
node.type === 'ClassDeclaration' || node.type === 'ArrowFunctionExpression';
|
|
157
|
+
|
|
158
|
+
let previousCounter = 0;
|
|
159
|
+
if (isScopeNode) {
|
|
160
|
+
if (this.pass === 2) previousCounter = this.scopeCounter;
|
|
161
|
+
this.scopeCounter = 0;
|
|
162
|
+
this.pushScope();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (this.pass === 1) {
|
|
166
|
+
this.handleNodePass1(node, fileNode, content);
|
|
167
|
+
} else {
|
|
168
|
+
this.handleNodePass2(node, fileNode, content);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Traverse children
|
|
172
|
+
for (const key in node) {
|
|
173
|
+
const child = node[key];
|
|
174
|
+
if (Array.isArray(child)) {
|
|
175
|
+
child.forEach(c => this.walkOxcAst(c, fileNode, content));
|
|
176
|
+
} else if (child && typeof child === 'object' && child.type) {
|
|
177
|
+
this.walkOxcAst(child, fileNode, content);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (isScopeNode) {
|
|
182
|
+
this.popScope();
|
|
183
|
+
if (this.pass === 2) this.scopeCounter = previousCounter + 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
handleNodePass1(node, fileNode, content) {
|
|
188
|
+
switch (node.type) {
|
|
189
|
+
case 'ImportDeclaration':
|
|
190
|
+
this.handleImport(node, fileNode);
|
|
191
|
+
break;
|
|
192
|
+
case 'ExportNamedDeclaration':
|
|
193
|
+
this.handleExportNamed(node, fileNode);
|
|
194
|
+
break;
|
|
195
|
+
case 'ExportDefaultDeclaration':
|
|
196
|
+
fileNode.internalExports.set('default', { type: 'default', start: node.start, end: node.end });
|
|
197
|
+
break;
|
|
198
|
+
case 'ExportAllDeclaration':
|
|
199
|
+
if (node.source) {
|
|
200
|
+
const specifier = node.source.value;
|
|
201
|
+
fileNode.explicitImports.add(specifier);
|
|
202
|
+
fileNode.internalExports.set('*', { type: 're-export-all', source: specifier });
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
handleNodePass2(node, fileNode, content) {
|
|
209
|
+
switch (node.type) {
|
|
210
|
+
case 'Identifier':
|
|
211
|
+
if (!this.isLocalShadowing(node.name)) {
|
|
212
|
+
fileNode.instantiatedIdentifiers.add(node.name);
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
case 'CallExpression':
|
|
216
|
+
this.handleCall(node, fileNode);
|
|
217
|
+
break;
|
|
218
|
+
case 'MemberExpression':
|
|
219
|
+
if (node.property && node.property.name) {
|
|
220
|
+
fileNode.instantiatedIdentifiers.add(node.property.name);
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
handleImport(node, fileNode) {
|
|
227
|
+
if (!node.source) return;
|
|
228
|
+
const specifier = node.source.value;
|
|
229
|
+
fileNode.explicitImports.add(specifier);
|
|
230
|
+
|
|
231
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
232
|
+
const pkgName = this._extractPackageName(specifier);
|
|
233
|
+
let isInternal = false;
|
|
234
|
+
if (this.context.pathMapper?.isTsconfigAlias?.(specifier)) isInternal = true;
|
|
235
|
+
if (!isInternal && this.context.workspaceGraph?.isLocalWorkspaceSpecifier?.(specifier)) isInternal = true;
|
|
236
|
+
if (!isInternal) fileNode.externalPackageUsage.add(pkgName);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
handleExportNamed(node, fileNode) {
|
|
241
|
+
if (node.declaration) {
|
|
242
|
+
const decl = node.declaration;
|
|
243
|
+
const name = decl.id?.name || (decl.declarations?.[0]?.id?.name);
|
|
244
|
+
if (name) {
|
|
245
|
+
fileNode.internalExports.set(name, { type: 'export', start: node.start, end: node.end });
|
|
246
|
+
}
|
|
247
|
+
} else if (node.specifiers) {
|
|
248
|
+
node.specifiers.forEach(spec => {
|
|
249
|
+
const name = spec.exported.name || spec.exported.value;
|
|
250
|
+
fileNode.internalExports.set(name, { type: 'export', start: spec.start, end: spec.end });
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
handleCall(node, fileNode) {
|
|
256
|
+
const callee = node.callee;
|
|
257
|
+
if (callee.type === 'Identifier' && callee.name === 'require') {
|
|
258
|
+
const arg = node.arguments[0];
|
|
259
|
+
if (arg && (arg.type === 'StringLiteral' || arg.type === 'Literal')) {
|
|
260
|
+
const specifier = arg.value;
|
|
261
|
+
fileNode.explicitImports.add(specifier);
|
|
262
|
+
}
|
|
263
|
+
} else if (callee.type === 'Import') {
|
|
264
|
+
const arg = node.arguments[0];
|
|
265
|
+
if (arg && (arg.type === 'StringLiteral' || arg.type === 'Literal')) {
|
|
266
|
+
const specifier = arg.value;
|
|
267
|
+
fileNode.explicitImports.add(specifier);
|
|
268
|
+
fileNode.dynamicImports.add(specifier);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
isLocalShadowing(name) {
|
|
274
|
+
let scope = this.currentScope;
|
|
275
|
+
while (scope) {
|
|
276
|
+
if (scope.symbols.has(name)) return scope.parent !== null;
|
|
277
|
+
scope = scope.parent;
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_extractPackageName(specifier) {
|
|
283
|
+
if (specifier.startsWith('@')) {
|
|
284
|
+
const parts = specifier.split('/');
|
|
285
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
|
|
286
|
+
}
|
|
287
|
+
return specifier.split('/')[0];
|
|
125
288
|
}
|
|
126
289
|
}
|
package/src/index.js
CHANGED
|
@@ -113,22 +113,81 @@ export class RefactoringEngine {
|
|
|
113
113
|
|
|
114
114
|
// Pass 1: Boot environment contexts and load alias configuration maps
|
|
115
115
|
await this.oxcAnalyzer.init();
|
|
116
|
+
// UPGRADE: Wire pathMapper onto context so ASTAnalyzer alias-filtering works in all code paths
|
|
117
|
+
this.context.pathMapper = this.pathMapper;
|
|
116
118
|
await this.pathMapper.loadMappings(this.context.tsconfigFilename);
|
|
117
119
|
|
|
118
120
|
// Always attempt workspace mesh initialization
|
|
119
121
|
console.log(ansis.dim('🌐 Probing for monorepo workspace configuration...'));
|
|
120
122
|
await this.workspaceGraph.initializeWorkspaceMesh();
|
|
123
|
+
// UPGRADE: Always expose workspaceGraph on context (not only when workspace is enabled)
|
|
124
|
+
this.context.workspaceGraph = this.workspaceGraph;
|
|
125
|
+
|
|
126
|
+
// UPGRADE: Scan all discovered framework configs across the workspace EARLY
|
|
127
|
+
const discoveredConfigs = await this.workspaceGraph.findFrameworkConfigs();
|
|
128
|
+
if (discoveredConfigs.length > 0) {
|
|
129
|
+
console.log(ansis.dim(`⚙️ Detected ${discoveredConfigs.length} framework configurations...`));
|
|
130
|
+
const { FrameworkConfigParser } = await import('./resolution/FrameworkConfigParser.js');
|
|
131
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
132
|
+
|
|
133
|
+
for (const configPath of discoveredConfigs) {
|
|
134
|
+
try {
|
|
135
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
136
|
+
|
|
137
|
+
// 1. Core Parsing (Aliases)
|
|
138
|
+
const { aliases } = parser.parse(content, configPath);
|
|
139
|
+
for (const [key, target] of aliases.entries()) {
|
|
140
|
+
this.pathMapper.addAlias(key, target);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 2. Plugin-based Entry Detection
|
|
144
|
+
const detectedEntries = await this.magicDetector.detectEntryPointsFromPlugins(content, configPath);
|
|
145
|
+
for (const entry of detectedEntries) {
|
|
146
|
+
const absEntry = path.resolve(path.dirname(configPath), entry).replace(/\\/g, '/');
|
|
147
|
+
if (!this.context.entryPoints.includes(absEntry)) {
|
|
148
|
+
this.context.entryPoints.push(absEntry);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 3. Register config file itself as entry
|
|
153
|
+
if (!this.context.entryPoints.includes(configPath)) {
|
|
154
|
+
this.context.entryPoints.push(configPath);
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
121
160
|
if (this.context.isWorkspaceEnabled) {
|
|
122
161
|
console.log(ansis.dim('🌐 Monorepo workspace detected – mapping package mesh layers...'));
|
|
123
|
-
|
|
124
|
-
|
|
162
|
+
|
|
163
|
+
// NEW: Register all workspace package entry points
|
|
164
|
+
for (const [dir, manifest] of this.workspaceGraph.packageManifests.entries()) {
|
|
165
|
+
if (manifest.entryPoints && manifest.entryPoints.length > 0) {
|
|
166
|
+
for (const ep of manifest.entryPoints) {
|
|
167
|
+
if (!this.context.entryPoints.includes(ep)) {
|
|
168
|
+
this.context.entryPoints.push(ep);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
125
174
|
// Reload PathMapper aliases now that workspace roots are known
|
|
126
175
|
await this.pathMapper.loadMappings(this.context.tsconfigFilename);
|
|
127
176
|
if (this.context.verbose) {
|
|
128
|
-
|
|
177
|
+
const wsSummary = this.workspaceGraph.getSummary();
|
|
178
|
+
console.log(`[Workspace] Found ${wsSummary.packages} workspace packages:`);
|
|
129
179
|
for (const [dir, manifest] of this.workspaceGraph.packageManifests.entries()) {
|
|
130
180
|
console.log(ansis.dim(` • ${manifest.name || dir}`));
|
|
131
181
|
}
|
|
182
|
+
if (wsSummary.tsconfigsLoaded > 0) {
|
|
183
|
+
console.log(ansis.dim(`[Workspace] Loaded ${wsSummary.tsconfigsLoaded} tsconfig.json file(s) from workspace packages`));
|
|
184
|
+
}
|
|
185
|
+
if (wsSummary.configFilesLoaded > 0) {
|
|
186
|
+
console.log(ansis.dim(`[Workspace] Loaded ${wsSummary.configFilesLoaded} *.config.ts/js file(s) from workspace packages`));
|
|
187
|
+
}
|
|
188
|
+
if (wsSummary.monorepoConfigs && wsSummary.monorepoConfigs.length > 0) {
|
|
189
|
+
console.log(ansis.dim(`[Workspace] Detected global configs: ${wsSummary.monorepoConfigs.join(', ')}`));
|
|
190
|
+
}
|
|
132
191
|
}
|
|
133
192
|
}
|
|
134
193
|
|
|
@@ -200,6 +259,14 @@ export class RefactoringEngine {
|
|
|
200
259
|
for (const filePath of sourceCodeFilesList) {
|
|
201
260
|
const absFilePath = slashifyInternal(filePath);
|
|
202
261
|
const node = this.context.getOrCreateNode(absFilePath);
|
|
262
|
+
|
|
263
|
+
// Config data is now handled early in the workspace mesh initialization phase.
|
|
264
|
+
// We still mark the rawCode for plugin analysis.
|
|
265
|
+
if (absFilePath.includes('.config.')) {
|
|
266
|
+
try {
|
|
267
|
+
node.rawCode = await fs.readFile(absFilePath, 'utf8');
|
|
268
|
+
} catch (e) {}
|
|
269
|
+
}
|
|
203
270
|
const currentHash = await this.cacheManager.computeHash(absFilePath);
|
|
204
271
|
node.contentHash = currentHash;
|
|
205
272
|
|
|
@@ -227,6 +294,7 @@ export class RefactoringEngine {
|
|
|
227
294
|
} else if (!parallelParseCompleted) {
|
|
228
295
|
this.context.metrics.cacheMisses++;
|
|
229
296
|
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
297
|
+
node.rawCode = fileContent; // UPGRADE: Store raw code for plugin analysis
|
|
230
298
|
|
|
231
299
|
let success = false;
|
|
232
300
|
if (this.oxcAnalyzer.isAvailable) {
|
|
@@ -248,6 +316,10 @@ export class RefactoringEngine {
|
|
|
248
316
|
if (!success || (oxcFailedToFindDependencies && (hasImportExportKeywords || hasCommonJSKeywords))) {
|
|
249
317
|
await this.analyzer.parseFile(filePath, fileContent, node);
|
|
250
318
|
}
|
|
319
|
+
|
|
320
|
+
// UPGRADE 5.3.0: Run plugin-specific content analysis
|
|
321
|
+
await this.magicDetector.runPluginContentAnalysis(node, absFilePath);
|
|
322
|
+
|
|
251
323
|
// Secret scan on freshly parsed content
|
|
252
324
|
const secretFindings = this.secretScanner.scanFileContent(filePath, fileContent);
|
|
253
325
|
if (secretFindings.length > 0) {
|
|
@@ -562,6 +634,17 @@ export class RefactoringEngine {
|
|
|
562
634
|
const nodeBuiltins = ['assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console', 'constants', 'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'http2', 'https', 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks', 'process', 'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'timers', 'tls', 'trace_events', 'tty', 'url', 'util', 'v8', 'vm', 'worker_threads', 'zlib'];
|
|
563
635
|
if (nodeBuiltins.includes(basePkg) || nodeBuiltins.includes(basePkg.replace('node:', ''))) return;
|
|
564
636
|
|
|
637
|
+
// UPGRADE: Skip tsconfig path aliases (e.g. @shared/*, @idk/*, ~/*)
|
|
638
|
+
// Check full specifier AND basePkg against alias patterns
|
|
639
|
+
if (this.pathMapper && typeof this.pathMapper.isTsconfigAlias === 'function') {
|
|
640
|
+
if (this.pathMapper.isTsconfigAlias(specifier) || this.pathMapper.isTsconfigAlias(basePkg)) return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// UPGRADE: Skip workspace packages detected via pnpm-workspace.yaml / package.json workspaces
|
|
644
|
+
if (this.workspaceGraph && typeof this.workspaceGraph.isLocalWorkspaceSpecifier === 'function') {
|
|
645
|
+
if (this.workspaceGraph.isLocalWorkspaceSpecifier(specifier) || this.workspaceGraph.isLocalWorkspaceSpecifier(basePkg)) return;
|
|
646
|
+
}
|
|
647
|
+
|
|
565
648
|
if (!localDeps.has(basePkg)) {
|
|
566
649
|
const alreadyFlagged = this.context.unlistedDependencies.some(u => u.package === basePkg && u.file === filePath);
|
|
567
650
|
if (!alreadyFlagged) {
|
|
@@ -574,7 +657,7 @@ export class RefactoringEngine {
|
|
|
574
657
|
}
|
|
575
658
|
});
|
|
576
659
|
} catch (error) {
|
|
577
|
-
if (this.context.options.verbose) {
|
|
660
|
+
if (this.context.options.verbose && error.code !== 'ENOENT') {
|
|
578
661
|
console.error(ansis.red(` ❌ Manifest Parsing Exception: ${error.message}`));
|
|
579
662
|
}
|
|
580
663
|
}
|
|
@@ -59,6 +59,33 @@ export class IncrementalCacheManager {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* UPGRADE 5.4.3: Affected-Only Analysis
|
|
64
|
+
* Identifies which files need re-analysis based on Git changes or cache misses.
|
|
65
|
+
*/
|
|
66
|
+
async getAffectedFiles(allFiles) {
|
|
67
|
+
const manifest = await this.loadCacheManifest();
|
|
68
|
+
const affected = [];
|
|
69
|
+
const unchanged = [];
|
|
70
|
+
|
|
71
|
+
for (const file of allFiles) {
|
|
72
|
+
const cached = manifest[file];
|
|
73
|
+
if (!cached) {
|
|
74
|
+
affected.push(file);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const currentHash = await this.computeHash(file);
|
|
79
|
+
if (currentHash !== cached.hash) {
|
|
80
|
+
affected.push(file);
|
|
81
|
+
} else {
|
|
82
|
+
unchanged.push({ path: file, data: cached });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { affected, unchanged };
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
/**
|
|
63
90
|
* Serializes the current active dependency graph, translating Maps and Sets into JSON schemas.
|
|
64
91
|
* @param {Map<string, Object>} currentGraphState - In-memory structural project state map
|
|
@@ -47,10 +47,11 @@ export class WorkerPool {
|
|
|
47
47
|
|
|
48
48
|
try {
|
|
49
49
|
const analyticalResultsSubsets = await Promise.all(threadTaskExecutionsList);
|
|
50
|
+
const flatResults = analyticalResultsSubsets.flat();
|
|
50
51
|
|
|
51
52
|
// Merge thread structural subsets back into the primary context graph nodes
|
|
52
|
-
|
|
53
|
-
if (!result)
|
|
53
|
+
for (const result of flatResults) {
|
|
54
|
+
if (!result) continue;
|
|
54
55
|
// UPGRADE: Normalize filePath before merging to prevent duplicate nodes in the graph
|
|
55
56
|
const normalizedPath = path.resolve(this.context.cwd, result.filePath).replace(/\\/g, '/');
|
|
56
57
|
const node = masterEngineInstanceReference.context.getOrCreateNode(normalizedPath);
|
|
@@ -95,7 +96,13 @@ export class WorkerPool {
|
|
|
95
96
|
if (!node.globImports) node.globImports = [];
|
|
96
97
|
result.globImports.forEach(i => node.globImports.push(i));
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
+
|
|
100
|
+
// UPGRADE 5.3.0: Run plugin-specific content analysis for worker-parsed files
|
|
101
|
+
if (result.rawCode) {
|
|
102
|
+
node.rawCode = result.rawCode;
|
|
103
|
+
await masterEngineInstanceReference.magicDetector.runPluginContentAnalysis(node, normalizedPath);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
99
106
|
|
|
100
107
|
return true;
|
|
101
108
|
} catch (poolThreadFault) {
|
|
@@ -107,13 +114,24 @@ export class WorkerPool {
|
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
executeChunkInsideThread(fileChunkSubset) {
|
|
117
|
+
// UPGRADE: Serialize alias patterns and workspace package names so workers can filter false positives
|
|
118
|
+
const serializedAliasPatterns = (this.context.pathMapper && this.context.pathMapper._aliasPatterns)
|
|
119
|
+
? this.context.pathMapper._aliasPatterns.map(p => p.regex.source)
|
|
120
|
+
: [];
|
|
121
|
+
const workspacePackageNames = (this.context.workspaceGraph && this.context.workspaceGraph.workspacePackages)
|
|
122
|
+
? Array.from(this.context.workspaceGraph.workspacePackages.keys())
|
|
123
|
+
: [];
|
|
124
|
+
|
|
110
125
|
return new Promise((resolve, reject) => {
|
|
111
126
|
const workerInstance = new Worker(this.workerScriptPath, { type: 'module',
|
|
112
127
|
workerData: {
|
|
113
128
|
files: fileChunkSubset,
|
|
114
129
|
contextOptions: {
|
|
115
130
|
verbose: this.context.verbose,
|
|
116
|
-
cwd: this.context.cwd
|
|
131
|
+
cwd: this.context.cwd,
|
|
132
|
+
// UPGRADE: Pass alias/workspace data so workers can filter false positives
|
|
133
|
+
aliasPatternSources: serializedAliasPatterns,
|
|
134
|
+
workspacePackageNames
|
|
117
135
|
}
|
|
118
136
|
}
|
|
119
137
|
});
|
|
@@ -12,11 +12,35 @@ async function runTask() {
|
|
|
12
12
|
const { files, contextOptions } = workerData;
|
|
13
13
|
const results = [];
|
|
14
14
|
|
|
15
|
+
// UPGRADE: Reconstruct alias/workspace filter helpers from serialized contextOptions
|
|
16
|
+
// These are passed from WorkerPool.executeChunkInsideThread so workers can skip false positives
|
|
17
|
+
const aliasRegexes = (contextOptions.aliasPatternSources || []).map(src => {
|
|
18
|
+
try { return new RegExp(src); } catch { return null; }
|
|
19
|
+
}).filter(Boolean);
|
|
20
|
+
const workspacePackageSet = new Set(contextOptions.workspacePackageNames || []);
|
|
21
|
+
|
|
22
|
+
// Lightweight helpers mirroring PathMapper.isTsconfigAlias and WorkspaceGraph.isLocalWorkspaceSpecifier
|
|
23
|
+
const isTsconfigAlias = (spec) => aliasRegexes.some(r => r.test(spec));
|
|
24
|
+
const isWorkspacePkg = (spec) => {
|
|
25
|
+
if (workspacePackageSet.has(spec)) return true;
|
|
26
|
+
for (const name of workspacePackageSet) { if (spec.startsWith(name + '/')) return true; }
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
|
|
15
30
|
// Create a minimal context for analyzers
|
|
16
31
|
const mockContext = {
|
|
17
32
|
verbose: contextOptions.verbose,
|
|
18
33
|
cwd: contextOptions.cwd || process.cwd(),
|
|
19
34
|
projectGraph: new Map(),
|
|
35
|
+
// UPGRADE: Expose lightweight alias/workspace helpers so ASTAnalyzer can filter imports
|
|
36
|
+
pathMapper: {
|
|
37
|
+
isTsconfigAlias,
|
|
38
|
+
_aliasPatterns: aliasRegexes.map(r => ({ regex: r }))
|
|
39
|
+
},
|
|
40
|
+
workspaceGraph: {
|
|
41
|
+
isLocalWorkspaceSpecifier: isWorkspacePkg,
|
|
42
|
+
workspacePackages: workspacePackageSet
|
|
43
|
+
},
|
|
20
44
|
getOrCreateNode: (path) => ({
|
|
21
45
|
filePath: path,
|
|
22
46
|
explicitImports: new Set(),
|
|
@@ -50,6 +74,7 @@ async function runTask() {
|
|
|
50
74
|
try {
|
|
51
75
|
const content = await fs.readFile(filePath, 'utf8');
|
|
52
76
|
const node = mockContext.getOrCreateNode(filePath);
|
|
77
|
+
node.rawCode = content; // UPGRADE: Add raw code to node for plugin analysis in main thread
|
|
53
78
|
|
|
54
79
|
// Defensiver Check: Sicherstellen, dass die Map/Set-Instanzen sauber existieren
|
|
55
80
|
if (!(node.internalExports instanceof Map)) node.internalExports = new Map();
|
|
@@ -106,6 +131,7 @@ async function runTask() {
|
|
|
106
131
|
// Serialize the node data for transfer back to main thread
|
|
107
132
|
results.push({
|
|
108
133
|
filePath: node.filePath,
|
|
134
|
+
rawCode: node.rawCode, // UPGRADE: Pass raw code back to main thread for plugin analysis
|
|
109
135
|
explicitImports: Array.from(node.explicitImports || []),
|
|
110
136
|
dynamicImports: Array.from(node.dynamicImports || []),
|
|
111
137
|
importedSymbols: Array.from(node.importedSymbols || []),
|
|
@@ -5,7 +5,7 @@ import path from 'path';
|
|
|
5
5
|
* Base class for all entkapp plugins.
|
|
6
6
|
* Defines the contract for ecosystem detection, entry point mapping,
|
|
7
7
|
* and missing dependency / devDependency detection.
|
|
8
|
-
* Version 5.
|
|
8
|
+
* Version 5.4.0: Added detectEntryPoints() for dynamic framework entry detection.
|
|
9
9
|
*/
|
|
10
10
|
export class BasePlugin {
|
|
11
11
|
constructor(context) {
|
|
@@ -34,6 +34,17 @@ export class BasePlugin {
|
|
|
34
34
|
return [];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Version 5.4.0: Dynamically detect entry points from a file's content.
|
|
39
|
+
* Useful for parsing vite.config.ts, webpack.config.js, etc.
|
|
40
|
+
* @param {string} content - The file content
|
|
41
|
+
* @param {string} filePath - The absolute path to the file
|
|
42
|
+
* @returns {Array<string>} List of relative or absolute entry point paths
|
|
43
|
+
*/
|
|
44
|
+
detectEntryPoints(content, filePath) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
/**
|
|
38
49
|
* Returns symbols that are implicitly required/exported by the framework.
|
|
39
50
|
*/
|
|
@@ -43,25 +54,13 @@ export class BasePlugin {
|
|
|
43
54
|
|
|
44
55
|
/**
|
|
45
56
|
* Returns the npm package names required for this plugin to work.
|
|
46
|
-
* Each entry is either a string or an object:
|
|
47
|
-
* { name: string, dev?: boolean, optional?: boolean }
|
|
48
|
-
* - dev: true => expected in devDependencies
|
|
49
|
-
* - dev: false => expected in dependencies
|
|
50
|
-
* - optional => only warn, not error
|
|
51
|
-
*
|
|
52
|
-
* Override in subclasses to enable automatic dependency checking.
|
|
53
|
-
* @returns {Array<string | {name: string, dev?: boolean, optional?: boolean}>}
|
|
54
57
|
*/
|
|
55
58
|
getRequiredPackages() {
|
|
56
59
|
return [];
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
/**
|
|
60
|
-
*
|
|
61
|
-
* Returns an array of diagnostic objects describing missing or misplaced dependencies.
|
|
62
|
-
*
|
|
63
|
-
* @param {string} baseDir - The project root directory
|
|
64
|
-
* @returns {Promise<Array>}
|
|
63
|
+
* Checks whether all required packages are present in package.json.
|
|
65
64
|
*/
|
|
66
65
|
async getMissingDependencies(baseDir) {
|
|
67
66
|
const diagnostics = [];
|
|
@@ -147,11 +146,7 @@ export class BasePlugin {
|
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
/**
|
|
150
|
-
*
|
|
151
|
-
* Detects "orphaned" configs – e.g. .prettierrc exists but prettier is not installed.
|
|
152
|
-
*
|
|
153
|
-
* @param {string} baseDir
|
|
154
|
-
* @returns {Promise<Array>}
|
|
149
|
+
* Checks for config files that exist without the corresponding package.
|
|
155
150
|
*/
|
|
156
151
|
async getOrphanedConfigs(baseDir) {
|
|
157
152
|
const diagnostics = [];
|
|
@@ -195,11 +190,7 @@ export class BasePlugin {
|
|
|
195
190
|
}
|
|
196
191
|
|
|
197
192
|
/**
|
|
198
|
-
*
|
|
199
|
-
* Convenience method combining getMissingDependencies and getOrphanedConfigs.
|
|
200
|
-
*
|
|
201
|
-
* @param {string} baseDir
|
|
202
|
-
* @returns {Promise<Array>}
|
|
193
|
+
* Run all dependency diagnostics (missing + orphaned).
|
|
203
194
|
*/
|
|
204
195
|
async runDependencyDiagnostics(baseDir) {
|
|
205
196
|
const [missing, orphaned] = await Promise.all([
|
|
@@ -210,9 +201,7 @@ export class BasePlugin {
|
|
|
210
201
|
}
|
|
211
202
|
|
|
212
203
|
/**
|
|
213
|
-
*
|
|
214
|
-
* @param {string} key - The property key to retrieve
|
|
215
|
-
* @returns {any} The value of the custom property
|
|
204
|
+
* Dynamic getter for custom plugin properties.
|
|
216
205
|
*/
|
|
217
206
|
get(key) {
|
|
218
207
|
const methodName = `get${key.charAt(0).toUpperCase() + key.slice(1)}`;
|