entkapp 5.0.0 → 5.2.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.
Files changed (34) hide show
  1. package/README.md +63 -20
  2. package/bin/cli.js +2 -2
  3. package/index.js +38 -2241
  4. package/package.json +1 -1
  5. package/src/EngineContext.js +1 -1
  6. package/src/Initializer.js +82 -0
  7. package/src/analyzers/CodeSmellAnalyzer.js +106 -0
  8. package/src/ast/ASTAnalyzer.js +29 -14
  9. package/src/ast/BarrelParser.js +22 -20
  10. package/src/ast/OxcAnalyzer.js +33 -409
  11. package/src/index.js +78 -8
  12. package/src/performance/WorkerTaskRunner.js +7 -7
  13. package/src/plugins/BasePlugin.js +171 -2
  14. package/src/plugins/PluginRegistry.js +193 -81
  15. package/src/plugins/ecosystems/BackendServices.js +168 -32
  16. package/src/plugins/ecosystems/GenericPlugins.js +51 -34
  17. package/src/plugins/ecosystems/ModernFrameworks.js +97 -94
  18. package/src/plugins/ecosystems/MorePlugins.js +429 -51
  19. package/src/plugins/ecosystems/NewPlugins.js +526 -0
  20. package/src/plugins/ecosystems/NextJsPlugin.js +18 -6
  21. package/src/plugins/ecosystems/PluginLoader.js +190 -17
  22. package/src/plugins/ecosystems/TypeScriptPlugin.js +10 -10
  23. package/src/plugins/ecosystems/UltimateBundle.js +168 -0
  24. package/src/resolution/BuildOrchestrator.js +46 -0
  25. package/src/resolution/CircularDetector.js +64 -25
  26. package/src/resolution/ConfigGenerator.js +83 -0
  27. package/src/resolution/DepencyResolver.js +12 -1
  28. package/src/resolution/DependencyFixer.js +88 -0
  29. package/src/resolution/EntryPointDetector.js +4 -4
  30. package/src/resolution/GraphAnalyzer.js +80 -0
  31. package/src/resolution/MigrationAnalyzer.js +60 -0
  32. package/src/resolution/PathMapper.js +47 -3
  33. package/src/resolution/WorkSpaceGraph.js +4 -1
  34. package/docs.zip +0 -0
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.2.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",
@@ -189,7 +189,7 @@ export class EngineContext {
189
189
  });
190
190
 
191
191
  // --- PLUGIN-BASED ECOSYSTEM DETECTION ---
