entkapp 4.5.0 → 5.0.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
@@ -27,8 +27,8 @@ async function bootstrap() {
27
27
 
28
28
  program
29
29
  .name('entkapp')
30
- .description(ansis.cyan('Enterprise-Grade AST Syntax Refactoring & Self-Healing Engine'))
31
- .version(packageJsonContent.version || '4.0.0');
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');
32
32
 
33
33
  program
34
34
  .option('-c, --cwd <path>', 'Specify the execution context root directory', process.cwd())
@@ -85,7 +85,7 @@ async function bootstrap() {
85
85
  interface: "CLI",
86
86
  useBuiltinPlugins: true,
87
87
  useCustomPlugins: true,
88
- supportKnipPlugins: true,
88
+
89
89
  options: { verbose: false, fastMode: true, selfHealing: true },
90
90
  enabledPlugins: ["nextjs", "nuxt", "remix", "sveltekit", "astro"]
91
91
  };
@@ -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 || '4.0.0'} Engine Activation`));
135
+ console.log(ansis.bold.green(`\nšŸ“¦ entkapp v${packageJsonContent.version || '5.0.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/docs.zip ADDED
Binary file
@@ -5,7 +5,6 @@
5
5
  // Plugin activation toggles
6
6
  "useBuiltinPlugins": true,
7
7
  "useCustomPlugins": true,
8
- "supportKnipPlugins": true,
9
8
 
10
9
  // Engine performance and behavior options
11
10
  "options": {
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": "4.5.0",
4
+ "version": "5.0.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",
@@ -77,16 +77,15 @@
77
77
  },
78
78
  "homepage": "https://dreamlongyt.github.io/entkapp/",
79
79
  "dependencies": {
80
- "ansis": "^3.0.0",
81
- "commander": "^12.0.0",
82
- "enhanced-resolve": "^5.16.0",
80
+ "ansis": "^3.17.0",
81
+ "commander": "^12.1.0",
82
+ "enhanced-resolve": "^5.24.0",
83
83
  "execa": "^8.0.1",
84
84
  "oxc-parser": "^0.135.0",
85
- "typescript": "^5.0.0"
85
+ "typescript": "^5.9.3"
86
86
  },
87
87
  "devDependencies": {
88
88
  "@types/node": "^25.9.3",
89
- "knip": "^6.17.1",
90
89
  "vitepress": "^1.6.4",
91
90
  "vue": ">=3.5.0"
92
91
  },
@@ -1,4 +1,6 @@
1
+ import fsSync from 'fs';
1
2
  import path from 'path';
3
+ import ansis from 'ansis';
2
4
 
3
5
  /**
4
6
  * Core In-Memory Graph Node Representation.
@@ -36,76 +38,62 @@ export class GraphNode {
36
38
  if (visited.has(visitKey)) return false;
37
39
  visited.add(visitKey);
38
40
 
39
- if (this.isLibraryEntry) return true;
40
-
41
- // --- NEW: Self-reference check (Fix for SecretSeverity bug) ---
42
- // If the symbol is used within the same file, it is NOT dead.
41
+ // Self-reference check
43
42
  if (this.instantiatedIdentifiers.has(symbolName)) return true;
44
-
45
- // Check property access in the same file
46
43
  for (const accessChain of this.propertyAccessChains) {
47
- if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`)) {
48
- return true;
49
- }
44
+ if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`)) return true;
50
45
  }
51
- // --------------------------------------------------------------
52
46
 
53
47
  for (const parentPath of this.incomingEdges) {
54
48
  const parentNode = projectGraph.get(parentPath);
55
49
  if (!parentNode) continue;
56
50
 
57
- // Strategy 1: Check absolute path based tokens
51
+ // Direct Import Tokens
58
52
  const absoluteImportKey = `${this.filePath}:${symbolName}`;
59
53
  const absoluteStarKey = `${this.filePath}:*`;
54
+ if (parentNode.importedSymbols.has(absoluteImportKey) || parentNode.importedSymbols.has(absoluteStarKey)) return true;
60
55
 
61
- if (parentNode.importedSymbols.has(absoluteImportKey) || parentNode.importedSymbols.has(absoluteStarKey)) {
62
- return true;
56
+ // Advanced Member Usage Detection
57
+ if (parentNode.propertyAccessChains.has(symbolName)) return true;
58
+
59
+ // If it's a member access like 'Logger.error' or 'app.start', we check for the leaf name usage.
60
+ if (symbolName.includes('.')) {
61
+ const parts = symbolName.split('.');
62
+ const leafName = parts[parts.length - 1];
63
+ const parentName = parts[0];
64
+
65
+ if ((parentNode.instantiatedIdentifiers.has(parentName) ||
66
+ parentNode.importedSymbols.has(`${this.filePath}:${parentName}`)) &&
67
+ parentNode.instantiatedIdentifiers.has(leafName)) {
68
+ return true;
69
+ }
63
70
  }
64
71
 
65
- // Strategy 2: Check relative path based tokens
66
- const relativePath = path.relative(path.dirname(parentPath), this.filePath).replace(/\\/g, '/');
67
- const relativePathNoExt = relativePath.replace(/\.(js|ts|tsx|jsx)$/, '');
68
-
69
- const importKey = `${relativePath}:${symbolName}`;
70
- const importKeyAlt = `${relativePathNoExt}:${symbolName}`;
71
- const starKey = `${relativePath}:*`;
72
- const starKeyAlt = `${relativePathNoExt}:*`;
73
-
74
- if (parentNode.importedSymbols.has(importKey) || parentNode.importedSymbols.has(importKeyAlt) ||
75
- parentNode.importedSymbols.has(starKey) || parentNode.importedSymbols.has(starKeyAlt)) {
76
- return true;
72
+ // Dynamic Import Check
73
+ if (parentNode.dynamicImports.has(this.filePath) || parentNode.explicitImports.has(this.filePath)) {
74
+ if (parentNode.dynamicImports.has(this.filePath)) return true;
77
75
  }
78
76
 
79
- // NEW: Check for re-exports that might use this symbol (Recursive Barrel Resolution)
77
+ // Recursive Re-export Resolution
80
78
  for (const [exportedName, exportMeta] of parentNode.internalExports.entries()) {
81
- const isMatch = (exportMeta.type === 're-export' || exportMeta.type === 're-export-namespace') &&
82
- (exportMeta.originalName === symbolName || exportMeta.originalName === '*') &&
79
+ const isMatch = (exportMeta.type === 're-export' || exportMeta.type === 're-export-namespace' || exportMeta.type === 're-export-all') &&
80
+ (exportMeta.originalName === symbolName || exportMeta.originalName === '*' || exportMeta.type === 're-export-all') &&
83
81
  (exportMeta.source === this.filePath || (exportMeta.source && path.resolve(path.dirname(parentPath), exportMeta.source) === this.filePath));
84
82
 
85
83
  if (isMatch) {
86
- // If this parent re-exports our symbol, check if the re-exported symbol is used
87
- if (parentNode.isSymbolReferencedExternally(exportedName, projectGraph, visited)) {
88
- return true;
89
- }
84
+ if (parentNode.isSymbolReferencedExternally(exportedName, projectGraph, visited)) return true;
90
85
  }
91
86
  }
92
87
 
88
+ // Fuzzy / Dynamic usage (Identifiers, Strings, JSX, Decorators)
93
89
  if (parentNode.instantiatedIdentifiers.has(symbolName)) return true;
94
-
95
- for (const accessChain of parentNode.propertyAccessChains) {
96
- if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`)) {
97
- return true;
98
- }
99
- }
100
-
101
90
  if (parentNode.rawStringReferences.has(symbolName)) return true;
102
91
  if (parentNode.jsxComponents.has(symbolName)) return true;
103
-
104
- for (const jsxProp of parentNode.jsxProps) {
105
- if (jsxProp.endsWith(`:${symbolName}`)) return true;
106
- }
107
-
108
92
  if (parentNode.decorators.has(symbolName)) return true;
93
+
94
+ for (const accessChain of parentNode.propertyAccessChains) {
95
+ if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`)) return true;
96
+ }
109
97
  }
110
98
 
111
99
  return false;
@@ -114,14 +102,13 @@ export class GraphNode {
114
102
 
115
103
  export class EngineContext {
116
104
  constructor(cwd) {
117
- // FIX: Ensure cwd is always defined, fallback to process.cwd()
118
105
  this.cwd = cwd || process.cwd();
119
106
  this.projectGraph = new Map(); // Path -> GraphNode
120
107
  this.usedExternalPackages = new Set();
121
- this.unimportedUsedPackages = new Set(); // NEW: For "Unimported but used"
122
- this.importedUnusedPackages = new Set(); // NEW: For "Imported but unused"
123
- this.unusedBinaries = new Set(); // NEW: For "Unused Binaries"
124
- this.manifestDependencies = new Map(); // NEW: Store dependencies from package.json
108
+ this.unimportedUsedPackages = new Set();
109
+ this.importedUnusedPackages = new Set();
110
+ this.unusedBinaries = new Set();
111
+ this.manifestDependencies = new Map();
125
112
 
126
113
  this.isWorkspaceEnabled = false;
127
114
  this.monorepoPackageRoots = new Set();
@@ -137,62 +124,273 @@ export class EngineContext {
137
124
  return this.projectGraph.get(filePath);
138
125
  }
139
126
 
140
- generateSummaryReport() {
127
+ async generateSummaryReport() {
141
128
  const report = {
142
129
  orphanedFiles: [],
143
130
  deadExports: [],
144
131
  unusedDependencies: [],
145
- unimportedUsedPackages: [], // NEW
146
- importedUnusedPackages: [], // NEW
147
- unusedBinaries: [] // NEW
132
+ unimportedUsedPackages: [],
133
+ importedUnusedPackages: [],
134
+ unusedBinaries: []
148
135
  };
149
136
 
150
- // 1. Files & Exports
137
+ // --- DEEP REACHABILITY ANALYSIS (BFS) ---
138
+ const reachableFiles = new Set();
139
+ const queue = [];
140
+
141
+ // 1. Initialize BFS with Entry Points
142
+ for (const [filePath, node] of this.projectGraph.entries()) {
143
+ // FIX: Also check if the file is explicitly mentioned in package.json bin or main
144
+ if (node.isEntry || node.isLibraryEntry || node.isFrameworkComponent) {
145
+ reachableFiles.add(filePath);
146
+ queue.push(filePath);
147
+ }
148
+ }
149
+
150
+ // UPGRADE: Ensure package.json main and bin are always entry points
151
+ // UPGRADE: Comprehensive Entry Point Discovery (Manifests + Tooling)
152
+ for (const [manifestPath, deps] of this.manifestDependencies.entries()) {
153
+ try {
154
+ const data = JSON.parse(fsSync.readFileSync(manifestPath, 'utf8'));
155
+ const pkgDir = path.dirname(manifestPath);
156
+ const entries = [];
157
+
158
+ // 1. Standard Manifest Entries
159
+ if (data.main) entries.push(path.resolve(pkgDir, data.main));
160
+ if (data.module) entries.push(path.resolve(pkgDir, data.module));
161
+ if (data.exports) {
162
+ const unwind = (val) => {
163
+ if (typeof val === 'string') entries.push(path.resolve(pkgDir, val));
164
+ else if (typeof val === 'object' && val !== null) Object.values(val).forEach(unwind);
165
+ };
166
+ unwind(data.exports);
167
+ }
168
+ if (data.bin) {
169
+ if (typeof data.bin === 'string') entries.push(path.resolve(pkgDir, data.bin));
170
+ else Object.values(data.bin).forEach(b => entries.push(path.resolve(pkgDir, b)));
171
+ }
172
+
173
+ // 2. Protect Documentation & Configs
174
+ const possibleConfigs = ['vite.config.js', 'vite.config.ts', 'vitest.config.ts', 'tsconfig.json'];
175
+ possibleConfigs.forEach(c => entries.push(path.resolve(pkgDir, c)));
176
+
177
+ entries.forEach(e => {
178
+ const normalized = e.replace(/\\/g, '/');
179
+ // Check with common extensions if not found
180
+ const candidates = [normalized, normalized + '.js', normalized + '.ts', normalized + '.tsx', normalized + '/index.js', normalized + '/index.ts'];
181
+ for (const cand of candidates) {
182
+ if (this.projectGraph.has(cand) && !reachableFiles.has(cand)) {
183
+ reachableFiles.add(cand);
184
+ queue.push(cand);
185
+ this.projectGraph.get(cand).isEntry = true;
186
+ break;
187
+ }
188
+ }
189
+ });
190
+
191
+ // --- PLUGIN-BASED ECOSYSTEM DETECTION ---
192
+ // Plugins will now handle their own erreichbarkeit and dependency validation.
193
+ if (this.pluginRegistry) {
194
+ const activePlugins = await this.pluginRegistry.getActivePlugins(pkgDir);
195
+ for (const plugin of activePlugins) {
196
+ if (typeof plugin.onDiscovery === 'function') {
197
+ await plugin.onDiscovery({
198
+ pkgDir,
199
+ data,
200
+ reachableFiles,
201
+ queue,
202
+ projectGraph: this.projectGraph,
203
+ context: this
204
+ });
205
+ }
206
+ }
207
+ }
208
+ } catch (e) {}
209
+ }
210
+
211
+ // 2. BFS Traversal
212
+ while (queue.length > 0) {
213
+ const currentPath = queue.shift();
214
+ const node = this.projectGraph.get(currentPath);
215
+
216
+ if (!node) continue;
217
+
218
+ // Track outgoing edges (static and linked dynamic imports)
219
+ if (node.outgoingEdges) {
220
+ for (const outgoingPath of node.outgoingEdges) {
221
+ const normalizedPath = outgoingPath.replace(/\\/g, '/');
222
+ if (!reachableFiles.has(normalizedPath)) {
223
+ reachableFiles.add(normalizedPath);
224
+ queue.push(normalizedPath);
225
+ }
226
+ }
227
+ }
228
+
229
+ // UPGRADE: Handle non-literal dynamic imports (calculatedDynamicImports)
230
+ if ((node.calculatedDynamicImports && node.calculatedDynamicImports.length > 0) || (node.dynamicImports && node.dynamicImports.has('__DYNAMIC_PATTERN__'))) {
231
+ const dir = path.dirname(currentPath);
232
+ // Conservative: If we have a calculated dynamic import, any file in the same or sub directory could be a target
233
+ for (const [otherPath, _] of this.projectGraph.entries()) {
234
+ // Fix: Check if otherPath is in the same directory or a subdirectory of 'dir'
235
+ if (otherPath !== currentPath && otherPath.startsWith(dir) && !reachableFiles.has(otherPath)) {
236
+ // Additional check: If it's in a 'plugins' or 'modules' folder, it's very likely a dynamic target
237
+ const isLikelyDynamicTarget = otherPath.includes('/plugins/') || otherPath.includes('/modules/') || otherPath.includes('/dynamic/');
238
+ if (isLikelyDynamicTarget) {
239
+ reachableFiles.add(otherPath);
240
+ queue.push(otherPath);
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ // UPGRADE: Handle Worker/Threads usage (new Worker('path'))
247
+ for (const chain of node.propertyAccessChains) {
248
+ if (chain.includes('new Worker(') || chain.includes('Worker(')) {
249
+ // Check if any file path is mentioned in raw strings
250
+ for (const str of node.rawStringReferences) {
251
+ if (str.startsWith('.') || str.startsWith('/')) {
252
+ try {
253
+ const resolved = path.resolve(path.dirname(currentPath), str);
254
+ const extensions = ['.js', '.ts', '.mjs', '.cjs'];
255
+ for (const ext of extensions) {
256
+ const p = resolved.endsWith(ext) ? resolved : resolved + ext;
257
+ const normalized = p.replace(/\\/g, '/');
258
+ if (this.projectGraph.has(normalized) && !reachableFiles.has(normalized)) {
259
+ reachableFiles.add(normalized);
260
+ queue.push(normalized);
261
+ }
262
+ }
263
+ } catch (e) {}
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ // Additional check for unlinked dynamic imports (conservative)
270
+ if (node.dynamicImports) {
271
+ for (const dynamicPath of node.dynamicImports) {
272
+ if (dynamicPath.startsWith('__DYNAMIC_PATTERN__:')) {
273
+ const pattern = dynamicPath.split(':')[1];
274
+ const dir = path.dirname(currentPath);
275
+ for (const [otherPath, _] of this.projectGraph.entries()) {
276
+ if (otherPath.startsWith(dir) && !reachableFiles.has(otherPath)) {
277
+ reachableFiles.add(otherPath);
278
+ queue.push(otherPath);
279
+ }
280
+ }
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ // --- ANALYSIS PHASE ---
287
+ if (this.verbose) {
288
+ console.log(`[Reachability] Total reachable files: ${reachableFiles.size}`);
289
+ for (const f of reachableFiles) console.log(` • Reachable: ${path.relative(this.cwd, f)}`);
290
+ }
151
291
  for (const [filePath, node] of this.projectGraph.entries()) {
152
292
  const isSuppressed = node.localSuppressedRules.has('*') || node.localSuppressedRules.has('unused-file');
293
+ const isReachable = reachableFiles.has(filePath);
153
294
 
154
- if (node.incomingEdges.size === 0 && !node.isEntry && !node.isLibraryEntry && !node.isFrameworkComponent && !isSuppressed) {
155
- const relPath = path.relative(this.cwd, filePath);
295
+ // Check for Orphaned Files
296
+ if (!isReachable && !isSuppressed) {
297
+ const relPath = path.relative(this.cwd, filePath).replace(/\\/g, '/');
156
298
  report.orphanedFiles.push(relPath);
157
- if (this.verbose) {
158
- console.log(`[Orphan Auditor] File marked as orphan: ${relPath}`);
159
- console.log(` - Incoming Edges: ${node.incomingEdges.size}`);
160
- console.log(` - isEntry: ${node.isEntry}, isLibraryEntry: ${node.isLibraryEntry}, isFrameworkComponent: ${node.isFrameworkComponent}`);
161
- console.log(` - Suppressed: ${isSuppressed}`);
162
- }
163
299
  }
164
300
 
165
- for (const [symbol, meta] of node.internalExports.entries()) {
166
- if (symbol === '*' || symbol === 'default' || node.localSuppressedRules.has('unused-export') || node.localSuppressedRules.has(`unused-export:${symbol}`)) {
167
- continue;
301
+ // Check for Dead Exports
302
+ if (isReachable) {
303
+ const isPublicAPI = node.isEntry || node.isLibraryEntry;
304
+
305
+ for (const [symbol, meta] of node.internalExports.entries()) {
306
+ if (symbol === '*' || symbol === 'default' || node.localSuppressedRules.has('unused-export') || node.localSuppressedRules.has(`unused-export:${symbol}`)) {
307
+ continue;
308
+ }
309
+
310
+ const isSymbolUsed = isPublicAPI || node.isSymbolReferencedExternally(symbol, this.projectGraph);
311
+
312
+ if (!isSymbolUsed) {
313
+ const loc = node.symbolSourceLocations.get(symbol) || { line: 0, column: 0 };
314
+ report.deadExports.push({
315
+ symbol,
316
+ file: path.relative(this.cwd, filePath),
317
+ line: loc.line
318
+ });
319
+ }
320
+
321
+ // Member Analysis
322
+ if (meta.members && meta.members.length > 0 && isSymbolUsed) {
323
+ for (const member of meta.members) {
324
+ const fullMemberName = `${symbol}.${member.name}`;
325
+
326
+ if (isPublicAPI && member.isPublic !== false) {
327
+ continue;
328
+ }
329
+
330
+ // UPGRADE: Check for dynamic usage in ANY reachable file, not just direct imports
331
+ let isMemberUsed = false;
332
+ for (const [otherPath, otherNode] of this.projectGraph.entries()) {
333
+ if (reachableFiles.has(otherPath)) {
334
+ if (otherNode.instantiatedIdentifiers.has(member.name) ||
335
+ otherNode.propertyAccessChains.has(fullMemberName) ||
336
+ otherNode.rawStringReferences.has(member.name)) {
337
+ isMemberUsed = true;
338
+ break;
339
+ }
340
+ }
341
+ }
342
+
343
+ if (!isMemberUsed && !node.isSymbolReferencedExternally(fullMemberName, this.projectGraph)) {
344
+ const loc = node.symbolSourceLocations.get(fullMemberName) || { line: 0, column: 0 };
345
+ report.deadExports.push({
346
+ symbol: fullMemberName,
347
+ file: path.relative(this.cwd, filePath),
348
+ line: loc.line
349
+ });
350
+ }
351
+ }
352
+ }
168
353
  }
354
+ }
355
+ }
169
356
 
170
- if (!node.isSymbolReferencedExternally(symbol, this.projectGraph)) {
171
- const loc = node.symbolSourceLocations.get(symbol) || { line: 0, column: 0 };
172
- report.deadExports.push({
173
- symbol,
174
- file: path.relative(this.cwd, filePath),
175
- line: loc.line
357
+ // --- DEPENDENCY ANALYSIS ---
358
+ const usedByReachableFiles = new Set();
359
+ reachableFiles.forEach(filePath => {
360
+ const node = this.projectGraph.get(filePath);
361
+ if (node) {
362
+ node.externalPackageUsage.forEach(pkg => usedByReachableFiles.add(pkg));
363
+
364
+ // Conservative check for dynamic imports in reachable files
365
+ if (node.calculatedDynamicImports) {
366
+ node.calculatedDynamicImports.forEach(entry => {
367
+ const expr = typeof entry === 'string' ? entry : (entry.pattern || entry.text || '');
368
+ // If it looks like a package name (no dots/slashes), assume it's used
369
+ if (expr && !expr.includes('.') && !expr.includes('/') && !expr.includes('`') && !expr.includes("'") && !expr.includes('"')) {
370
+ usedByReachableFiles.add(expr);
371
+ }
372
+ // Check raw strings for potential package names
373
+ node.rawStringReferences.forEach(str => {
374
+ if (!str.startsWith('.') && !str.startsWith('/')) {
375
+ const pkg = str.startsWith('@') ? str.split('/').slice(0, 2).join('/') : str.split('/')[0];
376
+ usedByReachableFiles.add(pkg);
377
+ }
378
+ });
176
379
  });
177
380
  }
178
381
  }
179
- }
382
+ });
180
383
 
181
- // 2. Dependency Analysis (Extended)
182
- // Compare manifest dependencies with actually used packages
183
384
  for (const [manifestPath, deps] of this.manifestDependencies.entries()) {
184
385
  const allDeps = [...(deps.dependencies || []), ...(deps.devDependencies || [])];
185
-
186
386
  const hasTypeScriptFiles = Array.from(this.projectGraph.keys()).some(f => f.endsWith('.ts') || f.endsWith('.tsx'));
187
387
 
188
388
  for (const dep of allDeps) {
189
- // Skip @types packages and known safe packages
190
389
  if (dep.startsWith('@types/') || dep === 'entkapp' || (dep === 'typescript' && hasTypeScriptFiles)) {
191
390
  continue;
192
391
  }
193
392
 
194
- // Check if the dependency is actually used in the code
195
- if (!this.usedExternalPackages.has(dep)) {
393
+ if (!usedByReachableFiles.has(dep)) {
196
394
  report.unusedDependencies.push({
197
395
  package: dep,
198
396
  type: deps.dependencies.includes(dep) ? 'dependency' : 'devDependency',