pkg-scaffold 3.2.0 → 3.3.2
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/LICENSE +211 -21
- package/NOTICE +13 -0
- package/bin/cli.js +52 -59
- package/package.json +8 -6
- package/src/EngineContext.js +24 -20
- package/src/api/HeadlessAPI.js +376 -0
- package/src/api/PluginSDK.js +299 -0
- package/src/ast/ASTAnalyzer.js +3 -2
- package/src/ast/OxcAnalyzer.js +17 -7
- package/src/index.js +119 -148
- package/src/performance/SecretDetector.js +378 -0
- package/src/plugins/KnipAdapter.js +106 -0
- package/src/plugins/PluginRegistry.js +22 -4
- package/src/plugins/ecosystems/BackendServices.js +59 -0
- package/src/plugins/ecosystems/ModernFrameworks.js +157 -0
- package/src/resolution/CircularDetector.js +122 -0
- package/src/resolution/PathMapper.js +12 -2
- package/src/resolution/WorkSpaceGraph.js +10 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Headless API for pkg-scaffold v4.0.0
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* Provides a programmatic interface for integrating pkg-scaffold into
|
|
6
|
+
* custom workflows, CI/CD pipelines, and third-party tools.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Full control over analysis and refactoring operations
|
|
10
|
+
* - Event-driven architecture for real-time feedback
|
|
11
|
+
* - Streaming results for large codebases
|
|
12
|
+
* - Plugin SDK integration
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import EventEmitter from 'events';
|
|
16
|
+
import { RefactoringEngine } from '../index.js';
|
|
17
|
+
|
|
18
|
+
export class HeadlessAPI extends EventEmitter {
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
super();
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.engine = null;
|
|
23
|
+
this.analysisResults = null;
|
|
24
|
+
this.isRunning = false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize the API with a project context
|
|
29
|
+
* @param {string} projectRoot - Root directory of the project
|
|
30
|
+
* @param {Object} config - Configuration options
|
|
31
|
+
* @returns {Promise<void>}
|
|
32
|
+
*/
|
|
33
|
+
async initialize(projectRoot, config = {}) {
|
|
34
|
+
try {
|
|
35
|
+
this.emit('initialize:start', { projectRoot });
|
|
36
|
+
|
|
37
|
+
const engineOptions = {
|
|
38
|
+
cwd: projectRoot,
|
|
39
|
+
allowAutoFix: config.autoFix !== false,
|
|
40
|
+
skipConfirm: config.skipConfirm || false,
|
|
41
|
+
verbose: config.verbose || false,
|
|
42
|
+
...config
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
this.engine = new RefactoringEngine(engineOptions);
|
|
46
|
+
await this.engine.context.initialize();
|
|
47
|
+
|
|
48
|
+
this.emit('initialize:complete', {
|
|
49
|
+
projectRoot,
|
|
50
|
+
config: this.engine.context
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
this.emit('initialize:error', { error });
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Analyze the codebase without making changes
|
|
60
|
+
* @returns {Promise<Object>} Analysis results
|
|
61
|
+
*/
|
|
62
|
+
async analyze() {
|
|
63
|
+
if (!this.engine) {
|
|
64
|
+
throw new Error('API not initialized. Call initialize() first.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
this.isRunning = true;
|
|
69
|
+
this.emit('analysis:start');
|
|
70
|
+
|
|
71
|
+
// Initialize path mappings and workspace graph
|
|
72
|
+
await this.engine.pathMapper.loadMappings(this.engine.context.tsconfigFilename);
|
|
73
|
+
|
|
74
|
+
if (this.engine.context.isWorkspaceEnabled) {
|
|
75
|
+
this.emit('analysis:workspace-mapping-start');
|
|
76
|
+
await this.engine.workspaceGraph.initializeWorkspaceMesh();
|
|
77
|
+
this.emit('analysis:workspace-mapping-complete');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Load cache manifest
|
|
81
|
+
const cacheManifest = await this.engine.cacheManager.loadCacheManifest();
|
|
82
|
+
|
|
83
|
+
// Discover source files
|
|
84
|
+
this.emit('analysis:file-discovery-start');
|
|
85
|
+
const fileList = [];
|
|
86
|
+
await this.engine.discoverSourceFiles(this.engine.context.cwd, fileList);
|
|
87
|
+
this.engine.context.metrics.totalFilesScanned = fileList.length;
|
|
88
|
+
this.emit('analysis:file-discovery-complete', { fileCount: fileList.length });
|
|
89
|
+
|
|
90
|
+
// Identify framework ecosystems
|
|
91
|
+
this.emit('analysis:framework-detection-start');
|
|
92
|
+
const activeFrameworkEcosystems = await this.engine.magicDetector.identifyActiveProjectEcosystems(
|
|
93
|
+
this.engine.context.cwd
|
|
94
|
+
);
|
|
95
|
+
this.emit('analysis:framework-detection-complete', { ecosystems: activeFrameworkEcosystems });
|
|
96
|
+
|
|
97
|
+
// Process files
|
|
98
|
+
this.emit('analysis:file-processing-start');
|
|
99
|
+
const sourceCodeFilesList = [];
|
|
100
|
+
for (const file of fileList) {
|
|
101
|
+
if (file.endsWith('package.json')) {
|
|
102
|
+
await this.engine.auditManifestSupplyChain(file);
|
|
103
|
+
} else {
|
|
104
|
+
sourceCodeFilesList.push(file);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Initialize TypeScript program for AST analysis (required before processFile)
|
|
109
|
+
if (sourceCodeFilesList.length > 0) {
|
|
110
|
+
try {
|
|
111
|
+
this.engine.analyzer.initProgram(sourceCodeFilesList);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
if (this.engine.context.verbose) {
|
|
114
|
+
console.warn('Warning: Failed to initialize TypeScript program:', e.message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Parallel processing
|
|
120
|
+
let parallelParseCompleted = false;
|
|
121
|
+
if (sourceCodeFilesList.length > 10) {
|
|
122
|
+
parallelParseCompleted = await this.engine.workerPool.parallelAnalyzeCodebase(
|
|
123
|
+
sourceCodeFilesList,
|
|
124
|
+
this.engine
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Sequential processing
|
|
129
|
+
for (let i = 0; i < sourceCodeFilesList.length; i++) {
|
|
130
|
+
const filePath = sourceCodeFilesList[i];
|
|
131
|
+
const node = this.engine.context.createNode(filePath);
|
|
132
|
+
const currentHash = await this.engine.cacheManager.computeHash(filePath);
|
|
133
|
+
node.contentHash = currentHash;
|
|
134
|
+
|
|
135
|
+
const isFileCached = cacheManifest[filePath] && cacheManifest[filePath].hash === currentHash;
|
|
136
|
+
|
|
137
|
+
if (isFileCached) {
|
|
138
|
+
this.engine.context.metrics.cacheHits++;
|
|
139
|
+
this.engine.hydrateNodeFromCache(node, cacheManifest[filePath]);
|
|
140
|
+
} else if (!parallelParseCompleted) {
|
|
141
|
+
this.engine.context.metrics.cacheMisses++;
|
|
142
|
+
await this.engine.analyzer.processFile(filePath, node);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.engine.magicDetector.injectVirtualConsumerEdges(filePath, node, activeFrameworkEcosystems);
|
|
146
|
+
node.externalPackageUsage.forEach(pkg => this.engine.context.usedExternalPackages.add(pkg));
|
|
147
|
+
|
|
148
|
+
// Emit progress
|
|
149
|
+
if ((i + 1) % Math.ceil(sourceCodeFilesList.length / 10) === 0) {
|
|
150
|
+
this.emit('analysis:file-processing-progress', {
|
|
151
|
+
processed: i + 1,
|
|
152
|
+
total: sourceCodeFilesList.length,
|
|
153
|
+
percentage: Math.round(((i + 1) / sourceCodeFilesList.length) * 100)
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Link dependency graph
|
|
159
|
+
this.emit('analysis:graph-linking-start');
|
|
160
|
+
await this.engine.linkDependencyGraph();
|
|
161
|
+
this.emit('analysis:graph-linking-complete');
|
|
162
|
+
|
|
163
|
+
// Generate summary
|
|
164
|
+
this.analysisResults = this.engine.context.generateSummaryReport();
|
|
165
|
+
this.emit('analysis:complete', this.analysisResults);
|
|
166
|
+
|
|
167
|
+
this.isRunning = false;
|
|
168
|
+
return this.analysisResults;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
this.isRunning = false;
|
|
171
|
+
this.emit('analysis:error', { error });
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get detailed impact analysis for a specific file or export
|
|
178
|
+
* @param {string} filePath - Path to the file
|
|
179
|
+
* @param {string} symbol - Optional: specific export symbol to analyze
|
|
180
|
+
* @returns {Promise<Object>} Impact analysis results
|
|
181
|
+
*/
|
|
182
|
+
async getImpactAnalysis(filePath, symbol = null) {
|
|
183
|
+
if (!this.engine) {
|
|
184
|
+
throw new Error('API not initialized. Call initialize() first.');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const node = this.engine.context.graph.get(filePath);
|
|
189
|
+
if (!node) {
|
|
190
|
+
throw new Error(`File not found in analysis graph: ${filePath}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const impact = {
|
|
194
|
+
file: filePath,
|
|
195
|
+
symbol,
|
|
196
|
+
directDependents: Array.from(node.incomingEdges),
|
|
197
|
+
dependencies: Array.from(node.outgoingEdges),
|
|
198
|
+
internalExports: symbol
|
|
199
|
+
? [symbol]
|
|
200
|
+
: Array.from(node.internalExports.keys()),
|
|
201
|
+
externalPackages: Array.from(node.externalPackageUsage)
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
if (symbol) {
|
|
205
|
+
const safety = await this.engine.impactAnalyzer.verifyRefactorSafety(
|
|
206
|
+
filePath,
|
|
207
|
+
symbol,
|
|
208
|
+
this.engine.context.graph
|
|
209
|
+
);
|
|
210
|
+
impact.refactorSafety = safety;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return impact;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
this.emit('impact-analysis:error', { error, filePath, symbol });
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Apply refactoring changes with automatic rollback on test failure
|
|
222
|
+
* @param {Object} changes - Changes to apply
|
|
223
|
+
* @returns {Promise<Object>} Refactoring results
|
|
224
|
+
*/
|
|
225
|
+
async applyRefactoring(changes = {}) {
|
|
226
|
+
if (!this.engine) {
|
|
227
|
+
throw new Error('API not initialized. Call initialize() first.');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!this.analysisResults) {
|
|
231
|
+
throw new Error('No analysis results available. Call analyze() first.');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
this.isRunning = true;
|
|
236
|
+
this.emit('refactoring:start', changes);
|
|
237
|
+
|
|
238
|
+
const refactoringResults = {
|
|
239
|
+
filesDeleted: [],
|
|
240
|
+
exportsRemoved: [],
|
|
241
|
+
dependenciesRemoved: [],
|
|
242
|
+
errors: []
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
await this.engine.selfHealer.runSelfHealingLifecycle(async () => {
|
|
246
|
+
// Delete dead files
|
|
247
|
+
const filesToDelete = changes.deleteDeadFiles !== false
|
|
248
|
+
? this.analysisResults.structuralIssuesDetected.deadFiles
|
|
249
|
+
: [];
|
|
250
|
+
|
|
251
|
+
for (const relPath of filesToDelete) {
|
|
252
|
+
try {
|
|
253
|
+
const absPath = require('path').resolve(this.engine.context.cwd, relPath);
|
|
254
|
+
await this.engine.txManager.stageDeletion(absPath);
|
|
255
|
+
refactoringResults.filesDeleted.push(relPath);
|
|
256
|
+
this.emit('refactoring:file-deleted', { file: relPath });
|
|
257
|
+
} catch (error) {
|
|
258
|
+
refactoringResults.errors.push({ type: 'file-deletion', file: relPath, error: error.message });
|
|
259
|
+
this.emit('refactoring:error', { type: 'file-deletion', file: relPath, error });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Remove unused exports
|
|
264
|
+
const exportsToRemove = changes.removeUnusedExports !== false
|
|
265
|
+
? this.analysisResults.structuralIssuesDetected.deadExports
|
|
266
|
+
: [];
|
|
267
|
+
|
|
268
|
+
for (const unusedExport of exportsToRemove) {
|
|
269
|
+
try {
|
|
270
|
+
const absPath = require('path').resolve(this.engine.context.cwd, unusedExport.file);
|
|
271
|
+
const node = this.engine.context.graph.get(absPath);
|
|
272
|
+
|
|
273
|
+
if (node) {
|
|
274
|
+
const safety = await this.engine.impactAnalyzer.verifyRefactorSafety(
|
|
275
|
+
absPath,
|
|
276
|
+
unusedExport.symbol,
|
|
277
|
+
this.engine.context.graph
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (safety.isSafeToPrune) {
|
|
281
|
+
const nextText = await this.engine.sourceRewriter.stripNamedExportSignature(
|
|
282
|
+
absPath,
|
|
283
|
+
unusedExport.symbol,
|
|
284
|
+
node.internalExports.get(unusedExport.symbol)
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
await this.engine.txManager.stageWrite(absPath, nextText);
|
|
288
|
+
await this.engine.typeIntegrity.synchronizeDeclarationFile(absPath, unusedExport.symbol);
|
|
289
|
+
|
|
290
|
+
refactoringResults.exportsRemoved.push(unusedExport);
|
|
291
|
+
this.emit('refactoring:export-removed', unusedExport);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} catch (error) {
|
|
295
|
+
refactoringResults.errors.push({
|
|
296
|
+
type: 'export-removal',
|
|
297
|
+
export: unusedExport.symbol,
|
|
298
|
+
file: unusedExport.file,
|
|
299
|
+
error: error.message
|
|
300
|
+
});
|
|
301
|
+
this.emit('refactoring:error', { type: 'export-removal', export: unusedExport, error });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Remove unused dependencies
|
|
306
|
+
const depsToRemove = changes.removeUnusedDependencies !== false
|
|
307
|
+
? this.analysisResults.structuralIssuesDetected.unusedDependencies
|
|
308
|
+
: [];
|
|
309
|
+
|
|
310
|
+
for (const dep of depsToRemove) {
|
|
311
|
+
try {
|
|
312
|
+
const absPath = require('path').resolve(this.engine.context.cwd, dep.manifest);
|
|
313
|
+
// TODO: Implement package.json dependency removal
|
|
314
|
+
refactoringResults.dependenciesRemoved.push(dep);
|
|
315
|
+
this.emit('refactoring:dependency-removed', dep);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
refactoringResults.errors.push({
|
|
318
|
+
type: 'dependency-removal',
|
|
319
|
+
package: dep.package,
|
|
320
|
+
error: error.message
|
|
321
|
+
});
|
|
322
|
+
this.emit('refactoring:error', { type: 'dependency-removal', package: dep.package, error });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
this.isRunning = false;
|
|
328
|
+
this.emit('refactoring:complete', refactoringResults);
|
|
329
|
+
return refactoringResults;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
this.isRunning = false;
|
|
332
|
+
this.emit('refactoring:error', { error });
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Get current analysis metrics
|
|
339
|
+
* @returns {Object} Metrics
|
|
340
|
+
*/
|
|
341
|
+
getMetrics() {
|
|
342
|
+
if (!this.engine) {
|
|
343
|
+
throw new Error('API not initialized. Call initialize() first.');
|
|
344
|
+
}
|
|
345
|
+
return this.engine.context.metrics;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get all registered plugins
|
|
350
|
+
* @returns {Array} Plugin instances
|
|
351
|
+
*/
|
|
352
|
+
getPlugins() {
|
|
353
|
+
if (!this.engine) {
|
|
354
|
+
throw new Error('API not initialized. Call initialize() first.');
|
|
355
|
+
}
|
|
356
|
+
return this.engine.context.pluginRegistry?.getPlugins() || [];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get analysis results
|
|
361
|
+
* @returns {Object} Analysis results
|
|
362
|
+
*/
|
|
363
|
+
getAnalysisResults() {
|
|
364
|
+
return this.analysisResults;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Check if API is currently running
|
|
369
|
+
* @returns {boolean}
|
|
370
|
+
*/
|
|
371
|
+
isProcessing() {
|
|
372
|
+
return this.isRunning;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export default HeadlessAPI;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* Plugin SDK for pkg-scaffold v4.0.0
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* Provides utilities and helpers for developing custom plugins that extend
|
|
6
|
+
* pkg-scaffold's analysis and healing capabilities.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { BasePlugin } from '../plugins/BasePlugin.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extended plugin base class with SDK utilities
|
|
13
|
+
*/
|
|
14
|
+
export class PluginSDKBase extends BasePlugin {
|
|
15
|
+
constructor(context) {
|
|
16
|
+
super(context);
|
|
17
|
+
this.hooks = new Map();
|
|
18
|
+
this.transformers = [];
|
|
19
|
+
this.validators = [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register a hook for a specific lifecycle event
|
|
24
|
+
* @param {string} eventName - Event name (e.g., 'analyze:start', 'refactor:complete')
|
|
25
|
+
* @param {Function} handler - Handler function
|
|
26
|
+
*/
|
|
27
|
+
registerHook(eventName, handler) {
|
|
28
|
+
if (!this.hooks.has(eventName)) {
|
|
29
|
+
this.hooks.set(eventName, []);
|
|
30
|
+
}
|
|
31
|
+
this.hooks.get(eventName).push(handler);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Emit a hook event
|
|
36
|
+
* @param {string} eventName - Event name
|
|
37
|
+
* @param {Object} data - Event data
|
|
38
|
+
*/
|
|
39
|
+
async emitHook(eventName, data) {
|
|
40
|
+
const handlers = this.hooks.get(eventName) || [];
|
|
41
|
+
for (const handler of handlers) {
|
|
42
|
+
await handler(data);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Register a code transformer
|
|
48
|
+
* @param {Function} transformer - Transformer function
|
|
49
|
+
*/
|
|
50
|
+
registerTransformer(transformer) {
|
|
51
|
+
this.transformers.push(transformer);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Register a validator
|
|
56
|
+
* @param {Function} validator - Validator function
|
|
57
|
+
*/
|
|
58
|
+
registerValidator(validator) {
|
|
59
|
+
this.validators.push(validator);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Apply all registered transformers to a code string
|
|
64
|
+
* @param {string} code - Source code
|
|
65
|
+
* @param {string} filePath - File path
|
|
66
|
+
* @returns {Promise<string>} Transformed code
|
|
67
|
+
*/
|
|
68
|
+
async applyTransformers(code, filePath) {
|
|
69
|
+
let result = code;
|
|
70
|
+
for (const transformer of this.transformers) {
|
|
71
|
+
result = await transformer(result, filePath);
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run all validators on a code string
|
|
78
|
+
* @param {string} code - Source code
|
|
79
|
+
* @param {string} filePath - File path
|
|
80
|
+
* @returns {Promise<Array>} Validation errors
|
|
81
|
+
*/
|
|
82
|
+
async runValidators(code, filePath) {
|
|
83
|
+
const errors = [];
|
|
84
|
+
for (const validator of this.validators) {
|
|
85
|
+
const result = await validator(code, filePath);
|
|
86
|
+
if (result && result.length > 0) {
|
|
87
|
+
errors.push(...result);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return errors;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* SDK utilities for plugin development
|
|
96
|
+
*/
|
|
97
|
+
export class PluginSDK {
|
|
98
|
+
/**
|
|
99
|
+
* Create a custom plugin class
|
|
100
|
+
* @param {Object} config - Plugin configuration
|
|
101
|
+
* @returns {Class} Plugin class
|
|
102
|
+
*/
|
|
103
|
+
static createPlugin(config) {
|
|
104
|
+
return class CustomPlugin extends PluginSDKBase {
|
|
105
|
+
get name() {
|
|
106
|
+
return config.name;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getConfigFiles() {
|
|
110
|
+
return config.configFiles || [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getRoutePatterns() {
|
|
114
|
+
return config.routePatterns || [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getRequiredSystemContracts() {
|
|
118
|
+
return config.requiredContracts || ['default'];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async isActive(baseDir) {
|
|
122
|
+
if (config.isActive) {
|
|
123
|
+
return await config.isActive(baseDir);
|
|
124
|
+
}
|
|
125
|
+
return super.isActive(baseDir);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async initialize() {
|
|
129
|
+
if (config.initialize) {
|
|
130
|
+
await config.initialize(this.context);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async analyze(node, filePath) {
|
|
135
|
+
if (config.analyze) {
|
|
136
|
+
return await config.analyze(node, filePath, this.context);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async transform(code, filePath) {
|
|
141
|
+
if (config.transform) {
|
|
142
|
+
return await config.transform(code, filePath, this.context);
|
|
143
|
+
}
|
|
144
|
+
return code;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async validate(code, filePath) {
|
|
148
|
+
if (config.validate) {
|
|
149
|
+
return await config.validate(code, filePath, this.context);
|
|
150
|
+
}
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create a CSS-in-JS analyzer plugin
|
|
158
|
+
* @param {Object} config - Configuration for CSS-in-JS analysis
|
|
159
|
+
* @returns {Class} Plugin class
|
|
160
|
+
*/
|
|
161
|
+
static createCSSInJSPlugin(config = {}) {
|
|
162
|
+
const cssLibraries = config.libraries || [
|
|
163
|
+
'styled-components',
|
|
164
|
+
'emotion',
|
|
165
|
+
'@emotion/react',
|
|
166
|
+
'@emotion/styled',
|
|
167
|
+
'linaria',
|
|
168
|
+
'vanilla-extract'
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
return this.createPlugin({
|
|
172
|
+
name: config.name || 'css-in-js-analyzer',
|
|
173
|
+
configFiles: [],
|
|
174
|
+
routePatterns: [/\.(tsx?|jsx?)$/],
|
|
175
|
+
requiredContracts: [],
|
|
176
|
+
|
|
177
|
+
async analyze(node, filePath) {
|
|
178
|
+
// Track CSS-in-JS imports
|
|
179
|
+
for (const lib of cssLibraries) {
|
|
180
|
+
if (node.explicitImports.has(lib)) {
|
|
181
|
+
node.cssInJsLibraries = node.cssInJsLibraries || new Set();
|
|
182
|
+
node.cssInJsLibraries.add(lib);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Detect styled component definitions
|
|
187
|
+
const styledPattern = /(?:styled|css|keyframes)\s*\.\w+|styled\(\w+\)/g;
|
|
188
|
+
for (const match of (node.rawCode || '').matchAll(styledPattern)) {
|
|
189
|
+
node.styledComponentUsages = node.styledComponentUsages || [];
|
|
190
|
+
node.styledComponentUsages.push(match[0]);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
async validate(code, filePath) {
|
|
195
|
+
const errors = [];
|
|
196
|
+
// Check for unused CSS-in-JS definitions
|
|
197
|
+
const unusedStylesPattern = /(?:const|let|var)\s+(\w+)\s*=\s*(?:styled|css)\./g;
|
|
198
|
+
const matches = [...code.matchAll(unusedStylesPattern)];
|
|
199
|
+
|
|
200
|
+
for (const match of matches) {
|
|
201
|
+
const styleName = match[1];
|
|
202
|
+
const usagePattern = new RegExp(`\\b${styleName}\\b`);
|
|
203
|
+
if (!usagePattern.test(code.substring(match.index + match[0].length))) {
|
|
204
|
+
errors.push({
|
|
205
|
+
type: 'unused-style',
|
|
206
|
+
name: styleName,
|
|
207
|
+
line: code.substring(0, match.index).split('\n').length,
|
|
208
|
+
message: `Unused CSS-in-JS definition: ${styleName}`
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return errors;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Create an asset tracking plugin
|
|
220
|
+
* @param {Object} config - Configuration for asset tracking
|
|
221
|
+
* @returns {Class} Plugin class
|
|
222
|
+
*/
|
|
223
|
+
static createAssetTrackingPlugin(config = {}) {
|
|
224
|
+
const assetExtensions = config.extensions || [
|
|
225
|
+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp',
|
|
226
|
+
'.mp4', '.webm', '.mp3', '.wav',
|
|
227
|
+
'.woff', '.woff2', '.ttf', '.eot'
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
return this.createPlugin({
|
|
231
|
+
name: config.name || 'asset-tracker',
|
|
232
|
+
configFiles: [],
|
|
233
|
+
routePatterns: [/\.(tsx?|jsx?)$/],
|
|
234
|
+
|
|
235
|
+
async analyze(node, filePath) {
|
|
236
|
+
node.assetReferences = node.assetReferences || new Set();
|
|
237
|
+
|
|
238
|
+
// Track asset imports
|
|
239
|
+
const assetImportPattern = /import\s+(?:\*\s+as\s+\w+|[\w\s,{}]+)\s+from\s+['"]([^'"]+(?:${assetExtensions.join('|')}))['"]/g;
|
|
240
|
+
for (const match of (node.rawCode || '').matchAll(assetImportPattern)) {
|
|
241
|
+
node.assetReferences.add(match[1]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Track asset requires
|
|
245
|
+
const assetRequirePattern = /require\s*\(\s*['"]([^'"]+(?:${assetExtensions.join('|')}))['"]\s*\)/g;
|
|
246
|
+
for (const match of (node.rawCode || '').matchAll(assetRequirePattern)) {
|
|
247
|
+
node.assetReferences.add(match[1]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Track asset URLs in strings
|
|
251
|
+
const assetUrlPattern = /['"]([^'"]*(?:${assetExtensions.join('|')}))['"]/g;
|
|
252
|
+
for (const match of (node.rawCode || '').matchAll(assetUrlPattern)) {
|
|
253
|
+
node.assetReferences.add(match[1]);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Create a monorepo awareness plugin
|
|
261
|
+
* @param {Object} config - Configuration for monorepo support
|
|
262
|
+
* @returns {Class} Plugin class
|
|
263
|
+
*/
|
|
264
|
+
static createMonorepoPlugin(config = {}) {
|
|
265
|
+
return this.createPlugin({
|
|
266
|
+
name: config.name || 'monorepo-aware',
|
|
267
|
+
configFiles: config.configFiles || ['nx.json', 'pnpm-workspace.yaml', 'lerna.json'],
|
|
268
|
+
|
|
269
|
+
async analyze(node, filePath) {
|
|
270
|
+
// Track workspace package references
|
|
271
|
+
node.workspaceReferences = node.workspaceReferences || new Set();
|
|
272
|
+
|
|
273
|
+
// Detect workspace imports (e.g., @workspace/package-name)
|
|
274
|
+
const workspacePattern = /@[\w-]+\/[\w-]+/g;
|
|
275
|
+
for (const match of (node.rawCode || '').matchAll(workspacePattern)) {
|
|
276
|
+
node.workspaceReferences.add(match[0]);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create a circular dependency detector plugin
|
|
284
|
+
* @param {Object} config - Configuration
|
|
285
|
+
* @returns {Class} Plugin class
|
|
286
|
+
*/
|
|
287
|
+
static createCircularDepPlugin(config = {}) {
|
|
288
|
+
return this.createPlugin({
|
|
289
|
+
name: config.name || 'circular-dep-detector',
|
|
290
|
+
|
|
291
|
+
async analyze(node, filePath) {
|
|
292
|
+
node.potentialCycles = node.potentialCycles || [];
|
|
293
|
+
// Cycle detection will be handled by the main engine
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export default PluginSDK;
|
package/src/ast/ASTAnalyzer.js
CHANGED
|
@@ -40,8 +40,9 @@ export class ASTAnalyzer {
|
|
|
40
40
|
*/
|
|
41
41
|
async processFile(filePath, fileNode) {
|
|
42
42
|
// Fast Path: Use OXC for rapid scanning if type checking is not strictly required for this file
|
|
43
|
-
if (this.context.fastMode) {
|
|
44
|
-
|
|
43
|
+
if (this.context.fastMode && this.oxc.isAvailable) {
|
|
44
|
+
const success = await this.oxc.processFile(filePath, fileNode);
|
|
45
|
+
if (success) return true;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
if (!this.program) {
|