entkapp 5.3.0 → 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.
- package/README.md +1 -7
- package/bin/cli.js +58 -117
- package/package.json +1 -1
- package/src/EngineContext.js +6 -0
- package/src/ast/ASTAnalyzer.js +94 -6
- package/src/ast/MagicDetector.js +23 -43
- package/src/ast/OxcAnalyzer.js +190 -27
- package/src/ast/SecretScanner.js +112 -50
- package/src/index.js +87 -4
- package/src/performance/GraphCache.js +27 -0
- package/src/performance/WorkerPool.js +22 -4
- package/src/performance/WorkerTaskRunner.js +26 -0
- package/src/plugins/BasePlugin.js +16 -27
- package/src/plugins/PluginRegistry.js +44 -0
- package/src/plugins/ecosystems/UltimateBundle.js +259 -0
- package/src/resolution/ConfigLoader.js +190 -26
- package/src/resolution/FrameworkConfigParser.js +119 -0
- package/src/resolution/GraphAnalyzer.js +65 -1
- package/src/resolution/PathMapper.js +81 -0
- package/src/resolution/TSConfigLoader.js +3 -1
- package/src/resolution/WorkSpaceGraph.js +260 -89
|
@@ -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
|
-
|
|
53
|
-
if (!result)
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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)}`;
|
|
@@ -111,6 +111,31 @@ export class PluginRegistry {
|
|
|
111
111
|
return this.plugins.get(name);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Version 5.4.0: Collect entry points from all plugins for a given file.
|
|
116
|
+
* @param {string} content - The file content
|
|
117
|
+
* @param {string} filePath - The absolute path to the file
|
|
118
|
+
* @returns {Array<string>} List of detected entry points
|
|
119
|
+
*/
|
|
120
|
+
detectEntryPointsFromContent(content, filePath) {
|
|
121
|
+
const entryPoints = [];
|
|
122
|
+
for (const plugin of this.plugins.values()) {
|
|
123
|
+
if (typeof plugin.detectEntryPoints === 'function') {
|
|
124
|
+
try {
|
|
125
|
+
const detected = plugin.detectEntryPoints(content, filePath);
|
|
126
|
+
if (Array.isArray(detected)) {
|
|
127
|
+
entryPoints.push(...detected);
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (this.context?.verbose) {
|
|
131
|
+
console.warn(`[PluginRegistry] Entry detection failed for plugin "${plugin.name}" on ${filePath}:`, e.message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return entryPoints;
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
async getActivePlugins(baseDir) {
|
|
115
140
|
const active = [];
|
|
116
141
|
for (const plugin of this.plugins.values()) {
|
|
@@ -121,6 +146,25 @@ export class PluginRegistry {
|
|
|
121
146
|
return active;
|
|
122
147
|
}
|
|
123
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Version 5.3.0: Run content analysis on a node using all active plugins.
|
|
151
|
+
* @param {GraphNode} node - The node to analyze
|
|
152
|
+
* @param {string} filePath - The absolute path to the file
|
|
153
|
+
*/
|
|
154
|
+
async runPluginAnalysis(node, filePath) {
|
|
155
|
+
for (const plugin of this.plugins.values()) {
|
|
156
|
+
if (typeof plugin.analyze === 'function') {
|
|
157
|
+
try {
|
|
158
|
+
await plugin.analyze(node, filePath);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
if (this.context?.verbose) {
|
|
161
|
+
console.warn(`[PluginRegistry] Analysis failed for plugin "${plugin.name}" on ${filePath}:`, e.message);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
124
168
|
/**
|
|
125
169
|
* Version 5.0.0: Run dependency diagnostics across all registered plugins.
|
|
126
170
|
* Returns a consolidated list of missing/misplaced dependencies and orphaned configs.
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import fs from 'fs/promises';
|
|
10
10
|
import { BasePlugin } from '../BasePlugin.js';
|
|
11
|
+
import { FrameworkConfigParser } from '../../resolution/FrameworkConfigParser.js';
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
export class GraphQLPlugin extends BasePlugin {
|
|
@@ -211,6 +212,27 @@ export class NuxtPlugin extends BasePlugin {
|
|
|
211
212
|
}
|
|
212
213
|
return false;
|
|
213
214
|
}
|
|
215
|
+
detectEntryPoints(content, filePath) {
|
|
216
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
217
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
218
|
+
const { entries } = parser.parse(content, filePath);
|
|
219
|
+
return Array.from(entries);
|
|
220
|
+
}
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
async analyze(node, filePath) {
|
|
224
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
225
|
+
node.isEntry = true;
|
|
226
|
+
if (node.rawCode) {
|
|
227
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
228
|
+
const { aliases, entries } = parser.parse(node.rawCode, filePath);
|
|
229
|
+
if (aliases.size > 0) node.frameworkAliases = aliases;
|
|
230
|
+
if (entries.size > 0) node.frameworkEntries = entries;
|
|
231
|
+
if (node.rawCode.includes('modules:')) node.hasNuxtModules = true;
|
|
232
|
+
if (node.rawCode.includes('components:')) node.hasNuxtComponentsConfig = true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
214
236
|
}
|
|
215
237
|
|
|
216
238
|
export class RemixPlugin extends BasePlugin {
|
|
@@ -233,6 +255,21 @@ export class RemixPlugin extends BasePlugin {
|
|
|
233
255
|
}
|
|
234
256
|
return false;
|
|
235
257
|
}
|
|
258
|
+
detectEntryPoints(content, filePath) {
|
|
259
|
+
if (filePath.endsWith('remix.config.js') || filePath.includes('vite.config.')) {
|
|
260
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
261
|
+
const { entries } = parser.parse(content, filePath);
|
|
262
|
+
return Array.from(entries);
|
|
263
|
+
}
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
async analyze(node, filePath) {
|
|
267
|
+
if (filePath.endsWith('remix.config.js')) {
|
|
268
|
+
node.isEntry = true;
|
|
269
|
+
if (node.rawCode?.includes('ignoredRouteFiles:')) node.hasRemixIgnoredRoutes = true;
|
|
270
|
+
if (node.rawCode?.includes('serverBuildPath:')) node.hasRemixServerBuildPath = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
236
273
|
}
|
|
237
274
|
|
|
238
275
|
export class SvelteKitPlugin extends BasePlugin {
|
|
@@ -251,6 +288,29 @@ export class SvelteKitPlugin extends BasePlugin {
|
|
|
251
288
|
async isActive(baseDir) {
|
|
252
289
|
try { await fs.access(path.join(baseDir, 'svelte.config.js')); return true; } catch { return false; }
|
|
253
290
|
}
|
|
291
|
+
detectEntryPoints(content, filePath) {
|
|
292
|
+
if (filePath.includes('svelte.config.')) {
|
|
293
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
294
|
+
const { entries } = parser.parse(content, filePath);
|
|
295
|
+
return Array.from(entries);
|
|
296
|
+
}
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
async analyze(node, filePath) {
|
|
300
|
+
if (filePath.includes('svelte.config.')) {
|
|
301
|
+
node.isEntry = true;
|
|
302
|
+
if (node.rawCode) {
|
|
303
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
304
|
+
const { aliases, entries } = parser.parse(node.rawCode, filePath);
|
|
305
|
+
if (aliases.size > 0) {
|
|
306
|
+
node.frameworkAliases = aliases;
|
|
307
|
+
node.hasSvelteAliases = true;
|
|
308
|
+
}
|
|
309
|
+
if (entries.size > 0) node.frameworkEntries = entries;
|
|
310
|
+
if (node.rawCode.includes('adapter:')) node.hasSvelteAdapter = true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
254
314
|
}
|
|
255
315
|
|
|
256
316
|
export class AstroPlugin extends BasePlugin {
|
|
@@ -265,12 +325,39 @@ export class AstroPlugin extends BasePlugin {
|
|
|
265
325
|
}
|
|
266
326
|
return false;
|
|
267
327
|
}
|
|
328
|
+
detectEntryPoints(content, filePath) {
|
|
329
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
330
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
331
|
+
const { entries } = parser.parse(content, filePath);
|
|
332
|
+
return Array.from(entries);
|
|
333
|
+
}
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
async analyze(node, filePath) {
|
|
337
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
338
|
+
node.isEntry = true;
|
|
339
|
+
if (node.rawCode) {
|
|
340
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
341
|
+
const { aliases, entries } = parser.parse(node.rawCode, filePath);
|
|
342
|
+
if (aliases.size > 0) node.frameworkAliases = aliases;
|
|
343
|
+
if (entries.size > 0) node.frameworkEntries = entries;
|
|
344
|
+
if (node.rawCode.includes('integrations:')) node.hasAstroIntegrations = true;
|
|
345
|
+
if (node.rawCode.includes('output:')) node.hasAstroOutputConfig = true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
268
349
|
}
|
|
269
350
|
|
|
270
351
|
export class VitepressPlugin extends BasePlugin {
|
|
271
352
|
get name() { return 'vitepress'; }
|
|
272
353
|
getConfigFiles() { return ['package.json']; }
|
|
273
354
|
getRequiredPackages() { return [{ name: 'vitepress', dev: true }]; }
|
|
355
|
+
|
|
356
|
+
getRoutePatterns() {
|
|
357
|
+
// Protection: Every file inside a .vitepress folder is treated as an entry point
|
|
358
|
+
return [/\/\.vitepress\//];
|
|
359
|
+
}
|
|
360
|
+
|
|
274
361
|
async isActive(baseDir) {
|
|
275
362
|
try {
|
|
276
363
|
const pkgJson = JSON.parse(await fs.readFile(path.join(baseDir, 'package.json'), 'utf8'));
|
|
@@ -282,6 +369,62 @@ export class VitepressPlugin extends BasePlugin {
|
|
|
282
369
|
return hasDep;
|
|
283
370
|
} catch { return false; }
|
|
284
371
|
}
|
|
372
|
+
|
|
373
|
+
async runDependencyDiagnostics(baseDir) {
|
|
374
|
+
const diagnostics = await super.runDependencyDiagnostics(baseDir);
|
|
375
|
+
|
|
376
|
+
let pkg = null;
|
|
377
|
+
try {
|
|
378
|
+
const raw = await fs.readFile(path.join(baseDir, 'package.json'), 'utf8');
|
|
379
|
+
pkg = JSON.parse(raw);
|
|
380
|
+
} catch { return diagnostics; }
|
|
381
|
+
|
|
382
|
+
const scripts = pkg.scripts || {};
|
|
383
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
384
|
+
const hasVitepressDep = 'vitepress' in allDeps;
|
|
385
|
+
|
|
386
|
+
// Check if any script uses vitepress
|
|
387
|
+
const hasVitepressCommand = Object.values(scripts).some(s => s.includes('vitepress'));
|
|
388
|
+
|
|
389
|
+
// Logic for .vitepress folder existence
|
|
390
|
+
let hasVitepressFolder = false;
|
|
391
|
+
const possibleDirs = ['.vitepress', 'docs/.vitepress', 'docs/docs/.vitepress'];
|
|
392
|
+
for (const d of possibleDirs) {
|
|
393
|
+
try {
|
|
394
|
+
await fs.access(path.join(baseDir, d));
|
|
395
|
+
hasVitepressFolder = true;
|
|
396
|
+
break;
|
|
397
|
+
} catch {}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Scenario 1: build cmd/dependency exists but no folder -> unused dependency
|
|
401
|
+
if ((hasVitepressCommand || hasVitepressDep) && !hasVitepressFolder) {
|
|
402
|
+
diagnostics.push({
|
|
403
|
+
plugin: this.name,
|
|
404
|
+
severity: 'warning',
|
|
405
|
+
message: `Plugin "${this.name}": "vitepress" is defined in package.json or scripts, but no ".vitepress" folder was found. This might be an unused dependency.`
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Scenario 2: folder exists but no dependency -> missing dependency
|
|
410
|
+
if (hasVitepressFolder && !hasVitepressDep) {
|
|
411
|
+
diagnostics.push({
|
|
412
|
+
plugin: this.name,
|
|
413
|
+
severity: 'error',
|
|
414
|
+
message: `Plugin "${this.name}": ".vitepress" folder exists, but "vitepress" package is not installed in package.json.`
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return diagnostics;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async analyze(node, filePath) {
|
|
422
|
+
// If it's a config file inside .vitepress, mark it
|
|
423
|
+
if (filePath.includes('/.vitepress/config.') || filePath.includes('/.vitepress/theme/')) {
|
|
424
|
+
node.isEntry = true;
|
|
425
|
+
node.isVitepressInternal = true;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
285
428
|
}
|
|
286
429
|
|
|
287
430
|
export class GatsbyPlugin extends BasePlugin {
|
|
@@ -321,6 +464,14 @@ export class NextJsPlugin extends BasePlugin {
|
|
|
321
464
|
}
|
|
322
465
|
return false;
|
|
323
466
|
}
|
|
467
|
+
async analyze(node, filePath) {
|
|
468
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
469
|
+
node.isEntry = true;
|
|
470
|
+
if (node.rawCode?.includes('rewrites')) node.hasNextRewrites = true;
|
|
471
|
+
if (node.rawCode?.includes('redirects')) node.hasNextRedirects = true;
|
|
472
|
+
if (node.rawCode?.includes('images:')) node.hasNextImageConfig = true;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
324
475
|
}
|
|
325
476
|
|
|
326
477
|
export class ReactPlugin extends BasePlugin {
|
|
@@ -443,6 +594,42 @@ export class VitePlugin extends BasePlugin {
|
|
|
443
594
|
}
|
|
444
595
|
return false;
|
|
445
596
|
}
|
|
597
|
+
detectEntryPoints(content, filePath) {
|
|
598
|
+
if (filePath.includes('vite.config.')) {
|
|
599
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
600
|
+
const { entries } = parser.parse(content, filePath);
|
|
601
|
+
return Array.from(entries);
|
|
602
|
+
}
|
|
603
|
+
return [];
|
|
604
|
+
}
|
|
605
|
+
async analyze(node, filePath) {
|
|
606
|
+
if (filePath.includes('vite.config.')) {
|
|
607
|
+
// Mark config as an entry so its own dependencies are tracked
|
|
608
|
+
node.isEntry = true;
|
|
609
|
+
|
|
610
|
+
// UPGRADE: Use FrameworkConfigParser for deep structural extraction
|
|
611
|
+
if (node.rawCode) {
|
|
612
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
613
|
+
const { aliases, entries } = parser.parse(node.rawCode, filePath);
|
|
614
|
+
|
|
615
|
+
if (aliases.size > 0) {
|
|
616
|
+
node.frameworkAliases = aliases;
|
|
617
|
+
node.viteAliasesDetected = true;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (entries.size > 0) {
|
|
621
|
+
node.frameworkEntries = entries;
|
|
622
|
+
node.hasCustomViteInputs = true;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Detect Plugins (Keep existing simple check)
|
|
626
|
+
const pluginsPattern = /plugins\s*:\s*\[([\s\S]*?)\]/g;
|
|
627
|
+
if (pluginsPattern.test(node.rawCode)) {
|
|
628
|
+
node.hasVitePlugins = true;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
446
633
|
}
|
|
447
634
|
|
|
448
635
|
export class EsbuildPlugin extends BasePlugin {
|
|
@@ -479,6 +666,30 @@ export class WebpackPlugin extends BasePlugin {
|
|
|
479
666
|
}
|
|
480
667
|
return false;
|
|
481
668
|
}
|
|
669
|
+
detectEntryPoints(content, filePath) {
|
|
670
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
671
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
672
|
+
const { entries } = parser.parse(content, filePath);
|
|
673
|
+
return Array.from(entries);
|
|
674
|
+
}
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
detectEntryPoints(content, filePath) {
|
|
678
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
679
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
680
|
+
const { entries } = parser.parse(content, filePath);
|
|
681
|
+
return Array.from(entries);
|
|
682
|
+
}
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
async analyze(node, filePath) {
|
|
686
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
687
|
+
node.isEntry = true;
|
|
688
|
+
if (node.rawCode?.includes('entry:')) node.hasWebpackEntries = true;
|
|
689
|
+
if (node.rawCode?.includes('resolve:')) node.hasWebpackResolve = true;
|
|
690
|
+
if (node.rawCode?.includes('plugins:')) node.hasWebpackPlugins = true;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
482
693
|
}
|
|
483
694
|
|
|
484
695
|
export class ParcelPlugin extends BasePlugin {
|
|
@@ -521,6 +732,14 @@ export class TailwindPlugin extends BasePlugin {
|
|
|
521
732
|
}
|
|
522
733
|
return false;
|
|
523
734
|
}
|
|
735
|
+
async analyze(node, filePath) {
|
|
736
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
737
|
+
node.isEntry = true;
|
|
738
|
+
if (node.rawCode?.includes('content:')) node.hasTailwindContent = true;
|
|
739
|
+
if (node.rawCode?.includes('theme:')) node.hasTailwindTheme = true;
|
|
740
|
+
if (node.rawCode?.includes('plugins:')) node.hasTailwindPlugins = true;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
524
743
|
}
|
|
525
744
|
|
|
526
745
|
export class PostcssPlugin extends BasePlugin {
|
|
@@ -665,6 +884,13 @@ export class JestPlugin extends BasePlugin {
|
|
|
665
884
|
async isActive(baseDir) {
|
|
666
885
|
try { await fs.access(path.join(baseDir, 'jest.config.js')); return true; } catch { return false; }
|
|
667
886
|
}
|
|
887
|
+
async analyze(node, filePath) {
|
|
888
|
+
if (filePath.endsWith('jest.config.js')) {
|
|
889
|
+
node.isEntry = true;
|
|
890
|
+
if (node.rawCode?.includes('testEnvironment:')) node.hasJestTestEnv = true;
|
|
891
|
+
if (node.rawCode?.includes('setupFiles:')) node.hasJestSetupFiles = true;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
668
894
|
}
|
|
669
895
|
|
|
670
896
|
export class VitestPlugin extends BasePlugin {
|
|
@@ -677,6 +903,39 @@ export class VitestPlugin extends BasePlugin {
|
|
|
677
903
|
}
|
|
678
904
|
return false;
|
|
679
905
|
}
|
|
906
|
+
detectEntryPoints(content, filePath) {
|
|
907
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
908
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
909
|
+
const { entries } = parser.parse(content, filePath);
|
|
910
|
+
return Array.from(entries);
|
|
911
|
+
}
|
|
912
|
+
return [];
|
|
913
|
+
}
|
|
914
|
+
detectEntryPoints(content, filePath) {
|
|
915
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
916
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
917
|
+
const { entries } = parser.parse(content, filePath);
|
|
918
|
+
return Array.from(entries);
|
|
919
|
+
}
|
|
920
|
+
return [];
|
|
921
|
+
}
|
|
922
|
+
async analyze(node, filePath) {
|
|
923
|
+
if (this.getConfigFiles().some(f => filePath.endsWith(f))) {
|
|
924
|
+
node.isEntry = true;
|
|
925
|
+
if (node.rawCode) {
|
|
926
|
+
const parser = new FrameworkConfigParser(this.context);
|
|
927
|
+
const { aliases, entries } = parser.parse(node.rawCode, filePath);
|
|
928
|
+
if (aliases.size > 0) {
|
|
929
|
+
node.frameworkAliases = aliases;
|
|
930
|
+
node.hasWebpackAliases = true;
|
|
931
|
+
}
|
|
932
|
+
if (entries.size > 0) {
|
|
933
|
+
node.frameworkEntries = entries;
|
|
934
|
+
node.hasWebpackEntries = true;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
680
939
|
}
|
|
681
940
|
|
|
682
941
|
export class PlaywrightPlugin extends BasePlugin {
|