entkapp 4.4.0 → 4.5.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/bin/cli.js +4 -2
- package/package.json +1 -1
- package/src/EngineContext.js +8 -1
- package/src/ast/AdvancedAnalysis.js +518 -89
- package/src/ast/DeadCodeDetector.js +5 -0
- package/src/ast/OxcAnalyzer.js +77 -22
- package/src/index.js +213 -29
- package/src/performance/GraphCache.js +3 -1
- package/src/performance/WorkerPool.js +10 -1
- package/src/performance/WorkerTaskRunner.js +10 -3
- package/src/resolution/DepencyResolver.js +3 -4
|
@@ -62,6 +62,11 @@ export class DeadCodeDetector {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Additional check for library entries or index files
|
|
66
|
+
if (!isUsed && (node.isLibraryEntry || filePath.toLowerCase().includes('index'))) {
|
|
67
|
+
isUsed = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
if (!isUsed) {
|
|
66
71
|
deadExports.push({ file: filePath, symbol: exportName, line: exportInfo.start });
|
|
67
72
|
}
|
package/src/ast/OxcAnalyzer.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
1
3
|
export class OxcAnalyzer {
|
|
2
4
|
constructor(context) {
|
|
3
5
|
this.context = context;
|
|
@@ -8,8 +10,7 @@ export class OxcAnalyzer {
|
|
|
8
10
|
async init() {
|
|
9
11
|
if (this.isAvailable) return true;
|
|
10
12
|
try {
|
|
11
|
-
|
|
12
|
-
this.oxc = oxc;
|
|
13
|
+
this.oxc = await import("oxc-parser");
|
|
13
14
|
this.isAvailable = true;
|
|
14
15
|
return true;
|
|
15
16
|
} catch (e) {
|
|
@@ -36,42 +37,55 @@ export class OxcAnalyzer {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
try {
|
|
39
|
-
// Fix:
|
|
40
|
-
const
|
|
40
|
+
// Fix: Remove BOM if present (confuses some Rust parsers)
|
|
41
|
+
const cleanContent = content.startsWith('\uFEFF') ? content.slice(1) : content;
|
|
41
42
|
|
|
42
|
-
//
|
|
43
|
-
|
|
43
|
+
// Fix: Use absolute path with forward slashes for OXC (most stable for Rust on Windows)
|
|
44
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
45
|
+
const normalizedContent = cleanContent.replace(/\/([gimuy]*)([nh]+)([gimuy]*)/g, (match, p1, p2, p3) => {
|
|
46
|
+
if (this.context.verbose) console.log(`[OXC] Normalizing regex flag: ${p2} at ${filePath}`);
|
|
47
|
+
return `/${p1}${p3}`;
|
|
48
|
+
});
|
|
44
49
|
let result;
|
|
45
50
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
lang: "typescript"
|
|
50
|
-
});
|
|
51
|
+
// ONLY USE FLAT SIGNATURE: parseSync(sourceText, sourceFilename)
|
|
52
|
+
// This avoids the "rust type String" conversion error seen with configuration objects.
|
|
53
|
+
result = this.oxc.parseSync(normalizedContent, normalizedPath);
|
|
51
54
|
} catch (e) {
|
|
52
|
-
//
|
|
53
|
-
|
|
55
|
+
// Silent fallback to TS Compiler if OXC fails
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (this.context.verbose) {
|
|
60
|
+
console.log(`[OXC] Parsed ${filePath} using flat signature`);
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
// Fix: Handle cases where OXC returns a JSON string instead of an object
|
|
57
64
|
// Stabilize result through JSON round-trip to fix N-API conversion issues on Windows
|
|
58
|
-
|
|
65
|
+
let parsedResult;
|
|
59
66
|
try {
|
|
60
67
|
parsedResult = typeof result === 'string' ? JSON.parse(result) : JSON.parse(JSON.stringify(result));
|
|
61
68
|
} catch (err) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
69
|
+
parsedResult = result;
|
|
70
|
+
}
|
|
71
|
+
// REPORT OXC ERRORS: If the parser found syntax errors, show them in verbose mode
|
|
72
|
+
if (this.context.verbose && parsedResult.errors && parsedResult.errors.length > 0) {
|
|
73
|
+
console.log(`[OXC] ❌ Parser reported ${parsedResult.errors.length} errors for ${normalizedPath}:`);
|
|
74
|
+
parsedResult.errors.forEach(err => console.log(` - ${err.message || err}`));
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
let ast;
|
|
77
|
+
let ast = (parsedResult && parsedResult.program) ? parsedResult : { program: parsedResult };
|
|
78
|
+
fileNode.ast = ast.program;
|
|
79
|
+
fileNode.jsxComponents = new Set();
|
|
80
|
+
fileNode.jsxProps = new Set();
|
|
81
|
+
fileNode.decorators = new Set();
|
|
70
82
|
if (parsedResult && typeof parsedResult === 'object') {
|
|
71
83
|
if (parsedResult.program) {
|
|
72
84
|
ast = parsedResult;
|
|
73
85
|
} else if (parsedResult.ast) {
|
|
74
86
|
ast = { program: parsedResult.ast };
|
|
87
|
+
} else if (parsedResult.body || parsedResult.type === 'Program') {
|
|
88
|
+
ast = { program: parsedResult };
|
|
75
89
|
} else {
|
|
76
90
|
ast = { program: parsedResult };
|
|
77
91
|
}
|
|
@@ -84,8 +98,45 @@ export class OxcAnalyzer {
|
|
|
84
98
|
fileNode.jsxProps = new Set();
|
|
85
99
|
fileNode.decorators = new Set();
|
|
86
100
|
|
|
101
|
+
if (this.context.verbose) {
|
|
102
|
+
console.log(`[OXC] Analyzing ${filePath}`);
|
|
103
|
+
}
|
|
87
104
|
this.walkOxcAst(ast.program, fileNode, content);
|
|
88
105
|
|
|
106
|
+
// 7. Success confirmation
|
|
107
|
+
if (this.context.verbose) {
|
|
108
|
+
console.log(`[OXC] Successfully parsed and analyzed ${filePath}`);
|
|
109
|
+
}
|
|
110
|
+
// Fallback: If OXC found 0 imports but the file has content that looks like it has imports,
|
|
111
|
+
// return false to trigger the TS Compiler API fallback. This is a safety anchor.
|
|
112
|
+
if (fileNode.explicitImports.size === 0 && (content.includes('import') || content.includes('export')) && content.length > 50) {
|
|
113
|
+
if (this.context.verbose) {
|
|
114
|
+
console.log(`[OXC] ⚠️ Parser yielded 0 imports/exports for ${filePath}. Engaging TS Compiler fallback for accuracy.`);
|
|
115
|
+
|
|
116
|
+
// DEEP DIAGNOSTICS: Inspect the first few nodes of the AST to see what OXC is actually producing
|
|
117
|
+
if (ast.program && ast.program.body && ast.program.body.length > 0) {
|
|
118
|
+
console.log(`[OXC-DEBUG] AST Program Body Length: ${ast.program.body.length}`);
|
|
119
|
+
const firstNodes = ast.program.body.slice(0, 3).map(n => ({
|
|
120
|
+
type: n.type,
|
|
121
|
+
hasSource: !!n.source,
|
|
122
|
+
hasDeclaration: !!n.declaration,
|
|
123
|
+
specifiersCount: n.specifiers ? n.specifiers.length : 0
|
|
124
|
+
}));
|
|
125
|
+
console.log(`[OXC-DEBUG] First 3 Nodes structure: ${JSON.stringify(firstNodes, null, 2)}`);
|
|
126
|
+
} else {
|
|
127
|
+
console.log(`[OXC-DEBUG] AST Program Body is EMPTY or UNDEFINED.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (this.context.verbose) {
|
|
134
|
+
console.log(`[OXC] Found ${fileNode.explicitImports.size} imports in ${filePath}`);
|
|
135
|
+
if (fileNode.explicitImports.size > 0) {
|
|
136
|
+
console.log(`[OXC] Imports for ${filePath}: ${Array.from(fileNode.explicitImports).join(', ')}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
89
140
|
const lines = content.split('\n');
|
|
90
141
|
const getLineCol = (pos) => {
|
|
91
142
|
let count = 0;
|
|
@@ -154,6 +205,7 @@ export class OxcAnalyzer {
|
|
|
154
205
|
}
|
|
155
206
|
|
|
156
207
|
handleImportDeclaration(node, fileNode) {
|
|
208
|
+
if (!node.source || typeof node.source.value !== 'string') return;
|
|
157
209
|
const specifier = node.source.value;
|
|
158
210
|
fileNode.explicitImports.add(specifier);
|
|
159
211
|
|
|
@@ -164,8 +216,11 @@ export class OxcAnalyzer {
|
|
|
164
216
|
if (node.specifiers) {
|
|
165
217
|
node.specifiers.forEach((spec) => {
|
|
166
218
|
if (spec.type === "ImportSpecifier") {
|
|
167
|
-
|
|
168
|
-
|
|
219
|
+
// In OXC, imported name can be in .imported.name or .imported.value
|
|
220
|
+
const importedName = spec.imported.name || spec.imported.value || (spec.imported.type === "Identifier" ? spec.imported.name : null);
|
|
221
|
+
if (importedName) {
|
|
222
|
+
fileNode.importedSymbols.add(`${specifier}:${importedName}`);
|
|
223
|
+
}
|
|
169
224
|
} else if (spec.type === "ImportDefaultSpecifier") {
|
|
170
225
|
fileNode.importedSymbols.add(`${specifier}:default`);
|
|
171
226
|
} else if (spec.type === "ImportNamespaceSpecifier") {
|
package/src/index.js
CHANGED
|
@@ -134,7 +134,7 @@ export class RefactoringEngine {
|
|
|
134
134
|
|
|
135
135
|
// Pass 3: Process source file tokens using high-performance concurrent workers
|
|
136
136
|
let parallelParseCompleted = false;
|
|
137
|
-
if (sourceCodeFilesList.length >
|
|
137
|
+
if (sourceCodeFilesList.length > 1) {
|
|
138
138
|
parallelParseCompleted = await this.workerPool.parallelAnalyzeCodebase(sourceCodeFilesList, this);
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -176,9 +176,12 @@ export class RefactoringEngine {
|
|
|
176
176
|
await this.magicDetector.injectVirtualConsumerEdges(filePath, node, activeFrameworkEcosystems);
|
|
177
177
|
|
|
178
178
|
// Fix: Explicitly protect entry points defined in local configuration
|
|
179
|
+
const slashify = (p) => path.resolve(this.context.cwd, p).replace(/\\/g, '/');
|
|
179
180
|
if (this.context.entryPoints && this.context.entryPoints.some(ep => {
|
|
180
|
-
const absEp =
|
|
181
|
-
|
|
181
|
+
const absEp = slashify(ep);
|
|
182
|
+
const cleanAbsEp = absEp.replace(/\.[^/.]+$/, "");
|
|
183
|
+
const cleanFilePath = slashify(filePath).replace(/\.[^/.]+$/, "");
|
|
184
|
+
return absEp === slashify(filePath) || cleanAbsEp === cleanFilePath;
|
|
182
185
|
})) {
|
|
183
186
|
node.isLibraryEntry = true;
|
|
184
187
|
}
|
|
@@ -220,7 +223,18 @@ export class RefactoringEngine {
|
|
|
220
223
|
|
|
221
224
|
// Pass 4: Evaluate graph edges and link connections across the codebase mesh
|
|
222
225
|
console.log(ansis.dim('🔗 Linking graph edges and checking structural usage paths...'));
|
|
226
|
+
if (this.context.verbose) {
|
|
227
|
+
console.log(`[Linker] Starting dependency graph linkage for ${this.context.projectGraph.size} nodes.`);
|
|
228
|
+
}
|
|
223
229
|
await this.linkDependencyGraph();
|
|
230
|
+
if (this.context.verbose) {
|
|
231
|
+
const totalEdges = Array.from(this.context.projectGraph.values()).reduce((sum, node) => sum + node.outgoingEdges.size, 0);
|
|
232
|
+
console.log(`[Linker] Completed linkage. Total edges created: ${totalEdges}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (this.context.options.visualize) {
|
|
236
|
+
await this._generateVisualization(this.context.projectGraph);
|
|
237
|
+
}
|
|
224
238
|
|
|
225
239
|
// NEW: Circular Dependency Detection
|
|
226
240
|
console.log(ansis.dim('🔄 Detecting circular dependencies...'));
|
|
@@ -243,37 +257,37 @@ export class RefactoringEngine {
|
|
|
243
257
|
// In a real scenario, we'd need the actual AST object here.
|
|
244
258
|
const ast = fileNode.ast || {}; // Assuming AST is stored in fileNode
|
|
245
259
|
this.advancedAnalysis.buildCFG(filePath, ast);
|
|
246
|
-
this.advancedAnalysis.handleComputedExports(fileNode, ast);
|
|
247
|
-
const unreachableCode = this.advancedAnalysis.analyzeReachability(filePath);
|
|
248
|
-
if (unreachableCode.length > 0) {
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
const taintFindings = this.advancedAnalysis.performTaintAnalysis(filePath, ast);
|
|
252
|
-
if (taintFindings.length > 0) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
260
|
+
// this.advancedAnalysis.handleComputedExports(fileNode, ast);
|
|
261
|
+
// const unreachableCode = this.advancedAnalysis.analyzeReachability(filePath);
|
|
262
|
+
// if (unreachableCode.length > 0) {
|
|
263
|
+
// console.log(ansis.yellow(` ⚠️ Unreachable code detected in ${path.relative(this.context.cwd, filePath)}: ${unreachableCode.length} blocks`));
|
|
264
|
+
// }
|
|
265
|
+
// const taintFindings = this.advancedAnalysis.performTaintAnalysis(filePath, ast);
|
|
266
|
+
// if (taintFindings.length > 0) {
|
|
267
|
+
// console.log(ansis.red(` 🚨 Taint violations detected in ${path.relative(this.context.cwd, filePath)}: ${taintFindings.length} findings`));
|
|
268
|
+
// taintFindings.forEach(f => console.log(ansis.dim(` • ${f.type} at ${f.file}:${f.line} (Sink: ${f.sink})`)));
|
|
269
|
+
// this.context.allSecretFindings.push(...taintFindings); // Add taint findings to overall secrets
|
|
270
|
+
// }
|
|
257
271
|
|
|
258
272
|
// Detect unused members
|
|
259
|
-
const unusedMembers = this.advancedAnalysis.detectUnusedMembers(fileNode);
|
|
260
|
-
if (unusedMembers.length > 0) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
273
|
+
// const unusedMembers = this.advancedAnalysis.detectUnusedMembers(fileNode);
|
|
274
|
+
// if (unusedMembers.length > 0) {
|
|
275
|
+
// console.log(ansis.yellow(` ⚠️ Unused members detected in ${path.relative(this.context.cwd, filePath)}:`));
|
|
276
|
+
// unusedMembers.forEach(m => console.log(ansis.dim(` • ${m.member}: ${m.message}`)));
|
|
277
|
+
// }
|
|
264
278
|
|
|
265
279
|
// NEW: Type-Jail Analysis
|
|
266
|
-
const typeJailViolations = this.workspaceDiagnostic.analyzeTypeJail(fileNode);
|
|
267
|
-
if (typeJailViolations.length > 0) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
280
|
+
// const typeJailViolations = this.workspaceDiagnostic.analyzeTypeJail(fileNode);
|
|
281
|
+
// if (typeJailViolations.length > 0) {
|
|
282
|
+
// console.log(ansis.yellow(` ⚠️ Type-Jail violations in ${path.relative(this.context.cwd, filePath)}:`));
|
|
283
|
+
// typeJailViolations.forEach(v => console.log(ansis.dim(` • ${v.message}`)));
|
|
284
|
+
// }
|
|
271
285
|
|
|
272
286
|
// NEW: Ghost Code Detection
|
|
273
|
-
const ghostExports = this.advancedAnalysis.detectGhostCode(fileNode, this.context.projectGraph);
|
|
274
|
-
if (ghostExports.length > 0) {
|
|
275
|
-
|
|
276
|
-
}
|
|
287
|
+
// const ghostExports = this.advancedAnalysis.detectGhostCode(fileNode, this.context.projectGraph);
|
|
288
|
+
// if (ghostExports.length > 0) {
|
|
289
|
+
// console.log(ansis.yellow(` 👻 Ghost exports detected in ${path.relative(this.context.cwd, filePath)}: [${ghostExports.join(', ')}]`));
|
|
290
|
+
// }
|
|
277
291
|
}
|
|
278
292
|
|
|
279
293
|
// NEW: Workspace Diagnostic & Architecture Enforcement
|
|
@@ -578,6 +592,60 @@ export class RefactoringEngine {
|
|
|
578
592
|
}
|
|
579
593
|
analysisSummary.unlistedDependencies = this.context.unlistedDependencies || [];
|
|
580
594
|
|
|
595
|
+
// NEW: Advanced Program Analysis v4.5.0 (JIT, Topology, SAST)
|
|
596
|
+
const advancedResults = this.advancedAnalysis.runAll(this.context.projectGraph, this.workspaceGraph.packageManifests);
|
|
597
|
+
|
|
598
|
+
if (this.context.options.verbose) {
|
|
599
|
+
if (advancedResults.jitWarnings.length > 0) {
|
|
600
|
+
console.log(ansis.bold.yellow(`\n⚡ JIT Optimization Warnings (${advancedResults.jitWarnings.length}):`));
|
|
601
|
+
advancedResults.jitWarnings.forEach(w => console.log(ansis.yellow(` • [${w.type}] ${w.message} (${w.file}:${w.line})`)));
|
|
602
|
+
}
|
|
603
|
+
if (advancedResults.securityFindings.length > 0) {
|
|
604
|
+
console.log(ansis.bold.red(`\n🛡️ Security Vulnerabilities (${advancedResults.securityFindings.length}):`));
|
|
605
|
+
advancedResults.securityFindings.forEach(w => console.log(ansis.red(` • [${w.type}] ${w.message} (${w.file}:${w.line})`)));
|
|
606
|
+
}
|
|
607
|
+
if (advancedResults.topologyWarnings.length > 0) {
|
|
608
|
+
console.log(ansis.bold.blue(`\n🌐 Topology & Reachability Warnings (${advancedResults.topologyWarnings.length}):`));
|
|
609
|
+
advancedResults.topologyWarnings.forEach(w => console.log(ansis.blue(` • [${w.type}] ${w.message} (${w.file})`)));
|
|
610
|
+
}
|
|
611
|
+
if (advancedResults.integrityWarnings.length > 0) {
|
|
612
|
+
console.log(ansis.bold.cyan(`\n📊 Config Integrity Checks (${advancedResults.integrityWarnings.length}):`));
|
|
613
|
+
advancedResults.integrityWarnings.forEach(w => console.log(ansis.cyan(` • [${w.type}] ${w.message} (${w.file})`)));
|
|
614
|
+
}
|
|
615
|
+
if (advancedResults.leakWarnings.length > 0) {
|
|
616
|
+
console.log(ansis.bold.magenta(`\n💧 Event Leak Risk Analysis (${advancedResults.leakWarnings.length}):`));
|
|
617
|
+
advancedResults.leakWarnings.forEach(w => console.log(ansis.magenta(` • [${w.type}] ${w.message} (${w.file}:${w.line})`)));
|
|
618
|
+
}
|
|
619
|
+
if (advancedResults.binaryWarnings.length > 0) {
|
|
620
|
+
console.log(ansis.bold.white(`\n🏗️ Binary Shaking Audit (${advancedResults.binaryWarnings.length}):`));
|
|
621
|
+
advancedResults.binaryWarnings.forEach(w => console.log(ansis.white(` • [${w.type}] ${w.message} (${w.file}:${w.line})`)));
|
|
622
|
+
}
|
|
623
|
+
if (advancedResults.sandboxViolations.length > 0) {
|
|
624
|
+
console.log(ansis.bold.bgRed.white(`\n🧱 Architectural Sandbox Violations (${advancedResults.sandboxViolations.length}):`));
|
|
625
|
+
advancedResults.sandboxViolations.forEach(w => console.log(ansis.red(` • [${w.type}] ${w.message} (${w.file})`)));
|
|
626
|
+
}
|
|
627
|
+
if (advancedResults.dereferenceWarnings.length > 0) {
|
|
628
|
+
console.log(ansis.bold.bgBlack.red(`\n💥 Guaranteed Runtime Exceptions (${advancedResults.dereferenceWarnings.length}):`));
|
|
629
|
+
advancedResults.dereferenceWarnings.forEach(w => console.log(ansis.red(` • [${w.type}] ${w.message} (${w.file}:${w.line})`)));
|
|
630
|
+
}
|
|
631
|
+
if (advancedResults.cloneFindings.length > 0) {
|
|
632
|
+
console.log(ansis.bold.bgCyan.black(`\n👯 Structural AST Clones Detected (${advancedResults.cloneFindings.length}):`));
|
|
633
|
+
advancedResults.cloneFindings.forEach(w => console.log(ansis.cyan(` • [${w.type}] ${w.message} (${w.file}:${w.line})`)));
|
|
634
|
+
}
|
|
635
|
+
if (advancedResults.scopeWarnings.length > 0) {
|
|
636
|
+
console.log(ansis.bold.bgGreen.black(`\n🔭 Scope Minimization Suggestions (${advancedResults.scopeWarnings.length}):`));
|
|
637
|
+
advancedResults.scopeWarnings.forEach(w => console.log(ansis.green(` • [${w.type}] ${w.message} (${w.file}:${w.line})`)));
|
|
638
|
+
}
|
|
639
|
+
if (advancedResults.loopWarnings.length > 0) {
|
|
640
|
+
console.log(ansis.bold.bgYellow.black(`\n🔄 Infinite Execution Proofs (${advancedResults.loopWarnings.length}):`));
|
|
641
|
+
advancedResults.loopWarnings.forEach(w => console.log(ansis.yellow(` • [${w.type}] ${w.message} (${w.file}:${w.line})`)));
|
|
642
|
+
}
|
|
643
|
+
if (advancedResults.configWarnings.length > 0) {
|
|
644
|
+
console.log(ansis.bold.bgBlue.white(`\n🧼 Configuration Sanitizer (${advancedResults.configWarnings.length}):`));
|
|
645
|
+
advancedResults.configWarnings.forEach(w => console.log(ansis.blue(` • [${w.type}] ${w.message} (${w.file}:${w.line})`)));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
581
649
|
const cycles = cyclesResult; // Use the already detected cycles
|
|
582
650
|
|
|
583
651
|
const structuralModificationsStaged =
|
|
@@ -683,6 +751,104 @@ export class RefactoringEngine {
|
|
|
683
751
|
}
|
|
684
752
|
}
|
|
685
753
|
|
|
754
|
+
async _generateVisualization(graph) {
|
|
755
|
+
console.log(ansis.bold.green('\n🌐 [VISUALIZER] Generating Interactive Execution Graph...'));
|
|
756
|
+
|
|
757
|
+
const nodes = [];
|
|
758
|
+
const edges = [];
|
|
759
|
+
const fileToIndex = new Map();
|
|
760
|
+
let idCounter = 1;
|
|
761
|
+
|
|
762
|
+
for (const [file, node] of graph.entries()) {
|
|
763
|
+
const relPath = path.relative(this.context.cwd, file);
|
|
764
|
+
const id = idCounter++;
|
|
765
|
+
fileToIndex.set(file, id);
|
|
766
|
+
nodes.push({
|
|
767
|
+
id,
|
|
768
|
+
label: relPath,
|
|
769
|
+
color: node.isLibraryEntry ? '#ff7675' : '#74b9ff',
|
|
770
|
+
shape: node.isLibraryEntry ? 'diamond' : 'dot',
|
|
771
|
+
size: node.isLibraryEntry ? 25 : 15
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
for (const [file, node] of graph.entries()) {
|
|
776
|
+
const fromId = fileToIndex.get(file);
|
|
777
|
+
node.outgoingEdges.forEach(edgeFile => {
|
|
778
|
+
const toId = fileToIndex.get(edgeFile);
|
|
779
|
+
if (toId) {
|
|
780
|
+
edges.push({ from: fromId, to: toId, arrows: 'to' });
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const html = `
|
|
786
|
+
<!DOCTYPE html>
|
|
787
|
+
<html>
|
|
788
|
+
<head>
|
|
789
|
+
<title>entkapp Execution Graph</title>
|
|
790
|
+
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
|
791
|
+
<style type="text/css">
|
|
792
|
+
body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background-color: #2d3436; color: #dfe6e9; font-family: sans-serif; }
|
|
793
|
+
#network { width: 100%; height: 100%; }
|
|
794
|
+
#header { position: absolute; top: 10px; left: 10px; z-index: 10; pointer-events: none; }
|
|
795
|
+
h1 { margin: 0; font-size: 1.5rem; color: #fab1a0; }
|
|
796
|
+
.legend { margin-top: 5px; font-size: 0.9rem; }
|
|
797
|
+
.legend-item { display: inline-block; margin-right: 15px; }
|
|
798
|
+
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 5px; }
|
|
799
|
+
</style>
|
|
800
|
+
</head>
|
|
801
|
+
<body>
|
|
802
|
+
<div id="header">
|
|
803
|
+
<h1>entkapp Execution Graph</h1>
|
|
804
|
+
<div class="legend">
|
|
805
|
+
<div class="legend-item"><span class="dot" style="background-color: #ff7675; border-radius: 0; transform: rotate(45deg);"></span> Entry Point</div>
|
|
806
|
+
<div class="legend-item"><span class="dot" style="background-color: #74b9ff;"></span> Module</div>
|
|
807
|
+
</div>
|
|
808
|
+
</div>
|
|
809
|
+
<div id="network"></div>
|
|
810
|
+
<script type="text/javascript">
|
|
811
|
+
const nodes = new vis.DataSet(${JSON.stringify(nodes)});
|
|
812
|
+
const edges = new vis.DataSet(${JSON.stringify(edges)});
|
|
813
|
+
const container = document.getElementById('network');
|
|
814
|
+
const data = { nodes, edges };
|
|
815
|
+
const options = {
|
|
816
|
+
nodes: { font: { color: '#dfe6e9', size: 14 } },
|
|
817
|
+
edges: { color: { color: '#636e72', highlight: '#fab1a0' }, width: 2 },
|
|
818
|
+
physics: {
|
|
819
|
+
forceAtlas2Based: { gravitationalConstant: -50, centralGravity: 0.01, springLength: 100, springConstant: 0.08 },
|
|
820
|
+
maxVelocity: 50,
|
|
821
|
+
solver: 'forceAtlas2Based',
|
|
822
|
+
timestep: 0.35,
|
|
823
|
+
stabilization: { iterations: 150 }
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
new vis.Network(container, data, options);
|
|
827
|
+
</script>
|
|
828
|
+
</body>
|
|
829
|
+
</html>`;
|
|
830
|
+
|
|
831
|
+
const http = await import('http');
|
|
832
|
+
const server = http.createServer((req, res) => {
|
|
833
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
834
|
+
res.end(html);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
const port = 3000;
|
|
838
|
+
server.listen(port, () => {
|
|
839
|
+
console.log(ansis.bold.cyan(`\n🚀 Web Viewer active at: http://localhost:${port}`));
|
|
840
|
+
console.log(ansis.yellow('Press Ctrl+C to terminate the session and continue...'));
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
return new Promise((resolve) => {
|
|
844
|
+
process.on('SIGINT', () => {
|
|
845
|
+
server.close();
|
|
846
|
+
console.log(ansis.bold.red('\n🛑 Web Viewer stopped.'));
|
|
847
|
+
resolve();
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
686
852
|
async discoverSourceFiles(dir, fileList) {
|
|
687
853
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
688
854
|
for (const entry of entries) {
|
|
@@ -702,10 +868,16 @@ export class RefactoringEngine {
|
|
|
702
868
|
|
|
703
869
|
async linkDependencyGraph() {
|
|
704
870
|
for (const [filePath, node] of this.context.projectGraph.entries()) {
|
|
871
|
+
if (this.context.verbose && node.explicitImports.size > 0) {
|
|
872
|
+
console.log(`[Linker] Linking ${node.explicitImports.size} imports from ${path.relative(this.context.cwd, filePath)}`);
|
|
873
|
+
}
|
|
705
874
|
// Pass A: Link all explicit imports (static + dynamic + re-export sources)
|
|
706
875
|
for (const specifier of node.explicitImports) {
|
|
707
876
|
const resolvedPath = this.resolver.resolveModulePath(filePath, specifier);
|
|
708
877
|
if (resolvedPath && this.context.projectGraph.has(resolvedPath)) {
|
|
878
|
+
if (this.context.verbose) {
|
|
879
|
+
console.log(` -> Resolved ${specifier} to ${path.relative(this.context.cwd, resolvedPath)}`);
|
|
880
|
+
}
|
|
709
881
|
this.context.projectGraph.get(resolvedPath).incomingEdges.add(filePath);
|
|
710
882
|
node.outgoingEdges.add(resolvedPath);
|
|
711
883
|
|
|
@@ -727,10 +899,20 @@ export class RefactoringEngine {
|
|
|
727
899
|
}
|
|
728
900
|
}
|
|
729
901
|
}
|
|
902
|
+
// Auto-detect root index as entry point if no entry points are defined
|
|
903
|
+
const rootIndexFiles = ['src/index.ts', 'src/index.js', 'index.ts', 'index.js'];
|
|
904
|
+
const slashify = (p) => path.resolve(this.context.cwd, p).replace(/\\/g, '/');
|
|
905
|
+
for (const indexFile of rootIndexFiles) {
|
|
906
|
+
const absIndex = slashify(indexFile);
|
|
907
|
+
if (this.context.projectGraph.has(absIndex)) {
|
|
908
|
+
this.context.projectGraph.get(absIndex).isLibraryEntry = true;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
730
911
|
|
|
731
912
|
// Pass B: Link named-symbol imports through barrel/re-export chains
|
|
732
913
|
for (const specToken of node.importedSymbols) {
|
|
733
|
-
|
|
914
|
+
// Fix: Use lastIndexOf for Windows paths (C:/path:symbol)
|
|
915
|
+
const delimiterIndex = specToken.lastIndexOf(':');
|
|
734
916
|
if (delimiterIndex === -1) continue;
|
|
735
917
|
const specifier = specToken.slice(0, delimiterIndex);
|
|
736
918
|
const symbol = specToken.slice(delimiterIndex + 1);
|
|
@@ -822,5 +1004,7 @@ export class RefactoringEngine {
|
|
|
822
1004
|
if (cachedRecord.localSuppressedRules) cachedRecord.localSuppressedRules.forEach(r => node.localSuppressedRules.add(r));
|
|
823
1005
|
if (cachedRecord.calculatedDynamicImports) node.calculatedDynamicImports = cachedRecord.calculatedDynamicImports;
|
|
824
1006
|
if (cachedRecord.isLibraryEntry !== undefined) node.isLibraryEntry = cachedRecord.isLibraryEntry;
|
|
1007
|
+
if (cachedRecord.isEntry !== undefined) node.isEntry = cachedRecord.isEntry;
|
|
1008
|
+
if (cachedRecord.isFrameworkComponent !== undefined) node.isFrameworkComponent = cachedRecord.isFrameworkComponent;
|
|
825
1009
|
}
|
|
826
1010
|
}
|
|
@@ -66,7 +66,9 @@ export class IncrementalCacheManager {
|
|
|
66
66
|
securityThreats: node.securityThreats || [],
|
|
67
67
|
localSuppressedRules: Array.from(node.localSuppressedRules),
|
|
68
68
|
symbolSourceLocations: Object.fromEntries(node.symbolSourceLocations),
|
|
69
|
-
externalPackageUsage: Array.from(node.externalPackageUsage)
|
|
69
|
+
externalPackageUsage: Array.from(node.externalPackageUsage),
|
|
70
|
+
isEntry: node.isEntry,
|
|
71
|
+
isFrameworkComponent: node.isFrameworkComponent
|
|
70
72
|
};
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -82,6 +82,9 @@ export class WorkerPool {
|
|
|
82
82
|
if (result.jsxProps) result.jsxProps.forEach(p => node.jsxProps.add(p));
|
|
83
83
|
if (result.decorators) result.decorators.forEach(d => node.decorators.add(d));
|
|
84
84
|
if (result.isFrameworkContract) node.isFrameworkContract = true;
|
|
85
|
+
if (result.isEntry) node.isEntry = true;
|
|
86
|
+
if (result.isLibraryEntry) node.isLibraryEntry = true;
|
|
87
|
+
if (result.isFrameworkComponent) node.isFrameworkComponent = true;
|
|
85
88
|
});
|
|
86
89
|
|
|
87
90
|
return true;
|
|
@@ -96,7 +99,13 @@ export class WorkerPool {
|
|
|
96
99
|
executeChunkInsideThread(fileChunkSubset) {
|
|
97
100
|
return new Promise((resolve, reject) => {
|
|
98
101
|
const workerInstance = new Worker(this.workerScriptPath, { type: 'module',
|
|
99
|
-
workerData: {
|
|
102
|
+
workerData: {
|
|
103
|
+
files: fileChunkSubset,
|
|
104
|
+
contextOptions: {
|
|
105
|
+
verbose: this.context.verbose,
|
|
106
|
+
cwd: this.context.cwd
|
|
107
|
+
}
|
|
108
|
+
}
|
|
100
109
|
});
|
|
101
110
|
|
|
102
111
|
workerInstance.on('message', (payload) => resolve(payload));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parentPort, workerData } from 'worker_threads';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import { ASTAnalyzer } from '../ast/ASTAnalyzer.js';
|
|
4
|
-
import { OxcAnalyzer } from '../
|
|
4
|
+
import { OxcAnalyzer } from '../ast/OxcAnalyzer.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Worker Thread Execution Script
|
|
@@ -14,6 +14,7 @@ async function runTask() {
|
|
|
14
14
|
// Create a minimal context for analyzers
|
|
15
15
|
const mockContext = {
|
|
16
16
|
verbose: contextOptions.verbose,
|
|
17
|
+
cwd: contextOptions.cwd || process.cwd(),
|
|
17
18
|
projectGraph: new Map(),
|
|
18
19
|
getOrCreateNode: (path) => ({
|
|
19
20
|
filePath: path,
|
|
@@ -31,7 +32,10 @@ async function runTask() {
|
|
|
31
32
|
jsxComponents: new Set(),
|
|
32
33
|
jsxProps: new Set(),
|
|
33
34
|
decorators: new Set(),
|
|
34
|
-
isFrameworkContract: false
|
|
35
|
+
isFrameworkContract: false,
|
|
36
|
+
isEntry: false,
|
|
37
|
+
isLibraryEntry: false,
|
|
38
|
+
isFrameworkComponent: false
|
|
35
39
|
})
|
|
36
40
|
};
|
|
37
41
|
|
|
@@ -71,7 +75,10 @@ async function runTask() {
|
|
|
71
75
|
jsxComponents: Array.from(node.jsxComponents),
|
|
72
76
|
jsxProps: Array.from(node.jsxProps),
|
|
73
77
|
decorators: Array.from(node.decorators),
|
|
74
|
-
isFrameworkContract: node.isFrameworkContract
|
|
78
|
+
isFrameworkContract: node.isFrameworkContract,
|
|
79
|
+
isEntry: node.isEntry,
|
|
80
|
+
isLibraryEntry: node.isLibraryEntry,
|
|
81
|
+
isFrameworkComponent: node.isFrameworkComponent
|
|
75
82
|
});
|
|
76
83
|
} catch (err) {
|
|
77
84
|
if (contextOptions.verbose) {
|
|
@@ -45,14 +45,13 @@ export class DependencyResolver {
|
|
|
45
45
|
// we should try to resolve the .ts version first.
|
|
46
46
|
let effectiveSpecifier = importSpecifier;
|
|
47
47
|
const isTsFile = /\.(ts|tsx|mts|cts)$/.test(containingFile);
|
|
48
|
-
if (
|
|
49
|
-
// Try replacing .js with .ts
|
|
48
|
+
if (importSpecifier.endsWith('.js')) {
|
|
50
49
|
const tsSpecifier = importSpecifier.replace(/\.js$/, '.ts');
|
|
51
50
|
try {
|
|
52
51
|
const resolvedTs = this.nativeResolver(containingDir, tsSpecifier);
|
|
53
52
|
if (this.isAbsoluteInternalPath(resolvedTs)) return resolvedTs;
|
|
54
53
|
} catch (e) {
|
|
55
|
-
// Fall back to original specifier
|
|
54
|
+
// Fall back to original specifier
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
// -----------------------------------------
|
|
@@ -106,7 +105,7 @@ export class DependencyResolver {
|
|
|
106
105
|
} catch {}
|
|
107
106
|
}
|
|
108
107
|
if (this.context.verbose) {
|
|
109
|
-
console.
|
|
108
|
+
console.log(`[Resolution Trace Skip] Specifier unresolvable: ${effectiveSpecifier} inside ${containingFile}`);
|
|
110
109
|
}
|
|
111
110
|
}
|
|
112
111
|
|