192
- // Plugins will now handle their own erreichbarkeit and dependency validation.
192
+ // Plugins will now handle their own reachability and dependency validation.
193
193
  if (this.pluginRegistry) {
194
194
  const activePlugins = await this.pluginRegistry.getActivePlugins(pkgDir);
195
195
  for (const plugin of activePlugins) {
@@ -0,0 +1,82 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { glob } from 'glob';
4
+
5
+ /**
6
+ * Entkapp Initializer: Analyzes the codebase to generate a package.json
7
+ * with correctly categorized dependencies and devDependencies.
8
+ */
9
+ export class Initializer {
10
+ constructor(context) {
11
+ this.context = context;
12
+ }
13
+
14
+ async run() {
15
+ console.log("🚀 Starting entkapp intelligent initialization...");
16
+ const cwd = this.context.cwd || process.cwd();
17
+
18
+ // 1. Scan for all JS/TS files
19
+ const files = await glob('**/*.{js,ts,jsx,tsx,vue,svelte,astro}', {
20
+ ignore: ['node_modules/**', 'dist/**', '.git/**'],
21
+ cwd
22
+ });
23
+
24
+ const dependencies = new Set();
25
+ const devDependencies = new Set();
26
+
27
+ // Common dev tool indicators
28
+ const devIndicators = ['test', 'spec', 'config', 'stories', 'mock'];
29
+
30
+ console.log(`🔍 Analyzing ${files.length} files for imports...`);
31
+
32
+ for (const file of files) {
33
+ const content = await fs.readFile(path.join(cwd, file), 'utf8');
34
+ const isDevFile = devIndicators.some(ind => file.toLowerCase().includes(ind));
35
+
36
+ // Simple but effective regex for imports
37
+ const importMatches = content.matchAll(/(?:import|from|require)\s*\(?\s*['"]([^' "./][^'"]*)['"]/g);
38
+
39
+ for (const match of importMatches) {
40
+ const pkg = this.extractPackageName(match[1]);
41
+ if (pkg && !this.isBuiltIn(pkg)) {
42
+ if (isDevFile) devDependencies.add(pkg);
43
+ else dependencies.add(pkg);
44
+ }
45
+ }
46
+ }
47
+
48
+ // 2. Generate package.json
49
+ const pkgJson = {
50
+ name: path.basename(cwd),
51
+ version: "1.0.0",
52
+ description: "Initialized with entkapp",
53
+ main: "index.js",
54
+ scripts: {
55
+ "entkapp:run": "entkapp -r",
56
+ "entkapp:check": "entkapp --verbose"
57
+ },
58
+ dependencies: Object.fromEntries([...dependencies].sort().map(d => [d, "latest"])),
59
+ devDependencies: Object.fromEntries([...devDependencies].sort().map(d => [d, "latest"]))
60
+ };
61
+
62
+ await fs.writeFile(path.join(cwd, 'package.json'), JSON.stringify(pkgJson, null, 2));
63
+ console.log("✅ package.json generated with analyzed dependencies.");
64
+
65
+ // 3. Create /entkapp folder
66
+ await fs.mkdir(path.join(cwd, 'entkapp'), { recursive: true });
67
+ console.log("✅ /entkapp configuration folder created.");
68
+ }
69
+
70
+ extractPackageName(specifier) {
71
+ if (specifier.startsWith('@')) {
72
+ const parts = specifier.split('/');
73
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
74
+ }
75
+ return specifier.split('/')[0];
76
+ }
77
+
78
+ isBuiltIn(pkg) {
79
+ const builtins = ['path', 'fs', 'os', 'crypto', 'http', 'https', 'stream', 'util', 'events', 'module', 'process'];
80
+ return builtins.includes(pkg) || pkg.startsWith('node:');
81
+ }
82
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * CodeSmellAnalyzer: Performs deep static analysis to find runtime risks and logic smells.
3
+ */
4
+ export class CodeSmellAnalyzer {
5
+ constructor(context) {
6
+ this.context = context;
7
+ this.issues = [];
8
+ this.rules = {
9
+ 'potential-null-pointer': {
10
+ message: 'Potential Null Pointer Risk: Accessing property on an object that might be undefined.',
11
+ link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cant_access_property'
12
+ },
13
+ 'infinite-loop-risk': {
14
+ message: 'Potential Infinite Loop: Loop condition does not seem to change within the body.',
15
+ link: 'https://en.wikipedia.org/wiki/Infinite_loop'
16
+ },
17
+ 'implicit-type-coercion': {
18
+ message: 'Implicit Type Coercion: Using loose equality (==) can lead to unexpected runtime behavior.',
19
+ link: 'https://dorey.github.io/JavaScript-Equality-Table/'
20
+ }
21
+ };
22
+ }
23
+
24
+ analyze(fileNode) {
25
+ if (!fileNode.ast) return;
26
+ this.walk(fileNode.ast, fileNode);
27
+ }
28
+
29
+ walk(node, fileNode) {
30
+ if (!node) return;
31
+
32
+ // 1. Check for Loose Equality (==)
33
+ if (node.type === 'BinaryExpression' && (node.operator === '==' || node.operator === '!=')) {
34
+ this.addIssue('implicit-type-coercion', node, fileNode);
35
+ }
36
+
37
+ // 2. Check for Potential Infinite Loops (While loops with static conditions)
38
+ if (node.type === 'WhileStatement' && node.test.type === 'Literal' && node.test.value === true) {
39
+ // Check if there is a break or return inside
40
+ if (!this.hasExitStatement(node.body)) {
41
+ this.addIssue('infinite-loop-risk', node, fileNode);
42
+ }
43
+ }
44
+
45
+ // 3. Check for Null Pointer Risks (Accessing properties on potentially uninitialized vars)
46
+ if (node.type === 'MemberExpression' && node.object.type === 'Identifier') {
47
+ const varName = node.object.name;
48
+ if (this.isPotentiallyNull(varName, fileNode)) {
49
+ this.addIssue('potential-null-pointer', node, fileNode);
50
+ }
51
+ }
52
+
53
+ // Recursively walk the AST
54
+ for (const key in node) {
55
+ if (node[key] && typeof node[key] === 'object') {
56
+ if (Array.isArray(node[key])) {
57
+ node[key].forEach(child => this.walk(child, fileNode));
58
+ } else {
59
+ this.walk(node[key], fileNode);
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ addIssue(ruleId, node, fileNode) {
66
+ const rule = this.rules[ruleId];
67
+ fileNode.diagnostics = fileNode.diagnostics || [];
68
+ fileNode.diagnostics.push({
69
+ ruleId,
70
+ message: rule.message,
71
+ link: rule.link,
72
+ line: node.start ? this.getLineNumber(node.start, fileNode.content) : 0,
73
+ severity: 'warning'
74
+ });
75
+ }
76
+
77
+ hasExitStatement(node) {
78
+ let found = false;
79
+ const check = (n) => {
80
+ if (!n || found) return;
81
+ if (n.type === 'BreakStatement' || n.type === 'ReturnStatement' || n.type === 'ThrowStatement') {
82
+ found = true;
83
+ return;
84
+ }
85
+ for (const key in n) {
86
+ if (n[key] && typeof n[key] === 'object') {
87
+ if (Array.isArray(n[key])) n[key].forEach(check);
88
+ else check(n[key]);
89
+ }
90
+ }
91
+ };
92
+ check(node);
93
+ return found;
94
+ }
95
+
96
+ isPotentiallyNull(name, fileNode) {
97
+ // Simple heuristic: if it's a variable declared without init or in a try-catch
98
+ const symbol = fileNode.symbolTable?.get(name);
99
+ return symbol && symbol.type === 'variable' && !symbol.initialized;
100
+ }
101
+
102
+ getLineNumber(pos, content) {
103
+ if (!content) return 0;
104
+ return content.substring(0, pos).split('\n').length;
105
+ }
106
+ }
@@ -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
+ }