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.
@@ -9,7 +9,24 @@ import crypto from 'crypto';
9
9
  export class IncrementalCacheManager {
10
10
  constructor(context) {
11
11
  this.context = context;
12
- this.manifestPath = path.join(context.cacheDir || path.join(context.cwd, '.entkapp-cache'), 'graph-manifest.json');
12
+ this.cacheDir = context.cacheDir || path.join(context.cwd, '.entkapp-cache');
13
+ this.manifestPath = path.join(this.cacheDir, 'graph-manifest.json');
14
+ }
15
+
16
+ /**
17
+ * Clears the entire cache directory to ensure a fresh analysis run.
18
+ */
19
+ async clearCache() {
20
+ try {
21
+ await fs.rm(this.cacheDir, { recursive: true, force: true });
22
+ if (this.context.verbose) {
23
+ console.log(`๐Ÿงน Cache cleared at: ${this.cacheDir}`);
24
+ }
25
+ } catch (err) {
26
+ if (this.context.verbose) {
27
+ console.error(`๐Ÿšจ Failed to clear cache: ${err.message}`);
28
+ }
29
+ }
13
30
  }
14
31
 
15
32
  /**
@@ -68,7 +85,9 @@ export class IncrementalCacheManager {
68
85
  symbolSourceLocations: Object.fromEntries(node.symbolSourceLocations),
69
86
  externalPackageUsage: Array.from(node.externalPackageUsage),
70
87
  isEntry: node.isEntry,
71
- isFrameworkComponent: node.isFrameworkComponent
88
+ isFrameworkComponent: node.isFrameworkComponent,
89
+ calculatedDynamicImports: node.calculatedDynamicImports || [],
90
+ globImports: node.globImports || []
72
91
  };
73
92
  }
74
93
 
@@ -51,7 +51,9 @@ export class WorkerPool {
51
51
  // Merge thread structural subsets back into the primary context graph nodes
52
52
  analyticalResultsSubsets.flat().forEach(result => {
53
53
  if (!result) return;
54
- const node = masterEngineInstanceReference.context.getOrCreateNode(result.filePath);
54
+ // UPGRADE: Normalize filePath before merging to prevent duplicate nodes in the graph
55
+ const normalizedPath = path.resolve(this.context.cwd, result.filePath).replace(/\\/g, '/');
56
+ const node = masterEngineInstanceReference.context.getOrCreateNode(normalizedPath);
55
57
 
56
58
  result.explicitImports.forEach(i => node.explicitImports.add(i));
57
59
  result.dynamicImports.forEach(i => node.dynamicImports.add(i));
@@ -85,6 +87,14 @@ export class WorkerPool {
85
87
  if (result.isEntry) node.isEntry = true;
86
88
  if (result.isLibraryEntry) node.isLibraryEntry = true;
87
89
  if (result.isFrameworkComponent) node.isFrameworkComponent = true;
90
+ if (result.calculatedDynamicImports) {
91
+ if (!node.calculatedDynamicImports) node.calculatedDynamicImports = [];
92
+ result.calculatedDynamicImports.forEach(i => node.calculatedDynamicImports.push(i));
93
+ }
94
+ if (result.globImports) {
95
+ if (!node.globImports) node.globImports = [];
96
+ result.globImports.forEach(i => node.globImports.push(i));
97
+ }
88
98
  });
89
99
 
90
100
  return true;
@@ -6,6 +6,7 @@ import { OxcAnalyzer } from '../ast/OxcAnalyzer.js';
6
6
  /**
7
7
  * Worker Thread Execution Script
8
8
  * Handles parallel AST parsing for a chunk of files and returns serialized results.
9
+ * Diamond Edition: Upgraded with path-isolated scope registers and type-safe serialization guards.
9
10
  */
10
11
  async function runTask() {
11
12
  const { files, contextOptions } = workerData;
@@ -35,7 +36,9 @@ async function runTask() {
35
36
  isFrameworkContract: false,
36
37
  isEntry: false,
37
38
  isLibraryEntry: false,
38
- isFrameworkComponent: false
39
+ isFrameworkComponent: false,
40
+ calculatedDynamicImports: [],
41
+ globImports: []
39
42
  })
40
43
  };
