entkapp 5.3.1 → 5.4.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.
@@ -2,13 +2,18 @@ import path from 'path';
2
2
 
3
3
  /**
4
4
  * Production-Grade Native Rust AST Parser Bridge (OXC)
5
- * Fixed for Windows environments to prevent path-based "Unexpected token" errors.
5
+ * Optimized for Monorepos and Windows environments.
6
+ * Implements the same semantic logic as ASTAnalyzer but using the high-performance OXC parser.
6
7
  */
7
8
  export class OxcAnalyzer {
8
9
  constructor(context) {
9
10
  this.context = context;
10
11
  this.oxc = null;
11
12
  this.isAvailable = false;
13
+ this.scopeStack = [];
14
+ this.currentScope = null;
15
+ this.scopeCounter = 0;
16
+ this.pass = 1;
12
17
  }
13
18
 
14
19
  async init() {
@@ -35,7 +40,6 @@ export class OxcAnalyzer {
35
40
 
36
41
  /**
37
42
  * WINDOWS FIX: Robust path normalization for OXC.
38
- * Replaces backslashes with forward slashes to prevent escape sequence errors.
39
43
  */
40
44
  normalizePath(filePath) {
41
45
  if (!filePath) return filePath;
@@ -64,13 +68,10 @@ export class OxcAnalyzer {
64
68
  lang: "typescript"
65
69
  });
66
70
  } catch (e) {
67
- // Fallback with normalized path if the options object fails
68
71
  try {
69
72
  result = this.oxc.parseSync(normalizedPath, cleanContent);
70
73
  } catch (innerErr) {
71
- if (this.context.verbose) {
72
- console.error(`[OXC-ERROR] Native parse failed for: ${normalizedPath}`);
73
- }
74
+ if (this.context.verbose) console.error(`[OXC-ERROR] Native parse failed for: ${normalizedPath}`);
74
75
  return false;
75
76
  }
76
77
  }
@@ -79,11 +80,8 @@ export class OxcAnalyzer {
79
80
  try {
80
81
  parsedResult = typeof result === 'string' ? JSON.parse(result) : JSON.parse(JSON.stringify(result));
81
82
  } catch (err) {
82
- if (result && typeof result === 'object') {
83
- parsedResult = result;
84
- } else {
85
- return false;
86
- }
83
+ if (result && typeof result === 'object') parsedResult = result;
84
+ else return false;
87
85
  }
88
86
 
89
87
  let programRoot = null;
@@ -93,24 +91,35 @@ export class OxcAnalyzer {
93
91
  else if (parsedResult.type === 'Program' || parsedResult.body) programRoot = parsedResult;
94
92
  }
95
93
 
96
- // --- VALIDATION CHECK ---
97
- // If the file has content but OXC returns an empty body, it's a silent failure.
98
94
  if (cleanContent.trim().length > 0 && (!programRoot || !programRoot.body || programRoot.body.length === 0)) {
99
- if (this.context.verbose) {
100
- console.warn(`[OXC-WARNING] Silent failure detected for ${normalizedPath}. Body is empty despite content. Triggering Fallback...`);
101
- }
102
- return false; // This triggers the fallback to TypeScript in the engine
103
- }
104
-
105
- if (!programRoot || !programRoot.body) {
95
+ if (this.context.verbose) console.warn(`[OXC-WARNING] Silent failure detected for ${normalizedPath}. Triggering Fallback...`);
106
96
  return false;
107
97
  }
108
98
 
109
- fileNode.ast = programRoot;
110
- fileNode.symbolTable = new Map();
99
+ if (!programRoot || !programRoot.body) return false;
100
+
101
+ fileNode.ast = programRoot;
102
+ fileNode.symbolTable = new Map();
103
+
104
+ // --- TWO-PASS ANALYSIS ---
111
105
 
112
- this.walkOxcAst(programRoot, fileNode, cleanContent, 1);
113
- this.walkOxcAst(programRoot, fileNode, cleanContent, 2);
106
+ // Pass 1: Declarations
107
+ this.currentScope = { symbols: new Map(), parent: null, children: [] };
108
+ this.scopeStack = [this.currentScope];
109
+ this.scopeCounter = 0;
110
+ this.pass = 1;
111
+ this.walkOxcAst(programRoot, fileNode, cleanContent);
112
+
113
+ // Pass 2: References
114
+ if (this.scopeStack.length > 0) {
115
+ this.currentScope = this.scopeStack[0];
116
+ this.scopeCounter = 0;
117
+ this.pass = 2;
118
+ this.walkOxcAst(programRoot, fileNode, cleanContent);
119
+ }
120
+
121
+ this.scopeStack = [];
122
+ this.currentScope = null;
114
123
 
115
124
  return true;
116
125
  } catch (e) {
@@ -118,9 +127,163 @@ export class OxcAnalyzer {
118
127
  }
119
128
  }
120
129
 
121
- // ... (walkOxcAst and other methods remain the same as in original)
122
- walkOxcAst(node, fileNode, content, pass) {
130
+ pushScope() {
131
+ if (this.pass === 1) {
132
+ const newScope = { symbols: new Map(), parent: this.currentScope, children: [] };
133
+ if (this.currentScope) this.currentScope.children.push(newScope);
134
+ this.scopeStack.push(newScope);
135
+ this.currentScope = newScope;
136
+ } else {
137
+ if (this.currentScope && this.currentScope.children) {
138
+ const nextScope = this.currentScope.children[this.scopeCounter++];
139
+ if (nextScope) {
140
+ this.scopeStack.push(nextScope);
141
+ this.currentScope = nextScope;
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ popScope() {
148
+ this.scopeStack.pop();
149
+ this.currentScope = this.scopeStack[this.scopeStack.length - 1];
150
+ }
151
+
152
+ walkOxcAst(node, fileNode, content) {
123
153
  if (!node) return;
124
- // (Implementation omitted for brevity, should be copied from original OxcAnalyzer.js)
154
+
155
+ const isScopeNode = node.type === 'BlockStatement' || node.type === 'FunctionDeclaration' ||
156
+ node.type === 'ClassDeclaration' || node.type === 'ArrowFunctionExpression';
157
+
158
+ let previousCounter = 0;
159
+ if (isScopeNode) {
160
+ if (this.pass === 2) previousCounter = this.scopeCounter;
161
+ this.scopeCounter = 0;
162
+ this.pushScope();
163
+ }
164
+
165
+ if (this.pass === 1) {
166
+ this.handleNodePass1(node, fileNode, content);
167
+ } else {
168
+ this.handleNodePass2(node, fileNode, content);
169
+ }
170
+
171
+ // Traverse children
172
+ for (const key in node) {
173
+ const child = node[key];
174
+ if (Array.isArray(child)) {
175
+ child.forEach(c => this.walkOxcAst(c, fileNode, content));
176
+ } else if (child && typeof child === 'object' && child.type) {
177
+ this.walkOxcAst(child, fileNode, content);
178
+ }
179
+ }
180
+
181
+ if (isScopeNode) {
182
+ this.popScope();
183
+ if (this.pass === 2) this.scopeCounter = previousCounter + 1;
184
+ }
185
+ }
186
+
187
+ handleNodePass1(node, fileNode, content) {
188
+ switch (node.type) {
189
+ case 'ImportDeclaration':
190
+ this.handleImport(node, fileNode);
191
+ break;
192
+ case 'ExportNamedDeclaration':
193
+ this.handleExportNamed(node, fileNode);
194
+ break;
195
+ case 'ExportDefaultDeclaration':
196
+ fileNode.internalExports.set('default', { type: 'default', start: node.start, end: node.end });
197
+ break;
198
+ case 'ExportAllDeclaration':
199
+ if (node.source) {
200
+ const specifier = node.source.value;
201
+ fileNode.explicitImports.add(specifier);
202
+ fileNode.internalExports.set('*', { type: 're-export-all', source: specifier });
203
+ }
204
+ break;
205
+ }
206
+ }
207
+
208
+ handleNodePass2(node, fileNode, content) {
209
+ switch (node.type) {
210
+ case 'Identifier':
211
+ if (!this.isLocalShadowing(node.name)) {
212
+ fileNode.instantiatedIdentifiers.add(node.name);
213
+ }
214
+ break;
215
+ case 'CallExpression':
216
+ this.handleCall(node, fileNode);
217
+ break;
218
+ case 'MemberExpression':
219
+ if (node.property && node.property.name) {
220
+ fileNode.instantiatedIdentifiers.add(node.property.name);
221
+ }
222
+ break;
223
+ }
224
+ }
225
+
226
+ handleImport(node, fileNode) {
227
+ if (!node.source) return;
228
+ const specifier = node.source.value;
229
+ fileNode.explicitImports.add(specifier);
230
+
231
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
232
+ const pkgName = this._extractPackageName(specifier);
233
+ let isInternal = false;
234
+ if (this.context.pathMapper?.isTsconfigAlias?.(specifier)) isInternal = true;
235
+ if (!isInternal && this.context.workspaceGraph?.isLocalWorkspaceSpecifier?.(specifier)) isInternal = true;
236
+ if (!isInternal) fileNode.externalPackageUsage.add(pkgName);
237
+ }
238
+ }
239
+
240
+ handleExportNamed(node, fileNode) {
241
+ if (node.declaration) {
242
+ const decl = node.declaration;
243
+ const name = decl.id?.name || (decl.declarations?.[0]?.id?.name);
244
+ if (name) {
245
+ fileNode.internalExports.set(name, { type: 'export', start: node.start, end: node.end });
246
+ }
247
+ } else if (node.specifiers) {
248
+ node.specifiers.forEach(spec => {
249
+ const name = spec.exported.name || spec.exported.value;
250
+ fileNode.internalExports.set(name, { type: 'export', start: spec.start, end: spec.end });
251
+ });
252
+ }
253
+ }
254
+
255
+ handleCall(node, fileNode) {
256
+ const callee = node.callee;
257
+ if (callee.type === 'Identifier' && callee.name === 'require') {
258
+ const arg = node.arguments[0];
259
+ if (arg && (arg.type === 'StringLiteral' || arg.type === 'Literal')) {
260
+ const specifier = arg.value;
261
+ fileNode.explicitImports.add(specifier);
262
+ }
263
+ } else if (callee.type === 'Import') {
264
+ const arg = node.arguments[0];
265
+ if (arg && (arg.type === 'StringLiteral' || arg.type === 'Literal')) {
266
+ const specifier = arg.value;
267
+ fileNode.explicitImports.add(specifier);
268
+ fileNode.dynamicImports.add(specifier);
269
+ }
270
+ }
271
+ }
272
+
273
+ isLocalShadowing(name) {
274
+ let scope = this.currentScope;
275
+ while (scope) {
276
+ if (scope.symbols.has(name)) return scope.parent !== null;
277
+ scope = scope.parent;
278
+ }
279
+ return false;
280
+ }
281
+
282
+ _extractPackageName(specifier) {
283
+ if (specifier.startsWith('@')) {
284
+ const parts = specifier.split('/');
285
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
286
+ }
287
+ return specifier.split('/')[0];
125
288
  }
126
289
  }
package/src/index.js CHANGED
@@ -113,22 +113,81 @@ export class RefactoringEngine {
113
113
 
114
114
  // Pass 1: Boot environment contexts and load alias configuration maps
115
115
  await this.oxcAnalyzer.init();
116
+ // UPGRADE: Wire pathMapper onto context so ASTAnalyzer alias-filtering works in all code paths
117
+ this.context.pathMapper = this.pathMapper;
116
118
  await this.pathMapper.loadMappings(this.context.tsconfigFilename);
117
119
 
118
120
  // Always attempt workspace mesh initialization
119
121
  console.log(ansis.dim('🌐 Probing for monorepo workspace configuration...'));
120
122
  await this.workspaceGraph.initializeWorkspaceMesh();
123
+ // UPGRADE: Always expose workspaceGraph on context (not only when workspace is enabled)
124
+ this.context.workspaceGraph = this.workspaceGraph;
125
+
126
+ // UPGRADE: Scan all discovered framework configs across the workspace EARLY
127
+ const discoveredConfigs = await this.workspaceGraph.findFrameworkConfigs();
128
+ if (discoveredConfigs.length > 0) {
129
+ console.log(ansis.dim(`⚙️ Detected ${discoveredConfigs.length} framework configurations...`));
130
+ const { FrameworkConfigParser } = await import('./resolution/FrameworkConfigParser.js');
131
+ const parser = new FrameworkConfigParser(this.context);
132
+
133
+ for (const configPath of discoveredConfigs) {
134
+ try {
135
+ const content = await fs.readFile(configPath, 'utf8');
136
+
137
+ // 1. Core Parsing (Aliases)
138
+ const { aliases } = parser.parse(content, configPath);
139
+ for (const [key, target] of aliases.entries()) {
140
+ this.pathMapper.addAlias(key, target);
141
+ }
142
+
143
+ // 2. Plugin-based Entry Detection
144
+ const detectedEntries = await this.magicDetector.detectEntryPointsFromPlugins(content, configPath);
145
+ for (const entry of detectedEntries) {
146
+ const absEntry = path.resolve(path.dirname(configPath), entry).replace(/\\/g, '/');
147
+ if (!this.context.entryPoints.includes(absEntry)) {
148
+ this.context.entryPoints.push(absEntry);
149
+ }
150
+ }
151
+
152
+ // 3. Register config file itself as entry
153
+ if (!this.context.entryPoints.includes(configPath)) {
154
+ this.context.entryPoints.push(configPath);
155
+ }
156
+ } catch (e) {}
157
+ }
158
+ }
159
+
121
160
  if (this.context.isWorkspaceEnabled) {
122
161
  console.log(ansis.dim('🌐 Monorepo workspace detected – mapping package mesh layers...'));
123
- // Expose workspaceGraph on context for WorkspaceDiagnostic and other components
124
- this.context.workspaceGraph = this.workspaceGraph;
162
+
163
+ // NEW: Register all workspace package entry points
164
+ for (const [dir, manifest] of this.workspaceGraph.packageManifests.entries()) {
165
+ if (manifest.entryPoints && manifest.entryPoints.length > 0) {
166
+ for (const ep of manifest.entryPoints) {
167
+ if (!this.context.entryPoints.includes(ep)) {
168
+ this.context.entryPoints.push(ep);
169
+ }
170
+ }
171
+ }
172
+ }
173
+
125
174
  // Reload PathMapper aliases now that workspace roots are known
126
175
  await this.pathMapper.loadMappings(this.context.tsconfigFilename);
127
176
  if (this.context.verbose) {
128
- console.log(`[Workspace] Found ${this.workspaceGraph.packageManifests.size} workspace packages:`);
177
+ const wsSummary = this.workspaceGraph.getSummary();
178
+ console.log(`[Workspace] Found ${wsSummary.packages} workspace packages:`);
129
179
  for (const [dir, manifest] of this.workspaceGraph.packageManifests.entries()) {
130
180
  console.log(ansis.dim(` • ${manifest.name || dir}`));
131
181
  }
182
+ if (wsSummary.tsconfigsLoaded > 0) {
183
+ console.log(ansis.dim(`[Workspace] Loaded ${wsSummary.tsconfigsLoaded} tsconfig.json file(s) from workspace packages`));
184
+ }
185
+ if (wsSummary.configFilesLoaded > 0) {
186
+ console.log(ansis.dim(`[Workspace] Loaded ${wsSummary.configFilesLoaded} *.config.ts/js file(s) from workspace packages`));
187
+ }
188
+ if (wsSummary.monorepoConfigs && wsSummary.monorepoConfigs.length > 0) {
189
+ console.log(ansis.dim(`[Workspace] Detected global configs: ${wsSummary.monorepoConfigs.join(', ')}`));
190
+ }
132
191
  }
133
192
  }
134
193
 
@@ -200,6 +259,14 @@ export class RefactoringEngine {
200
259
  for (const filePath of sourceCodeFilesList) {
201
260
  const absFilePath = slashifyInternal(filePath);
202
261
  const node = this.context.getOrCreateNode(absFilePath);
262
+
263
+ // Config data is now handled early in the workspace mesh initialization phase.
264
+ // We still mark the rawCode for plugin analysis.
265
+ if (absFilePath.includes('.config.')) {
266
+ try {
267
+ node.rawCode = await fs.readFile(absFilePath, 'utf8');
268
+ } catch (e) {}
269
+ }
203
270
  const currentHash = await this.cacheManager.computeHash(absFilePath);
204
271
  node.contentHash = currentHash;
205
272
 
@@ -227,6 +294,7 @@ export class RefactoringEngine {
227
294
  } else if (!parallelParseCompleted) {
228
295
  this.context.metrics.cacheMisses++;
229
296
  const fileContent = await fs.readFile(filePath, 'utf8');
297
+ node.rawCode = fileContent; // UPGRADE: Store raw code for plugin analysis
230
298
 
231
299
  let success = false;
232
300
  if (this.oxcAnalyzer.isAvailable) {
@@ -248,6 +316,10 @@ export class RefactoringEngine {
248
316
  if (!success || (oxcFailedToFindDependencies && (hasImportExportKeywords || hasCommonJSKeywords))) {
249
317
  await this.analyzer.parseFile(filePath, fileContent, node);
250
318
  }
319
+
320
+ // UPGRADE 5.3.0: Run plugin-specific content analysis
321
+ await this.magicDetector.runPluginContentAnalysis(node, absFilePath);
322
+
251
323
  // Secret scan on freshly parsed content
252
324
  const secretFindings = this.secretScanner.scanFileContent(filePath, fileContent);
253
325
  if (secretFindings.length > 0) {
@@ -562,6 +634,17 @@ export class RefactoringEngine {
562
634
  const nodeBuiltins = ['assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console', 'constants', 'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'http2', 'https', 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks', 'process', 'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'timers', 'tls', 'trace_events', 'tty', 'url', 'util', 'v8', 'vm', 'worker_threads', 'zlib'];
563
635
  if (nodeBuiltins.includes(basePkg) || nodeBuiltins.includes(basePkg.replace('node:', ''))) return;
564
636
 
637
+ // UPGRADE: Skip tsconfig path aliases (e.g. @shared/*, @idk/*, ~/*)
638
+ // Check full specifier AND basePkg against alias patterns
639
+ if (this.pathMapper && typeof this.pathMapper.isTsconfigAlias === 'function') {
640
+ if (this.pathMapper.isTsconfigAlias(specifier) || this.pathMapper.isTsconfigAlias(basePkg)) return;
641
+ }
642
+
643
+ // UPGRADE: Skip workspace packages detected via pnpm-workspace.yaml / package.json workspaces
644
+ if (this.workspaceGraph && typeof this.workspaceGraph.isLocalWorkspaceSpecifier === 'function') {
645
+ if (this.workspaceGraph.isLocalWorkspaceSpecifier(specifier) || this.workspaceGraph.isLocalWorkspaceSpecifier(basePkg)) return;
646
+ }
647
+
565
648
  if (!localDeps.has(basePkg)) {
566
649
  const alreadyFlagged = this.context.unlistedDependencies.some(u => u.package === basePkg && u.file === filePath);
567
650
  if (!alreadyFlagged) {
@@ -574,7 +657,7 @@ export class RefactoringEngine {
574
657
  }
575
658
  });
576
659
  } catch (error) {
577
- if (this.context.options.verbose) {
660
+ if (this.context.options.verbose && error.code !== 'ENOENT') {
578
661
  console.error(ansis.red(` ❌ Manifest Parsing Exception: ${error.message}`));
579
662
  }
580
663
  }
@@ -59,6 +59,33 @@ export class IncrementalCacheManager {
59
59
  }
60
60
  }
61
61
 
62
+ /**
63
+ * UPGRADE 5.4.3: Affected-Only Analysis
64
+ * Identifies which files need re-analysis based on Git changes or cache misses.
65
+ */
66
+ async getAffectedFiles(allFiles) {
67
+ const manifest = await this.loadCacheManifest();
68
+ const affected = [];
69
+ const unchanged = [];
70
+
71
+ for (const file of allFiles) {
72
+ const cached = manifest[file];
73
+ if (!cached) {
74
+ affected.push(file);
75
+ continue;
76
+ }
77
+
78
+ const currentHash = await this.computeHash(file);
79
+ if (currentHash !== cached.hash) {
80
+ affected.push(file);
81
+ } else {
82
+ unchanged.push({ path: file, data: cached });
83
+ }
84
+ }
85
+
86
+ return { affected, unchanged };
87
+ }
88
+
62
89
  /**
63
90
  * Serializes the current active dependency graph, translating Maps and Sets into JSON schemas.
64
91
  * @param {Map<string, Object>} currentGraphState - In-memory structural project state map
@@ -47,10 +47,11 @@ export class WorkerPool {
47
47
 
48
48
  try {
49
49
  const analyticalResultsSubsets = await Promise.all(threadTaskExecutionsList);
50
+ const flatResults = analyticalResultsSubsets.flat();
50
51
 
51
52
  // Merge thread structural subsets back into the primary context graph nodes
52
- analyticalResultsSubsets.flat().forEach(result => {
53
- if (!result) return;
53
+ for (const result of flatResults) {
54
+ if (!result) continue;
54
55
  // UPGRADE: Normalize filePath before merging to prevent duplicate nodes in the graph
55
56
  const normalizedPath = path.resolve(this.context.cwd, result.filePath).replace(/\\/g, '/');
56
57
  const node = masterEngineInstanceReference.context.getOrCreateNode(normalizedPath);
@@ -95,7 +96,13 @@ export class WorkerPool {
95
96
  if (!node.globImports) node.globImports = [];
96
97
  result.globImports.forEach(i => node.globImports.push(i));
97
98
  }
98
- });
99
+
100
+ // UPGRADE 5.3.0: Run plugin-specific content analysis for worker-parsed files
101
+ if (result.rawCode) {
102
+ node.rawCode = result.rawCode;
103
+ await masterEngineInstanceReference.magicDetector.runPluginContentAnalysis(node, normalizedPath);
104
+ }
105
+ }
99
106
 
100
107
  return true;
101
108
  } catch (poolThreadFault) {
@@ -107,13 +114,24 @@ export class WorkerPool {
107
114
  }
108
115
 
109
116
  executeChunkInsideThread(fileChunkSubset) {
117
+ // UPGRADE: Serialize alias patterns and workspace package names so workers can filter false positives
118
+ const serializedAliasPatterns = (this.context.pathMapper && this.context.pathMapper._aliasPatterns)
119
+ ? this.context.pathMapper._aliasPatterns.map(p => p.regex.source)
120
+ : [];
121
+ const workspacePackageNames = (this.context.workspaceGraph && this.context.workspaceGraph.workspacePackages)
122
+ ? Array.from(this.context.workspaceGraph.workspacePackages.keys())
123
+ : [];
124
+
110
125
  return new Promise((resolve, reject) => {
111
126
  const workerInstance = new Worker(this.workerScriptPath, { type: 'module',
112
127
  workerData: {
113
128
  files: fileChunkSubset,
114
129
  contextOptions: {
115
130
  verbose: this.context.verbose,
116
- cwd: this.context.cwd
131
+ cwd: this.context.cwd,
132
+ // UPGRADE: Pass alias/workspace data so workers can filter false positives
133
+ aliasPatternSources: serializedAliasPatterns,
134
+ workspacePackageNames
117
135
  }
118
136
  }
119
137
  });
@@ -12,11 +12,35 @@ async function runTask() {
12
12
  const { files, contextOptions } = workerData;
13
13
  const results = [];
14
14
 
15
+ // UPGRADE: Reconstruct alias/workspace filter helpers from serialized contextOptions
16
+ // These are passed from WorkerPool.executeChunkInsideThread so workers can skip false positives
17
+ const aliasRegexes = (contextOptions.aliasPatternSources || []).map(src => {
18
+ try { return new RegExp(src); } catch { return null; }
19
+ }).filter(Boolean);
20
+ const workspacePackageSet = new Set(contextOptions.workspacePackageNames || []);
21
+
22
+ // Lightweight helpers mirroring PathMapper.isTsconfigAlias and WorkspaceGraph.isLocalWorkspaceSpecifier
23
+ const isTsconfigAlias = (spec) => aliasRegexes.some(r => r.test(spec));
24
+ const isWorkspacePkg = (spec) => {
25
+ if (workspacePackageSet.has(spec)) return true;
26
+ for (const name of workspacePackageSet) { if (spec.startsWith(name + '/')) return true; }
27
+ return false;
28
+ };
29
+
15
30
  // Create a minimal context for analyzers
16
31
  const mockContext = {
17
32
  verbose: contextOptions.verbose,
18
33
  cwd: contextOptions.cwd || process.cwd(),
19
34
  projectGraph: new Map(),
35
+ // UPGRADE: Expose lightweight alias/workspace helpers so ASTAnalyzer can filter imports
36
+ pathMapper: {
37
+ isTsconfigAlias,
38
+ _aliasPatterns: aliasRegexes.map(r => ({ regex: r }))
39
+ },
40
+ workspaceGraph: {
41
+ isLocalWorkspaceSpecifier: isWorkspacePkg,
42
+ workspacePackages: workspacePackageSet
43
+ },
20
44
  getOrCreateNode: (path) => ({
21
45
  filePath: path,
22
46
  explicitImports: new Set(),
@@ -50,6 +74,7 @@ async function runTask() {
50
74
  try {
51
75
  const content = await fs.readFile(filePath, 'utf8');
52
76
  const node = mockContext.getOrCreateNode(filePath);
77
+ node.rawCode = content; // UPGRADE: Add raw code to node for plugin analysis in main thread
53
78
 
54
79
  // Defensiver Check: Sicherstellen, dass die Map/Set-Instanzen sauber existieren
55
80
  if (!(node.internalExports instanceof Map)) node.internalExports = new Map();
@@ -106,6 +131,7 @@ async function runTask() {
106
131
  // Serialize the node data for transfer back to main thread
107
132
  results.push({
108
133
  filePath: node.filePath,
134
+ rawCode: node.rawCode, // UPGRADE: Pass raw code back to main thread for plugin analysis
109
135
  explicitImports: Array.from(node.explicitImports || []),
110
136
  dynamicImports: Array.from(node.dynamicImports || []),
111
137
  importedSymbols: Array.from(node.importedSymbols || []),
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  * Base class for all entkapp plugins.
6
6
  * Defines the contract for ecosystem detection, entry point mapping,
7
7
  * and missing dependency / devDependency detection.
8
- * Version 5.0.0: Added getMissingDependencies(), getOrphanedConfigs(), runDependencyDiagnostics().
8
+ * Version 5.4.0: Added detectEntryPoints() for dynamic framework entry detection.
9
9
  */
10
10
  export class BasePlugin {
11
11
  constructor(context) {
@@ -34,6 +34,17 @@ export class BasePlugin {
34
34
  return [];
35
35
  }
36
36
 
37
+ /**
38
+ * Version 5.4.0: Dynamically detect entry points from a file's content.
39
+ * Useful for parsing vite.config.ts, webpack.config.js, etc.
40
+ * @param {string} content - The file content
41
+ * @param {string} filePath - The absolute path to the file
42
+ * @returns {Array<string>} List of relative or absolute entry point paths
43
+ */
44
+ detectEntryPoints(content, filePath) {
45
+ return [];
46
+ }
47
+
37
48
  /**
38
49
  * Returns symbols that are implicitly required/exported by the framework.
39
50
  */
@@ -43,25 +54,13 @@ export class BasePlugin {
43
54
 
44
55
  /**
45
56
  * Returns the npm package names required for this plugin to work.
46
- * Each entry is either a string or an object:
47
- * { name: string, dev?: boolean, optional?: boolean }
48
- * - dev: true => expected in devDependencies
49
- * - dev: false => expected in dependencies
50
- * - optional => only warn, not error
51
- *
52
- * Override in subclasses to enable automatic dependency checking.
53
- * @returns {Array<string | {name: string, dev?: boolean, optional?: boolean}>}
54
57
  */
55
58
  getRequiredPackages() {
56
59
  return [];
57
60
  }
58
61
 
59
62
  /**
60
- * Version 5.0.0: Checks whether all required packages are present in package.json.
61
- * Returns an array of diagnostic objects describing missing or misplaced dependencies.
62
- *
63
- * @param {string} baseDir - The project root directory
64
- * @returns {Promise<Array>}
63
+ * Checks whether all required packages are present in package.json.
65
64
  */
66
65
  async getMissingDependencies(baseDir) {
67
66
  const diagnostics = [];
@@ -147,11 +146,7 @@ export class BasePlugin {
147
146
  }
148
147
 
149
148
  /**
150
- * Version 5.0.0: Checks for config files that exist without the corresponding package.
151
- * Detects "orphaned" configs – e.g. .prettierrc exists but prettier is not installed.
152
- *
153
- * @param {string} baseDir
154
- * @returns {Promise<Array>}
149
+ * Checks for config files that exist without the corresponding package.
155
150
  */
156
151
  async getOrphanedConfigs(baseDir) {
157
152
  const diagnostics = [];
@@ -195,11 +190,7 @@ export class BasePlugin {
195
190
  }
196
191
 
197
192
  /**
198
- * Version 5.0.0: Run all dependency diagnostics (missing + orphaned).
199
- * Convenience method combining getMissingDependencies and getOrphanedConfigs.
200
- *
201
- * @param {string} baseDir
202
- * @returns {Promise<Array>}
193
+ * Run all dependency diagnostics (missing + orphaned).
203
194
  */
204
195
  async runDependencyDiagnostics(baseDir) {
205
196
  const [missing, orphaned] = await Promise.all([
@@ -210,9 +201,7 @@ export class BasePlugin {
210
201
  }
211
202
 
212
203
  /**
213
- * Version 3.2.0: Dynamic getter for custom plugin properties.
214
- * @param {string} key - The property key to retrieve
215
- * @returns {any} The value of the custom property
204
+ * Dynamic getter for custom plugin properties.
216
205
  */
217
206
  get(key) {
218
207
  const methodName = `get${key.charAt(0).toUpperCase() + key.slice(1)}`;