entkapp 5.0.0 → 5.1.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 CHANGED
@@ -28,7 +28,7 @@ async function bootstrap() {
28
28
  program
29
29
  .name('entkapp')
30
30
  .description(ansis.cyan('The Ultimate Enterprise Codebase Janitor with OXC integration, type-aware analysis, and automated structural healing.'))
31
- .version(packageJsonContent.version || '5.0.0');
31
+ .version(packageJsonContent.version || '5.1.0');
32
32
 
33
33
  program
34
34
  .option('-c, --cwd <path>', 'Specify the execution context root directory', process.cwd())
@@ -132,7 +132,7 @@ async function bootstrap() {
132
132
  }, timeoutMs);
133
133
  timeoutTimer.unref(); // Allow process to exit if work finishes
134
134
 
135
- console.log(ansis.bold.green(`\n📦 entkapp v${packageJsonContent.version || '5.0.0'} Engine Activation`));
135
+ console.log(ansis.bold.green(`\n📦 entkapp v${packageJsonContent.version || '5.1.0'} Engine Activation`));
136
136
  console.log(ansis.dim('------------------------------------------------------------'));
137
137
  console.log(`${ansis.bold('Target Workspace Root :')} ${ansis.blue(targetCwd)}`);
138
138
  console.log(`${ansis.bold('Refactoring Mode :')} ${options.fix ? ansis.yellow('Active Fixing & Self-Healing Enabled') : ansis.gray('Dry-Run Reporting Only')}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "name": "entkapp",
4
- "version": "5.0.0",
4
+ "version": "5.1.0",
5
5
  "description": "The Ultimate Enterprise Codebase Janitor. Faster than Knip with OXC integration, type-aware analysis, and automated structural healing. Fully standalone - solving what Knip cannot.",
6
6
  "type": "module",
7
7
  "main": "index.js",
@@ -234,7 +234,11 @@ export class ASTAnalyzer {
234
234
  }
235
235
  }
236
236
  break;
237
+
237
238
  case ts.SyntaxKind.CallExpression:
239
+ // FIX: Führe zuerst die generische Aufrufanalyse aus, die vorher blockiert war
240
+ this.handleCallExpression(node, fileNode, sourceFile);
241
+
238
242
  if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
239
243
  const arg = node.arguments[0];
240
244
  if (arg) {
@@ -244,14 +248,12 @@ export class ASTAnalyzer {
244
248
  fileNode.explicitImports.add(specifier);
245
249
  } else {
246
250
  // UPGRADE: Handle Dynamic Imports with variables/expressions
247
- // We mark the file as having "calculated" dynamic imports to be conservative
248
251
  fileNode.hasCalculatedDynamicImports = true;
249
252
  const exprText = arg.getText(sourceFile);
250
- if (!fileNode.calculatedDynamicImports) fileNode.calculatedDynamicImports = new Set();
253
+ if (!fileNode.calculatedDynamicImports || Array.isArray(fileNode.calculatedDynamicImports)) {
254
+ fileNode.calculatedDynamicImports = new Set();
255
+ }
251
256
  fileNode.calculatedDynamicImports.add(exprText);
252
-
253
- // Conservative approach: If we see `import(path)`, any file in the same or sub directory could be a target.
254
- // This is a simplified version of what knip/entkapp does for plugins.
255
257
  fileNode.dynamicImports.add('__DYNAMIC_PATTERN__');
256
258
  }
257
259
  }
@@ -269,12 +271,23 @@ export class ASTAnalyzer {
269
271
  // Dynamic require with template literal
270
272
  fileNode.hasCalculatedDynamicImports = true;
271
273
  const exprText = arg.getText(sourceFile);
272
- if (!fileNode.calculatedDynamicImports) fileNode.calculatedDynamicImports = [];
273
- fileNode.calculatedDynamicImports.push({ kind: 'DynamicRequire', expression: exprText, start: arg.getStart(sourceFile) });
274
+
275
+ // FIX: Typ-Integrität als Set erzwingen (kein Array [])
276
+ if (!fileNode.calculatedDynamicImports || Array.isArray(fileNode.calculatedDynamicImports)) {
277
+ fileNode.calculatedDynamicImports = new Set();
278
+ }
279
+
280
+ // FIX: .add() statt .push() verwenden, um Absturz in Pass 2 zu verhindern
281
+ fileNode.calculatedDynamicImports.add({
282
+ kind: 'DynamicRequire',
283
+ expression: exprText,
284
+ start: arg.getStart(sourceFile)
285
+ });
274
286
  fileNode.dynamicImports.add('__DYNAMIC_PATTERN__');
275
287
  }
276
288
  }
277
289
  break;
290
+
278
291
  case ts.SyntaxKind.PropertyAccessExpression:
279
292
  {
280
293
  const chain = node.getText(sourceFile);
@@ -284,9 +297,7 @@ export class ASTAnalyzer {
284
297
  }
285
298
  }
286
299
  break;
287
- case ts.SyntaxKind.CallExpression:
288
- this.handleCallExpression(node, fileNode, sourceFile);
289
- break;
300
+
290
301
  case ts.SyntaxKind.JsxElement:
291
302
  case ts.SyntaxKind.JsxSelfClosingElement:
292
303
  this.handleJsxElement(node, fileNode, sourceFile);
@@ -580,7 +591,9 @@ export class ASTAnalyzer {
580
591
  }
581
592
  } else {
582
593
  // Non-literal dynamic import
583
- if (!fileNode.calculatedDynamicImports) fileNode.calculatedDynamicImports = [];
594
+ if (!Array.isArray(fileNode.calculatedDynamicImports)) {
595
+ fileNode.calculatedDynamicImports = [];
596
+ }
584
597
  fileNode.calculatedDynamicImports.push({ kind: ts.SyntaxKind[arg.kind], start: arg.getStart(sourceFile) });
585
598
 
586
599
  // UPGRADE 2: Try to perform Glob-Analysis on Template Strings
@@ -605,11 +618,13 @@ export class ASTAnalyzer {
605
618
  if (ts.isTemplateExpression(node)) {
606
619
  const head = node.head.text;
607
620
  if (head.startsWith('./') || head.startsWith('../')) {
608
- // Pattern: `./directory/${variable}`
621
+ // FIX: Typ-Integrität als Set erzwingen, kein Array!
622
+ if (!fileNode.globImports || Array.isArray(fileNode.globImports)) {
623
+ fileNode.globImports = new Set();
624
+ }
609
625
  const dirPath = path.dirname(head);
610
626
  if (dirPath && dirPath !== '.') {
611
- if (!fileNode.globImports) fileNode.globImports = new Set();
612
- fileNode.globImports.add(dirPath);
627
+ fileNode.globImports.add(dirPath); // Jetzt klappt .add() garantiert!
613
628
  }
614
629
  }
615
630
  }
@@ -7,7 +7,7 @@ import fs from 'fs/promises';
7
7
  */
8
8
  export class BarrelParser {
9
9
  constructor(context, resolver) {
10
- this.context = context;
10
+ this.context = context; // Enthält globale Konfigurationen (z.B. context.entrypoints)
11
11
  this.resolver = resolver;
12
12
  this.cachedSpecifications = new Map();
13
13
  }
@@ -23,28 +23,43 @@ export class BarrelParser {
23
23
 
24
24
  const specification = {
25
25
  isBarrelInstance: false,
26
+ isApplicationEntrypoint: false, // FIX: Schutzschicht für Einstiegspunkte gegen Fehldetektion
26
27
  wildcardExports: new Set(), // export * from './module';
27
28
  namespacedWildcardExports: new Map(), // export * as Utils from './module';
28
29
  forwardedNamedExports: new Map(), // export { token as alias } from './module';
29
30
  declaredLocalExports: new Set() // export const a = 1;
30
31
  };
31
32
 
33
+ // FIX: Überprüfe, ob die aktuelle Datei als App-Einstiegspunkt deklariert ist
34
+ if (this.context && this.context.entrypoints) {
35
+ const isEntry = this.context.entrypoints.some(entry =>
36
+ this.resolver.resolveModulePath(filePath, entry) === filePath || entry === filePath
37
+ );
38
+ if (isEntry) {
39
+ specification.isApplicationEntrypoint = true;
40
+ }
41
+ }
42
+
32
43
  try {
33
44
  const code = await fs.readFile(filePath, 'utf8');
34
45
  const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
35
46
 
36
47
  this.harvestExportSignatures(sourceFile, specification);
37
48
 
38
- if (specification.wildcardExports.size > 0 ||
49
+ // FIX: Eine Datei ist NUR DANN ein rein kosmetisches Barrel, wenn sie Re-Exports besitzt
50
+ // UND NICHT der geschützte Haupteinstiegspunkt (Entrypoint) der Anwendung ist!
51
+ if (!specification.isApplicationEntrypoint && (
52
+ specification.wildcardExports.size > 0 ||
39
53
  specification.namespacedWildcardExports.size > 0 ||
40
- specification.forwardedNamedExports.size > 0) {
54
+ specification.forwardedNamedExports.size > 0
55
+ )) {
41
56
  specification.isBarrelInstance = true;
42
57
  }
43
58
 
44
59
  this.cachedSpecifications.set(filePath, specification);
45
60
  return specification;
46
61
  } catch {
47
- return specification; // Error state defaults to safe boundaries layout manifest
62
+ return specification; // Error-State fällt auf sicheres Standard-Layout zurück
48
63
  }
49
64
  }
50
65
 
@@ -117,14 +132,7 @@ export class BarrelParser {
117
132
  break;
118
133
  }
119
134
 
120
- // Track default exports declared directly within the file boundary:
121
- // Handles both `export default function foo()` and `export default expression`.
122
- // The canonical symbol name stored is 'default' so that forwardedNamedExports
123
- // entries whose sourceSymbol is 'default' can resolve correctly via Rule A.
124
135
  case ts.SyntaxKind.ExportAssignment: {
125
- // ExportAssignment covers `export default <expr>` (isExportEquals === false)
126
- // as well as `export = <expr>` (isExportEquals === true, CommonJS style).
127
- // We register 'default' for both to ensure the barrel tracer can settle here.
128
136
  spec.declaredLocalExports.add('default');
129
137
  break;
130
138
  }
@@ -140,10 +148,6 @@ export class BarrelParser {
140
148
  * @param {Map} activeProjectGraph - Global active module maps directory registry
141
149
  * @param {Set} [protectionStack] - Avoids cyclic validation traps inside self-referencing links
142
150
  */
143
- /**
144
- * UPGRADE 3: Cross-Barrel Symbol Tracking.
145
- * Resolves the original source of a symbol through re-exports and aliases.
146
- */
147
151
  async determineSymbolDeclarationOrigin(contextFilePath, targetSymbolName, activeProjectGraph, protectionStack = new Set()) {
148
152
  if (protectionStack.has(contextFilePath)) {
149
153
  return { originFile: contextFilePath, originSymbol: targetSymbolName };
@@ -172,7 +176,6 @@ export class BarrelParser {
172
176
  // Rule C: Evaluate structural namespace alias groupings (export * as name from 'module')
173
177
  for (const [namespaceAlias, relativeModule] of spec.namespacedWildcardExports.entries()) {
174
178
  if (targetSymbolName === namespaceAlias) {
175
- // The symbol IS the namespace itself
176
179
  const fullyResolvedPath = this.resolver.resolveModulePath(contextFilePath, relativeModule);
177
180
  return { originFile: fullyResolvedPath, originSymbol: '*' };
178
181
  }
@@ -190,7 +193,7 @@ export class BarrelParser {
190
193
  const fullyResolvedPath = this.resolver.resolveModulePath(contextFilePath, relativePath);
191
194
 
192
195
  if (fullyResolvedPath) {
193
- // FIX: Mark the target module as active immediately when export * is found
196
+ // Verbinde Graph-Kanten direkt während der Wildcard-Traversierung
194
197
  const contextNode = activeProjectGraph.get(contextFilePath);
195
198
  if (contextNode) {
196
199
  contextNode.outgoingEdges.add(fullyResolvedPath);
@@ -204,13 +207,12 @@ export class BarrelParser {
204
207
  fullyResolvedPath,
205
208
  targetSymbolName,
206
209
  activeProjectGraph,
207
- new Set(protectionStack) // Use a copy so sibling branches don't block each other
210
+ new Set(protectionStack) // Kopie verhindert geschwisterliche Branch-Blockaden
208
211
  );
209
212
 
210
213
  if (!continuousResolutionTrace) continue;
211
214
  if (continuousResolutionTrace.originFile === contextFilePath) continue;
212
215
 
213
- // Verify that the resolved origin actually declares the symbol.
214
216
  const originSpec = await this.parseBarrelSpecification(continuousResolutionTrace.originFile);
215
217
  if (originSpec.declaredLocalExports.has(continuousResolutionTrace.originSymbol) ||
216
218
  continuousResolutionTrace.originSymbol === '*') {
@@ -225,4 +227,4 @@ export class BarrelParser {
225
227
 
226
228
  return { originFile: contextFilePath, originSymbol: targetSymbolName };
227
229
  }
228
- }
230
+ }
@@ -250,6 +250,9 @@ export class OxcAnalyzer {
250
250
  } else if (node.source.type === "TemplateLiteral") {
251
251
  // Conservative Dynamic Analysis: Treat template literal imports as "potentially anything"
252
252
  const quasis = node.source.quasis.map(q => q.value.cooked).join('*');
253
+ if (!Array.isArray(fileNode.calculatedDynamicImports)) {
254
+ fileNode.calculatedDynamicImports = [];
255
+ }
253
256
  fileNode.calculatedDynamicImports.push({
254
257
  kind: 'TemplateLiteral',
255
258
  pattern: quasis,
@@ -437,6 +440,9 @@ export class OxcAnalyzer {
437
440
  handleDynamicRequire(node, fileNode) {
438
441
  if (node.type === "TemplateLiteral") {
439
442
  const quasis = node.quasis.map(q => q.value.cooked).join('*');
443
+ if (!Array.isArray(fileNode.calculatedDynamicImports)) {
444
+ fileNode.calculatedDynamicImports = [];
445
+ }
440
446
  fileNode.calculatedDynamicImports.push({
441
447
  kind: 'DynamicRequire',
442
448
  pattern: quasis,
package/src/index.js CHANGED
@@ -563,10 +563,13 @@ export class RefactoringEngine {
563
563
  if (node && (node.isLibraryEntry || node.isEntry)) return false;
564
564
 
565
565
  let isPackageEntryPoint = false;
566
- for (const [_, metadata] of this.workspaceGraph.packageManifests.entries()) {
567
- if (metadata.entryPoints && metadata.entryPoints.map(p => slashifyLocal(p)).includes(cleanAbsPath)) {
568
- isPackageEntryPoint = true;
569
- break;
566
+ const manifests = this.workspaceGraph?.packageManifests;
567
+ if (manifests) {
568
+ for (const [_, metadata] of manifests.entries()) {
569
+ if (metadata.entryPoints && metadata.entryPoints.map(p => slashifyLocal(p)).includes(cleanAbsPath)) {
570
+ isPackageEntryPoint = true;
571
+ break;
572
+ }
570
573
  }
571
574
  }
572
575
  return !isPackageEntryPoint;
@@ -574,7 +577,7 @@ export class RefactoringEngine {
574
577
  }
575
578
  analysisSummary.unlistedDependencies = this.context.unlistedDependencies || [];
576
579
 
577
- const advancedResults = this.advancedAnalysis.runAll(this.context.projectGraph, this.workspaceGraph.packageManifests);
580
+ const advancedResults = this.advancedAnalysis.runAll(this.context.projectGraph, this.workspaceGraph?.packageManifests || new Map());
578
581
  const cycles = cyclesResult;
579
582
 
580
583
  const structuralModificationsStaged =
@@ -6,47 +6,86 @@ export class CircularDetector {
6
6
  this.cycles = [];
7
7
  }
8
8
 
9
- detectCycles(projectGraph, context) {
10
- const visited = new Set();
11
- const stack = new Set();
12
- const cycles = [];
9
+ detectCycles(projectGraph) {
10
+ const stack = [];
11
+ const onStack = new Set();
12
+ const indices = new Map();
13
+ const lowlink = new Map();
14
+ const sccs = [];
15
+ let index = 0;
13
16
 
14
- const traverse = (filePath, currentPath) => {
15
- visited.add(filePath);
16
- stack.add(filePath);
17
- currentPath.push(filePath);
17
+ const strongConnect = (v) => {
18
+ indices.set(v, index);
19
+ lowlink.set(v, index);
20
+ index++;
21
+ stack.push(v);
22
+ onStack.add(v);
18
23
 
19
- const node = projectGraph.get(filePath);
24
+ const node = projectGraph.get(v);
20
25
  if (node && node.outgoingEdges) {
21
- for (const neighbor of node.outgoingEdges) {
22
- if (stack.has(neighbor)) {
23
- const cycleStartIndex = currentPath.indexOf(neighbor);
24
- const cycle = currentPath.slice(cycleStartIndex);
25
- cycle.push(neighbor);
26
- cycles.push(cycle);
27
- } else if (!visited.has(neighbor)) {
28
- traverse(neighbor, currentPath);
26
+ // Ensure outgoingEdges is treated as an array-like structure
27
+ const edges = Array.isArray(node.outgoingEdges) ? node.outgoingEdges : Array.from(node.outgoingEdges);
28
+
29
+ for (let i = 0; i < edges.length; i++) {
30
+ const w = edges[i];
31
+ if (!indices.has(w)) {
32
+ strongConnect(w);
33
+ lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
34
+ } else if (onStack.has(w)) {
35
+ lowlink.set(v, Math.min(lowlink.get(v), indices.get(w)));
29
36
  }
30
37
  }
31
38
  }
32
39
 
33
- stack.delete(filePath);
34
- currentPath.pop();
40
+ if (lowlink.get(v) === indices.get(v)) {
41
+ const scc = [];
42
+ let w;
43
+ do {
44
+ w = stack.pop();
45
+ onStack.delete(w);
46
+ scc.push(w);
47
+ } while (w !== v);
48
+
49
+ if (scc.length > 1) {
50
+ const cycle = scc.slice().reverse();
51
+ cycle.push(cycle[0]);
52
+ sccs.push(cycle);
53
+ } else if (scc.length === 1) {
54
+ const singleNode = projectGraph.get(scc[0]);
55
+ if (singleNode && singleNode.outgoingEdges) {
56
+ const edges = Array.isArray(singleNode.outgoingEdges) ? singleNode.outgoingEdges : Array.from(singleNode.outgoingEdges);
57
+ let hasSelfLoop = false;
58
+ for (let i = 0; i < edges.length; i++) {
59
+ if (edges[i] === scc[0]) {
60
+ hasSelfLoop = true;
61
+ break;
62
+ }
63
+ }
64
+ if (hasSelfLoop) {
65
+ sccs.push([scc[0], scc[0]]);
66
+ }
67
+ }
68
+ }
69
+ }
35
70
  };
36
71
 
37
- for (const filePath of projectGraph.keys()) {
38
- if (!visited.has(filePath)) {
39
- traverse(filePath, []);
72
+ const keys = Array.from(projectGraph.keys());
73
+ for (let i = 0; i < keys.length; i++) {
74
+ if (!indices.has(keys[i])) {
75
+ strongConnect(keys[i]);
40
76
  }
41
77
  }
42
78
 
43
- this.cycles = cycles;
44
- return cycles;
79
+ this.cycles = sccs;
80
+ return sccs;
45
81
  }
46
82
 
47
83
  formatCycles() {
48
84
  return this.cycles.map(cycle => {
49
- return cycle.map(p => path.relative(this.context.cwd, p).replace(/\\/g, '/')).join(' -> ');
85
+ return cycle.map(p => {
86
+ const relativePath = path.relative(this.context.cwd, p);
87
+ return relativePath.replace(/\\/g, '/');
88
+ }).join(' -> ');
50
89
  });
51
90
  }
52
91
  }
@@ -24,12 +24,23 @@ export class DependencyResolver {
24
24
 
25
25
  resolveModulePath(sourceFile, specifier) {
26
26
  const cleanSource = this.normalizePath(sourceFile);
27
+
28
+ // UPGRADE: Use PathMapper for sophisticated resolution (TS-to-JS, aliases, etc.)
29
+ if (this.pathMapper) {
30
+ const dir = path.dirname(cleanSource);
31
+ const target = path.resolve(dir, specifier);
32
+ const resolved = this.pathMapper.resolvePath(target);
33
+ if (resolved && existsSync(resolved)) {
34
+ return this.normalizePath(resolved);
35
+ }
36
+ }
37
+
27
38
  if (specifier.startsWith('.')) {
28
39
  const dir = path.dirname(cleanSource);
29
40
  const target = path.resolve(dir, specifier);
30
41
  const normalizedTarget = this.normalizePath(target);
31
42
 
32
- const extensions = ['', '.js', '.ts', '.tsx', '.jsx', '/index.js', '/index.ts'];
43
+ const extensions = ['', '.js', '.ts', '.tsx', '.jsx', '/index.js', '/index.ts', '/index.tsx'];
33
44
  for (const ext of extensions) {
34
45
  const p = normalizedTarget + ext;
35
46
  if (existsSync(p)) return this.normalizePath(p);
@@ -1,5 +1,49 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
1
4
  export class PathMapper {
2
- constructor(context) { this.context = context; }
3
- async loadMappings() {}
4
- resolvePath(p) { return p; }
5
+ constructor(context) {
6
+ this.context = context;
7
+ }
8
+
9
+ async loadMappings() {
10
+ // Hier können später tsconfig-Pfade geladen werden
11
+ }
12
+
13
+ /**
14
+ * Resolves physical module paths on disk, translating modern .js imports
15
+ * back to their actual TypeScript source files.
16
+ * @param {string} p - The target module specifier or absolute path
17
+ */
18
+ resolvePath(p) {
19
+ if (!p || typeof p !== 'string') return p;
20
+
21
+ // FIX 1: Wenn der Import auf .js endet, übersetze ihn für die Suche auf .ts
22
+ if (p.endsWith('.js')) {
23
+ const tsPath = p.slice(0, -3) + '.ts';
24
+ if (fs.existsSync(tsPath)) return tsPath;
25
+ }
26
+
27
+ // FIX 2: Wenn der Import auf .jsx endet, übersetze ihn für die Suche auf .tsx
28
+ if (p.endsWith('.jsx')) {
29
+ const tsxPath = p.slice(0, -4) + '.tsx';
30
+ if (fs.existsSync(tsxPath)) return tsxPath;
31
+ }
32
+
33
+ // FIX 3: Unterstützung für Verzeichnis-Imports (z.B. ./adapters -> ./adapters/index.ts)
34
+ try {
35
+ const stat = fs.statSync(p);
36
+ if (stat.isDirectory()) {
37
+ const extensions = ['.ts', '.tsx', '.js', '.jsx'];
38
+ for (const ext of extensions) {
39
+ const indexPath = path.join(p, `index${ext}`);
40
+ if (fs.existsSync(indexPath)) return indexPath;
41
+ }
42
+ }
43
+ } catch {
44
+ // Datei existiert nicht oder ist kein Verzeichnis, fahre mit Standard fort
45
+ }
46
+
47
+ return p;
48
+ }
5
49
  }
@@ -1,5 +1,8 @@
1
1
  export class WorkspaceGraph {
2
- constructor(context) { this.context = context; }
2
+ constructor(context) {
3
+ this.context = context;
4
+ this.packageManifests = new Map();
5
+ }
3
6
  async initializeWorkspaceMesh() {}
4
7
  markWorkspacePackagesAsUsed() {}
5
8
  }
package/docs.zip DELETED
Binary file