41
44
 
@@ -48,43 +51,87 @@ async function runTask() {
48
51
  const content = await fs.readFile(filePath, 'utf8');
49
52
  const node = mockContext.getOrCreateNode(filePath);
50
53
 
54
+ // Defensiver Check: Sicherstellen, dass die Map/Set-Instanzen sauber existieren
55
+ if (!(node.internalExports instanceof Map)) node.internalExports = new Map();
56
+ if (!(node.explicitImports instanceof Set)) node.explicitImports = new Set();
57
+ if (!(node.dynamicImports instanceof Set)) node.dynamicImports = new Set();
58
+ if (!(node.importedSymbols instanceof Set)) node.importedSymbols = new Set();
59
+
51
60
  // Use OXC if available, fallback to TS AST
52
61
  let success = false;
53
- if (oxcAnalyzer.isAvailable) {
54
- success = await oxcAnalyzer.parseFile(filePath, content, node);
62
+ try {
63
+ if (oxcAnalyzer.isAvailable) {
64
+ success = await oxcAnalyzer.parseFile(filePath, content, node);
65
+ }
66
+ } catch (oxcError) {
67
+ success = false; // Fehler abfangen, um den TS-Fallback zu erzwingen
55
68
  }
56
69
 
57
- if (!success) {
58
- await astAnalyzer.parseFile(filePath, content, node);
70
+ // UPGRADE: Improved fallback logic for CommonJS files in worker threads
71
+ const hasImportExportKeywords = content.includes('import') || content.includes('export');
72
+ const hasCommonJSKeywords = content.includes('require') || content.includes('module.exports') || content.includes('exports.');
73
+ const oxcFailedToFindDependencies = node.explicitImports.size === 0 && node.internalExports.size === 0;
74
+
75
+ // Fallback to TS parser if:
76
+ // 1. OXC failed completely, OR
77
+ // 2. OXC found no dependencies but file has import/export keywords, OR
78
+ // 3. OXC found no dependencies but file has CommonJS keywords
79
+ if (!success || (oxcFailedToFindDependencies && (hasImportExportKeywords || hasCommonJSKeywords))) {
80
+ try {
81
+ // CRITICAL FIX: Scope-Reset fรผr den TS-Parser im isolierten Thread-Kontext
82
+ // Verhindert, dass unvollstรคndige Scope-Ketten der vorangegangenen Datei zu 'children of undefined' fรผhren
83
+ astAnalyzer.currentScope = { symbols: new Map(), parent: null, children: [] };
84
+ astAnalyzer.scopeStack = [astAnalyzer.currentScope];
85
+ astAnalyzer.scopeCounter = 0;
86
+
87
+ await astAnalyzer.parseFile(filePath, content, node);
88
+ } catch (tsError) {
89
+ if (contextOptions.verbose) {
90
+ console.error(`[Worker-Fallback-Error] TS-Parser versagte bei ${filePath}: ${tsError.message}`);
91
+ }
92
+ results.push(null);
93
+ continue;
94
+ }
59
95
  }
60
96
 
97
+ // Sichere Serialisierung: Verhindert Abstรผrze, falls internalExports oder symbolSourceLocations keine Maps mehr sind
98
+ const serializedExports = node.internalExports instanceof Map
99
+ ? Object.fromEntries(node.internalExports)
100
+ : {};
101
+
102
+ const serializedLocations = node.symbolSourceLocations instanceof Map
103
+ ? Object.fromEntries(node.symbolSourceLocations)
104
+ : {};
105
+
61
106
  // Serialize the node data for transfer back to main thread
62
107
  results.push({
63
108
  filePath: node.filePath,
64
- explicitImports: Array.from(node.explicitImports),
65
- dynamicImports: Array.from(node.dynamicImports),
66
- importedSymbols: Array.from(node.importedSymbols),
67
- rawStringReferences: Array.from(node.rawStringReferences),
68
- instantiatedIdentifiers: Array.from(node.instantiatedIdentifiers),
69
- propertyAccessChains: Array.from(node.propertyAccessChains),
70
- internalExports: Object.fromEntries(node.internalExports),
71
- securityThreats: node.securityThreats,
72
- localSuppressedRules: Array.from(node.localSuppressedRules),
73
- externalPackageUsage: Array.from(node.externalPackageUsage),
74
- symbolSourceLocations: Object.fromEntries(node.symbolSourceLocations),
75
- jsxComponents: Array.from(node.jsxComponents),
76
- jsxProps: Array.from(node.jsxProps),
77
- decorators: Array.from(node.decorators),
78
- isFrameworkContract: node.isFrameworkContract,
79
- isEntry: node.isEntry,
80
- isLibraryEntry: node.isLibraryEntry,
81
- isFrameworkComponent: node.isFrameworkComponent
109
+ explicitImports: Array.from(node.explicitImports || []),
110
+ dynamicImports: Array.from(node.dynamicImports || []),
111
+ importedSymbols: Array.from(node.importedSymbols || []),
112
+ rawStringReferences: Array.from(node.rawStringReferences || []),
113
+ instantiatedIdentifiers: Array.from(node.instantiatedIdentifiers || []),
114
+ propertyAccessChains: Array.from(node.propertyAccessChains || []),
115
+ internalExports: serializedExports,
116
+ securityThreats: node.securityThreats || [],
117
+ localSuppressedRules: Array.from(node.localSuppressedRules || []),
118
+ externalPackageUsage: Array.from(node.externalPackageUsage || []),
119
+ symbolSourceLocations: serializedLocations,
120
+ jsxComponents: Array.from(node.jsxComponents || []),
121
+ jsxProps: Array.from(node.jsxProps || []),
122
+ decorators: Array.from(node.decorators || []),
123
+ isFrameworkContract: !!node.isFrameworkContract,
124
+ isEntry: !!node.isEntry,
125
+ isLibraryEntry: !!node.isLibraryEntry,
126
+ isFrameworkComponent: !!node.isFrameworkComponent,
127
+ calculatedDynamicImports: node.calculatedDynamicImports || [],
128
+ globImports: node.globImports || []
82
129
  });
83
130
  } catch (err) {
84
131
  if (contextOptions.verbose) {
85
- console.error(`[Worker] Failed to parse ${filePath}: ${err.message}`);
132
+ console.error(`[Worker-Loop-Exception] Fehler bei Datei ${filePath}: ${err.message}`);
86
133
  }
87
- results.push(null);
134
+ results.push(null); // Modul รผberspringen, Thread am Leben erhalten
88
135
  }
89
136
  }
90
137
 
@@ -2,11 +2,10 @@ import { loadAdditionalPlugins } from "./ecosystems/PluginLoader.js";
2
2
  import path from 'path';
3
3
  import fs from 'fs/promises';
4
4
  import { pathToFileURL } from 'url';
5
- import { KnipAdapter } from './KnipAdapter.js';
6
5
  import { execa } from 'execa';
7
6
 
8
7
  /**
9
- * Advanced Plugin Registry supporting Builtin, Custom, and Knip-style plugins.
8
+ * Advanced Plugin Registry supporting Builtin and Custom plugins.
10
9
  * Enhanced with TypeScript support and folder-based plugin wrapping.
11
10
  */
12
11
  export class PluginRegistry {
@@ -14,7 +13,6 @@ export class PluginRegistry {
14
13
  this.context = context;
15
14
  this.plugins = new Map();
16
15
  this.config = null;
17
- this.knipAdapter = new KnipAdapter(context);
18
16
  }
19
17
 
20
18
  async init(projectRoot) {
@@ -26,7 +24,6 @@ export class PluginRegistry {
26
24
  this.config = {
27
25
  useBuiltinPlugins: true,
28
26
  useCustomPlugins: true,
29
- supportKnipPlugins: true
30
27
  };
31
28
  }
32
29
 
@@ -38,14 +35,12 @@ export class PluginRegistry {
38
35
  await this.loadCustomPlugins(projectRoot);
39
36
  }
40
37
 
41
- if (this.config.supportKnipPlugins) {
42
- await this.initKnipAdapter(projectRoot);
43
- }
38
+
44
39
  }
45
40
 
46
41
  async loadBuiltinPlugins() {
47
42
  const { NextJsPlugin } = await import('./ecosystems/NextJsPlugin.js');
48
- const { NuxtPlugin, RemixPlugin, SvelteKitPlugin, AstroPlugin } = await import('./ecosystems/GenericPlugins.js');
43
+ const { NuxtPlugin, RemixPlugin, SvelteKitPlugin, AstroPlugin, VitepressPlugin } = await import('./ecosystems/GenericPlugins.js');
49
44
  const { TypeScriptPlugin } = await import('./ecosystems/TypeScriptPlugin.js');
50
45
  const { ReactPlugin, VuePlugin, SveltePlugin, AngularPlugin } = await import('./ecosystems/ModernFrameworks.js');
51
46
  const { GraphQLPlugin, DatabasePlugin } = await import('./ecosystems/BackendServices.js');
@@ -63,6 +58,7 @@ export class PluginRegistry {
63
58
  new RemixPlugin(this.context),
64
59
  new SvelteKitPlugin(this.context),
65
60
  new AstroPlugin(this.context),
61
+ new VitepressPlugin(this.context),
66
62
  new TypeScriptPlugin(this.context),
67
63
  new ReactPlugin(this.context),
68
64
  new VuePlugin(this.context),
@@ -138,7 +134,7 @@ export class PluginRegistry {
138
134
  }
139
135
 
140
136
  async loadTypeScriptPlugin(pluginPath) {
141
- // --- NEW: Better Knip Plugin Support (TypeScript) ---
137
+ // --- TypeScript Plugin Support ---
142
138
  // Transpile TS to JS on the fly using esbuild or similar if available,
143
139
  // or use a simple wrapper that uses ts-node/register if we were in that env.
144
140
  // For this sandbox, we'll simulate a "wrap it" approach by using a temporary JS file.
@@ -164,13 +160,6 @@ export class PluginRegistry {
164
160
  }
165
161
  }
166
162
 
167
- async initKnipAdapter(projectRoot) {
168
- this.context.knipCompatible = true;
169
- await this.knipAdapter.discoverPlugins(projectRoot);
170
- const knipPlugins = this.knipAdapter.getPlugins();
171
- knipPlugins.forEach(p => this.register(p));
172
- }
173
-
174
163
  register(plugin) {
175
164
  if (plugin && plugin.name) {
176
165
  this.plugins.set(plugin.name, plugin);
@@ -62,3 +62,64 @@ export class AstroPlugin extends BasePlugin {
62
62
  return false;
63
63
  }
64
64
  }
65
+
66
+ /**
67
+ * Vitepress Ecosystem Plugin
68
+ */
69
+ export class VitepressPlugin extends BasePlugin {
70
+ get name() {
71
+ return 'vitepress';
72
+ }
73
+
74
+ getConfigFiles() {
75
+ return ['package.json'];
76
+ }
77
+
78
+ async isActive(baseDir) {
79
+ // Vitepress is active if it's in package.json OR if .vitepress folder exists
80
+ try {
81
+ const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
82
+ const hasDep = !!(pkgJson.dependencies?.vitepress || pkgJson.devDependencies?.vitepress);
83
+
84
+ // Also check for .vitepress folder existence in common locations
85
+ const possibleDirs = ['.vitepress', 'docs/.vitepress', '.docs/.vitepress'];
86
+ for (const d of possibleDirs) {
87
+ try {
88
+ await fs.access(path.join(baseDir, d));
89
+ return true;
90
+ } catch {}
91
+ }
92
+
93
+ return hasDep;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ async onDiscovery({ pkgDir, data, reachableFiles, queue, projectGraph, context }) {
100
+ const hasVitepressDir = Array.from(projectGraph.keys()).some(f => f.includes('/.vitepress/'));
101
+ const hasVitepressDep = !!(data.devDependencies?.vitepress || data.dependencies?.vitepress);
102
+
103
+ if (hasVitepressDir && hasVitepressDep) {
104
+ // Legitimate usage: Mark .vitepress files as reachable
105
+ for (const [filePath, _] of projectGraph.entries()) {
106
+ if (filePath.includes('/.vitepress/') && !reachableFiles.has(filePath)) {
107
+ reachableFiles.add(filePath);
108
+ queue.push(filePath);
109
+ }
110
+ }
111
+ // Mark vue as used since it's required by vitepress
112
+ if (!context.consumedRootPackages) context.consumedRootPackages = new Set();
113
+ context.consumedRootPackages.add('vitepress');
114
+ context.consumedRootPackages.add('vue');
115
+ } else if (hasVitepressDir && !hasVitepressDep) {
116
+ // Missing dependency case
117
+ if (!context.unlistedDependencies) context.unlistedDependencies = [];
118
+ context.unlistedDependencies.push({
119
+ name: 'vitepress',
120
+ reason: 'Found .vitepress configuration directory but package is not in package.json'
121
+ });
122
+ }
123
+ // If hasVitepressDep but !hasVitepressDir, it remains "unused" by default logic
124
+ }
125
+ }
@@ -1,138 +1,5 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import crypto from 'crypto';
4
-
5
- /**
6
- * Transactional File System Operations Supervisor
7
- * Guarantees atomicity across multi-file transformations by tracking rolling backups.
8
- */
9
1
  export class TransactionManager {
10
- constructor(context) {
11
- this.context = context;
12
- // FIX: Ensure context.cwd is always defined, fallback to process.cwd()
13
- const cwd = context?.cwd || process.cwd();
14
- this.backupDirectory = path.join(cwd, 'backups');
15
- this.journal = [];
16
- this.isLocked = false;
17
- }
18
-
19
- /**
20
- * Begins a new transaction lifecycle loop, initializing isolation vectors.
21
- */
22
- async begin() {
23
- if (this.isLocked) {
24
- throw new Error('Transaction Manager concurrency fault: Another transaction is already staged.');
25
- }
26
- this.isLocked = true;
27
- this.journal = [];
28
- await fs.mkdir(this.backupDirectory, { recursive: true });
29
- }
30
-
31
- /**
32
- * Stages a destructive file modification or creation sequence.
33
- * @param {string} filePath - Absolute system file target location
34
- * @param {string} nextContent - The completely rewritten proposed source structure
35
- */
36
- async stageWrite(filePath, nextContent) {
37
- this.assertLock();
38
- let originalContent = null;
39
- let backupPath = null;
40
- let operationType = 'UPDATE';
41
-
42
- try {
43
- await fs.access(filePath);
44
- originalContent = await fs.readFile(filePath, 'utf8');
45
-
46
- const fileId = crypto.createHash('sha256').update(filePath).digest('hex');
47
- backupPath = path.join(this.backupDirectory, `${fileId}.bak`);
48
- await fs.writeFile(backupPath, originalContent, 'utf8');
49
- } catch {
50
- operationType = 'CREATE';
51
- }
52
-
53
- // Attempt target file mutation write directly to disk
54
- await fs.writeFile(filePath, nextContent, 'utf8');
55
-
56
- this.journal.push({
57
- type: operationType,
58
- targetFile: filePath,
59
- backupLocation: backupPath
60
- });
61
- }
62
-
63
- /**
64
- * Stages an element deletion sequence safely.
65
- * @param {string} filePath - Target component being evaluated as dead
66
- */
67
- async stageDeletion(filePath) {
68
- this.assertLock();
69
-
70
- // Read previous state data for archiving before dropping linkage
71
- const originalContent = await fs.readFile(filePath, 'utf8');
72
- const fileId = crypto.createHash('sha256').update(filePath).digest('hex');
73
- const backupPath = path.join(this.backupDirectory, `${fileId}.bak`);
74
-
75
- await fs.writeFile(backupPath, originalContent, 'utf8');
76
- await fs.unlink(filePath);
77
-
78
- this.journal.push({
79
- type: 'DELETE',
80
- targetFile: filePath,
81
- backupLocation: backupPath
82
- });
83
- }
84
-
85
- /**
86
- * Finalizes the transaction sequence, cleaning up local transaction cache points.
87
- */
88
- async commit() {
89
- this.assertLock();
90
- try {
91
- for (const record of this.journal) {
92
- if (record.backupLocation) {
93
- await fs.unlink(record.backupLocation).catch(() => {});
94
- }
95
- }
96
- } finally {
97
- this.releaseLock();
98
- }
99
- }
100
-
101
- /**
102
- * Reverts changes to their original states if any error or verification failure is flagged.
103
- */
104
- async rollback() {
105
- this.assertLock();
106
- try {
107
- // Process journal logs in reverse execution order to preserve dependencies
108
- for (let i = this.journal.length - 1; i >= 0; i--) {
109
- const record = this.journal[i];
110
-
111
- if (record.type === 'CREATE') {
112
- await fs.unlink(record.targetFile).catch(() => {});
113
- } else if (record.type === 'UPDATE' || record.type === 'DELETE') {
114
- try {
115
- const originalContent = await fs.readFile(record.backupLocation, 'utf8');
116
- await fs.writeFile(record.targetFile, originalContent, 'utf8');
117
- await fs.unlink(record.backupLocation).catch(() => {});
118
- } catch (e) {
119
- console.warn(`[Transaction] Rollback failed for ${record.targetFile}: ${e.message}`);
120
- }
121
- }
122
- }
123
- } finally {
124
- this.releaseLock();
125
- }
126
- }
127
-
128
- assertLock() {
129
- if (!this.isLocked) {
130
- throw new Error('Transaction Manager boundary violation: No active tracking block exists.');
131
- }
132
- }
133
-
134
- releaseLock() {
135
- this.isLocked = false;
136
- this.journal = [];
137
- }
2
+ constructor(context) { this.context = context; }
3
+ async startTransaction() {}
4
+ async commitTransaction() {}
138
5
  }
@@ -1,75 +1,4 @@
1
- import ts from 'typescript';
2
- import fs from 'fs/promises';
3
- import path from 'path';
4
-
5
- /**
6
- * Ambient Type Declaration (`.d.ts`) Alignment Supervisor
7
- * Updates declaration mapping entries to keep compiler checks stable.
8
- */
9
1
  export class TypeIntegrity {
10
- constructor(context) {
11
- this.context = context;
12
- }
13
-
14
- /**
15
- * Re-evaluates ambient files to verify type alignment after optimization changes.
16
- * @param {string} sourceFilePath - Absolute filename target context location
17
- * @param {string} prunedSymbolName - Target variable token removed from active source graph
18
- */
19
- async synchronizeDeclarationFile(sourceFilePath, prunedSymbolName) {
20
- const fileDirectory = path.dirname(sourceFilePath);
21
- const baselineName = path.basename(sourceFilePath, path.extname(sourceFilePath));
22
-
23
- // Map standard ambient types build output combinations
24
- const declarationTargets = [
25
- path.join(fileDirectory, `${baselineName}.d.ts`),
26
- path.join(this.context.cwd, 'dist', `${baselineName}.d.ts`),
27
- path.join(this.context.cwd, 'types', `${baselineName}.d.ts`)
28
- ];
29
-
30
- for (const dtsPath of declarationTargets) {
31
- try {
32
- await fs.access(dtsPath);
33
- const code = await fs.readFile(dtsPath, 'utf8');
34
-
35
- const sourceFile = ts.createSourceFile(
36
- dtsPath,
37
- code,
38
- ts.ScriptTarget.Latest,
39
- true,
40
- ts.ScriptKind.TS
41
- );
42
-
43
- const replacementIntervals = [];
44
- this.inspectDtsNodes(sourceFile, prunedSymbolName, replacementIntervals);
45
-
46
- if (replacementIntervals.length > 0) {
47
- let updatedDtsText = code;
48
- // Apply substring text deletions from back to front to keep offset indexes accurate
49
- replacementIntervals.sort((a, b) => b.start - a.start);
50
-
51
- for (const interval of replacementIntervals) {
52
- updatedDtsText = updatedDtsText.slice(0, interval.start) + updatedDtsText.slice(interval.end);
53
- }
54
-
55
- await fs.writeFile(dtsPath, updatedDtsText, 'utf8');
56
- }
57
- } catch {
58
- // Declaration file variation target absent; proceed to fallback check routes
59
- }
60
- }
61
- }
62
-
63
- inspectDtsNodes(node, matchSymbol, intervals) {
64
- if (!node) return;
65
-
66
- // Match type mutations inside ambient structural parameters
67
- if (ts.isExportSpecifier(node) && node.name.text === matchSymbol) {
68
- intervals.push({ start: node.getStart(), end: node.getEnd() });
69
- } else if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node)) && node.name && node.name.text === matchSymbol) {
70
- intervals.push({ start: node.getStart(), end: node.getEnd() });
71
- }
72
-
73
- ts.forEachChild(node, child => this.inspectDtsNodes(child, matchSymbol, intervals));
74
- }
2
+ constructor(context) { this.context = context; }
3
+ async check() {}
75
4
  }
@@ -1,91 +1,52 @@
1
+ import path from 'path';
2
+
1
3
  export class CircularDetector {
2
4
  constructor(context) {
3
5
  this.context = context;
4
6
  this.cycles = [];
5
7
  }
6
8
 
7
- detectCycles(graph, context = null) {
8
- if (context) this.context = context;
9
- this.cwd = context?.cwd || this.context?.cwd || process.cwd();
10
- this.cycles = [];
11
-
12
- let index = 0;
13
- const stack = [];
14
- const indices = new Map();
15
- const lowlink = new Map();
16
- const onStack = new Set();
9
+ detectCycles(projectGraph, context) {
10
+ const visited = new Set();
11
+ const stack = new Set();
12
+ const cycles = [];
17
13
 
18
- const strongconnect = (v) => {
19
- indices.set(v, index);
20
- lowlink.set(v, index);
21
- index++;
22
- stack.push(v);
23
- onStack.add(v);
14
+ const traverse = (filePath, currentPath) => {
15
+ visited.add(filePath);
16
+ stack.add(filePath);
17
+ currentPath.push(filePath);
24
18
 
25
- const node = graph.get(v);
19
+ const node = projectGraph.get(filePath);
26
20
  if (node && node.outgoingEdges) {
27
- for (const w of node.outgoingEdges) {
28
- if (!indices.has(w)) {
29
- strongconnect(w);
30
- lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
31
- } else if (onStack.has(w)) {
32
- lowlink.set(v, Math.min(lowlink.get(v), indices.get(w)));
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);
33
29
  }
34
30
  }
35
31
  }
36
32
 
37
- if (lowlink.get(v) === indices.get(v)) {
38
- const component = [];
39
- let w;
40
- do {
41
- w = stack.pop();
42
- onStack.delete(w);
43
- component.push(w);
44
- } while (w !== v);
45
-
46
- if (component.length > 1) {
47
- this.cycles.push(component.reverse());
48
- } else {
49
- const node = graph.get(v);
50
- if (node && node.outgoingEdges && node.outgoingEdges.has(v)) {
51
- this.cycles.push([v]);
52
- }
53
- }
54
- }
33
+ stack.delete(filePath);
34
+ currentPath.pop();
55
35
  };
56
36
 
57
- for (const v of graph.keys()) {
58
- if (!indices.has(v)) {
59
- strongconnect(v);
37
+ for (const filePath of projectGraph.keys()) {
38
+ if (!visited.has(filePath)) {
39
+ traverse(filePath, []);
60
40
  }
61
41
  }
62
42
 
63
- return this.cycles;
43
+ this.cycles = cycles;
44
+ return cycles;
64
45
  }
65
46
 
66
47
  formatCycles() {
67
48
  return this.cycles.map(cycle => {
68
- const paths = cycle.map(p => {
69
- let rel = p.replace(this.context.cwd, '').replace(/^\//, '');
70
- if (rel.includes(':\\')) rel = rel.split(':\\')[1] || rel;
71
- return rel;
72
- });
73
- if (cycle.length === 1) return `${paths[0]} -> (self-loop)`;
74
- return paths.join(' -> ') + ' -> ' + paths[0];
49
+ return cycle.map(p => path.relative(this.context.cwd, p).replace(/\\/g, '/')).join(' -> ');
75
50
  });
76
51
  }
77
-
78
- getCycleDetails() {
79
- return this.cycles.map((cycle, idx) => ({
80
- cycleId: idx + 1,
81
- files: cycle.map(p => {
82
- let rel = p.replace(this.context.cwd, '').replace(/^\//, '');
83
- if (rel.includes(':\\')) rel = rel.split(':\\')[1] || rel;
84
- return rel;
85
- }),
86
- length: cycle.length,
87
- isSelfLoop: cycle.length === 1
88
- }));
89
- }
90
52
  }
91
- export default CircularDetector;