pulse-js-framework 1.4.1 → 1.4.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/README.md +414 -414
- package/cli/analyze.js +499 -499
- package/cli/build.js +341 -341
- package/cli/format.js +704 -704
- package/cli/index.js +398 -398
- package/cli/lint.js +642 -642
- package/cli/utils/file-utils.js +298 -298
- package/compiler/lexer.js +766 -766
- package/compiler/parser.js +1797 -1797
- package/compiler/transformer.js +1332 -1332
- package/index.js +1 -1
- package/package.json +68 -68
- package/runtime/router.js +596 -596
package/cli/analyze.js
CHANGED
|
@@ -1,499 +1,499 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse CLI - Analyze Command
|
|
3
|
-
* Analyzes bundle size and dependencies
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readFileSync, statSync, existsSync } from 'fs';
|
|
7
|
-
import { join, dirname, basename, relative } from 'path';
|
|
8
|
-
import { findPulseFiles, parseArgs, formatBytes, relativePath, resolveImportPath } from './utils/file-utils.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Analyze the bundle/project
|
|
12
|
-
* @param {string} root - Project root directory
|
|
13
|
-
* @returns {object} Analysis results
|
|
14
|
-
*/
|
|
15
|
-
export async function analyzeBundle(root = '.') {
|
|
16
|
-
const { parse } = await import('../compiler/index.js');
|
|
17
|
-
|
|
18
|
-
// Find all source files
|
|
19
|
-
const pulseFiles = findPulseFiles([join(root, 'src')], { extensions: ['.pulse'] });
|
|
20
|
-
const jsFiles = findPulseFiles([join(root, 'src')], { extensions: ['.js'] });
|
|
21
|
-
const allFiles = [...pulseFiles, ...jsFiles];
|
|
22
|
-
|
|
23
|
-
// Calculate summary
|
|
24
|
-
const summary = calculateSummary(allFiles, pulseFiles, jsFiles);
|
|
25
|
-
|
|
26
|
-
// Analyze each file
|
|
27
|
-
const fileBreakdown = await analyzeFiles(allFiles, parse);
|
|
28
|
-
|
|
29
|
-
// Build import graph
|
|
30
|
-
const importGraph = await buildImportGraph(allFiles, parse);
|
|
31
|
-
|
|
32
|
-
// Calculate complexity metrics
|
|
33
|
-
const complexity = await calculateComplexity(pulseFiles, parse);
|
|
34
|
-
|
|
35
|
-
// Detect dead code
|
|
36
|
-
const deadCode = detectDeadCode(allFiles, importGraph, root);
|
|
37
|
-
|
|
38
|
-
// Analyze state usage
|
|
39
|
-
const stateUsage = await analyzeStateUsage(pulseFiles, parse);
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
summary,
|
|
43
|
-
fileBreakdown,
|
|
44
|
-
importGraph,
|
|
45
|
-
complexity,
|
|
46
|
-
deadCode,
|
|
47
|
-
stateUsage
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Calculate summary statistics
|
|
53
|
-
*/
|
|
54
|
-
function calculateSummary(allFiles, pulseFiles, jsFiles) {
|
|
55
|
-
let totalSize = 0;
|
|
56
|
-
|
|
57
|
-
for (const file of allFiles) {
|
|
58
|
-
try {
|
|
59
|
-
const stats = statSync(file);
|
|
60
|
-
totalSize += stats.size;
|
|
61
|
-
} catch (e) {
|
|
62
|
-
// Skip inaccessible files
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
totalFiles: allFiles.length,
|
|
68
|
-
pulseFiles: pulseFiles.length,
|
|
69
|
-
jsFiles: jsFiles.length,
|
|
70
|
-
totalSize,
|
|
71
|
-
totalSizeFormatted: formatBytes(totalSize)
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Analyze individual files
|
|
77
|
-
*/
|
|
78
|
-
async function analyzeFiles(files, parse) {
|
|
79
|
-
const results = [];
|
|
80
|
-
|
|
81
|
-
for (const file of files) {
|
|
82
|
-
try {
|
|
83
|
-
const stats = statSync(file);
|
|
84
|
-
const source = readFileSync(file, 'utf-8');
|
|
85
|
-
|
|
86
|
-
const info = {
|
|
87
|
-
path: relativePath(file),
|
|
88
|
-
size: stats.size,
|
|
89
|
-
sizeFormatted: formatBytes(stats.size),
|
|
90
|
-
lines: source.split('\n').length
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
// Additional analysis for .pulse files
|
|
94
|
-
if (file.endsWith('.pulse')) {
|
|
95
|
-
try {
|
|
96
|
-
const ast = parse(source);
|
|
97
|
-
info.type = 'pulse';
|
|
98
|
-
info.componentName = ast.page?.name || basename(file, '.pulse');
|
|
99
|
-
info.stateCount = ast.state?.properties?.length || 0;
|
|
100
|
-
info.actionCount = ast.actions?.functions?.length || 0;
|
|
101
|
-
info.importCount = ast.imports?.length || 0;
|
|
102
|
-
info.hasStyles = !!ast.style;
|
|
103
|
-
} catch (e) {
|
|
104
|
-
info.type = 'pulse';
|
|
105
|
-
info.parseError = e.message;
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
info.type = 'js';
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
results.push(info);
|
|
112
|
-
} catch (e) {
|
|
113
|
-
// Skip inaccessible files
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return results.sort((a, b) => b.size - a.size);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Build import dependency graph
|
|
122
|
-
*/
|
|
123
|
-
export async function buildImportGraph(files, parse) {
|
|
124
|
-
const nodes = new Set();
|
|
125
|
-
const edges = [];
|
|
126
|
-
|
|
127
|
-
for (const file of files) {
|
|
128
|
-
const relPath = relativePath(file);
|
|
129
|
-
nodes.add(relPath);
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
const source = readFileSync(file, 'utf-8');
|
|
133
|
-
|
|
134
|
-
if (file.endsWith('.pulse')) {
|
|
135
|
-
const ast = parse(source);
|
|
136
|
-
for (const imp of ast.imports || []) {
|
|
137
|
-
const resolved = resolveImportPath(file, imp.source);
|
|
138
|
-
if (resolved) {
|
|
139
|
-
const resolvedRel = relativePath(resolved);
|
|
140
|
-
nodes.add(resolvedRel);
|
|
141
|
-
edges.push({ from: relPath, to: resolvedRel });
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
} else if (file.endsWith('.js')) {
|
|
145
|
-
// Parse JS imports with regex
|
|
146
|
-
const importRegex = /import\s+(?:.*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
147
|
-
let match;
|
|
148
|
-
while ((match = importRegex.exec(source)) !== null) {
|
|
149
|
-
const resolved = resolveImportPath(file, match[1]);
|
|
150
|
-
if (resolved) {
|
|
151
|
-
const resolvedRel = relativePath(resolved);
|
|
152
|
-
nodes.add(resolvedRel);
|
|
153
|
-
edges.push({ from: relPath, to: resolvedRel });
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
} catch (e) {
|
|
158
|
-
// Skip files with errors
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
nodes: Array.from(nodes).sort(),
|
|
164
|
-
edges
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Calculate component complexity metrics
|
|
170
|
-
*/
|
|
171
|
-
async function calculateComplexity(pulseFiles, parse) {
|
|
172
|
-
const results = [];
|
|
173
|
-
|
|
174
|
-
for (const file of pulseFiles) {
|
|
175
|
-
try {
|
|
176
|
-
const source = readFileSync(file, 'utf-8');
|
|
177
|
-
const ast = parse(source);
|
|
178
|
-
|
|
179
|
-
const metrics = {
|
|
180
|
-
file: relativePath(file),
|
|
181
|
-
componentName: ast.page?.name || basename(file, '.pulse'),
|
|
182
|
-
stateCount: ast.state?.properties?.length || 0,
|
|
183
|
-
actionCount: ast.actions?.functions?.length || 0,
|
|
184
|
-
viewDepth: calculateViewDepth(ast.view),
|
|
185
|
-
directiveCount: countDirectives(ast.view),
|
|
186
|
-
styleRuleCount: countStyleRules(ast.style),
|
|
187
|
-
importCount: ast.imports?.length || 0
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
// Calculate weighted complexity score
|
|
191
|
-
metrics.complexity = Math.round(
|
|
192
|
-
metrics.stateCount * 1 +
|
|
193
|
-
metrics.actionCount * 2 +
|
|
194
|
-
metrics.viewDepth * 1.5 +
|
|
195
|
-
metrics.directiveCount * 1 +
|
|
196
|
-
metrics.styleRuleCount * 0.5 +
|
|
197
|
-
metrics.importCount * 0.5
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
results.push(metrics);
|
|
201
|
-
} catch (e) {
|
|
202
|
-
// Skip files with parse errors
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return results.sort((a, b) => b.complexity - a.complexity);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Calculate maximum view depth
|
|
211
|
-
*/
|
|
212
|
-
function calculateViewDepth(view, depth = 0) {
|
|
213
|
-
if (!view || !view.children) return depth;
|
|
214
|
-
|
|
215
|
-
let maxDepth = depth;
|
|
216
|
-
for (const child of view.children) {
|
|
217
|
-
if (child.type === 'Element' && child.children) {
|
|
218
|
-
maxDepth = Math.max(maxDepth, calculateViewDepth(child, depth + 1));
|
|
219
|
-
}
|
|
220
|
-
if (child.type === 'IfDirective') {
|
|
221
|
-
if (child.consequent) {
|
|
222
|
-
maxDepth = Math.max(maxDepth, calculateViewDepth(child.consequent, depth + 1));
|
|
223
|
-
}
|
|
224
|
-
if (child.alternate) {
|
|
225
|
-
maxDepth = Math.max(maxDepth, calculateViewDepth(child.alternate, depth + 1));
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (child.type === 'EachDirective' && child.body) {
|
|
229
|
-
maxDepth = Math.max(maxDepth, calculateViewDepth(child.body, depth + 1));
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return maxDepth;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Count directives in view
|
|
237
|
-
*/
|
|
238
|
-
function countDirectives(view, count = 0) {
|
|
239
|
-
if (!view || !view.children) return count;
|
|
240
|
-
|
|
241
|
-
for (const child of view.children) {
|
|
242
|
-
if (child.type === 'IfDirective') count++;
|
|
243
|
-
if (child.type === 'EachDirective') count++;
|
|
244
|
-
if (child.directives) count += child.directives.length;
|
|
245
|
-
|
|
246
|
-
if (child.children) {
|
|
247
|
-
count = countDirectives(child, count);
|
|
248
|
-
}
|
|
249
|
-
if (child.consequent) {
|
|
250
|
-
count = countDirectives(child.consequent, count);
|
|
251
|
-
}
|
|
252
|
-
if (child.alternate) {
|
|
253
|
-
count = countDirectives(child.alternate, count);
|
|
254
|
-
}
|
|
255
|
-
if (child.body) {
|
|
256
|
-
count = countDirectives(child.body, count);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return count;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Count style rules
|
|
265
|
-
*/
|
|
266
|
-
function countStyleRules(style) {
|
|
267
|
-
if (!style || !style.rules) return 0;
|
|
268
|
-
|
|
269
|
-
let count = 0;
|
|
270
|
-
function countRules(rules) {
|
|
271
|
-
for (const rule of rules) {
|
|
272
|
-
count++;
|
|
273
|
-
if (rule.rules) {
|
|
274
|
-
countRules(rule.rules);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
countRules(style.rules);
|
|
279
|
-
return count;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Detect dead code (unreachable files)
|
|
284
|
-
*/
|
|
285
|
-
function detectDeadCode(allFiles, importGraph, root) {
|
|
286
|
-
const deadCode = [];
|
|
287
|
-
|
|
288
|
-
// Find entry points (main.js, index.js, App.pulse)
|
|
289
|
-
const entryPoints = new Set();
|
|
290
|
-
for (const file of allFiles) {
|
|
291
|
-
const name = basename(file);
|
|
292
|
-
if (name === 'main.js' || name === 'index.js' || name === 'App.pulse' || name === 'app.js') {
|
|
293
|
-
entryPoints.add(relativePath(file));
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Build reachability set
|
|
298
|
-
const reachable = new Set();
|
|
299
|
-
const queue = Array.from(entryPoints);
|
|
300
|
-
|
|
301
|
-
while (queue.length > 0) {
|
|
302
|
-
const current = queue.shift();
|
|
303
|
-
if (reachable.has(current)) continue;
|
|
304
|
-
reachable.add(current);
|
|
305
|
-
|
|
306
|
-
// Find all edges from this node
|
|
307
|
-
for (const edge of importGraph.edges) {
|
|
308
|
-
if (edge.from === current && !reachable.has(edge.to)) {
|
|
309
|
-
queue.push(edge.to);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Find unreachable files
|
|
315
|
-
for (const file of allFiles) {
|
|
316
|
-
const relPath = relativePath(file);
|
|
317
|
-
|
|
318
|
-
// Skip entry points and non-source files
|
|
319
|
-
if (entryPoints.has(relPath)) continue;
|
|
320
|
-
|
|
321
|
-
if (!reachable.has(relPath)) {
|
|
322
|
-
deadCode.push({
|
|
323
|
-
file: relPath,
|
|
324
|
-
reason: 'unreachable',
|
|
325
|
-
message: 'File is not imported from any entry point'
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return deadCode;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Analyze state variable usage across files
|
|
335
|
-
*/
|
|
336
|
-
async function analyzeStateUsage(pulseFiles, parse) {
|
|
337
|
-
const stateVars = new Map(); // name -> { declarations: [], references: [] }
|
|
338
|
-
|
|
339
|
-
for (const file of pulseFiles) {
|
|
340
|
-
try {
|
|
341
|
-
const source = readFileSync(file, 'utf-8');
|
|
342
|
-
const ast = parse(source);
|
|
343
|
-
const relPath = relativePath(file);
|
|
344
|
-
|
|
345
|
-
// Collect state declarations
|
|
346
|
-
if (ast.state && ast.state.properties) {
|
|
347
|
-
for (const prop of ast.state.properties) {
|
|
348
|
-
if (!stateVars.has(prop.name)) {
|
|
349
|
-
stateVars.set(prop.name, { declarations: [], files: new Set() });
|
|
350
|
-
}
|
|
351
|
-
stateVars.get(prop.name).declarations.push({
|
|
352
|
-
file: relPath,
|
|
353
|
-
line: prop.line || 1
|
|
354
|
-
});
|
|
355
|
-
stateVars.get(prop.name).files.add(relPath);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
} catch (e) {
|
|
359
|
-
// Skip files with parse errors
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Convert to array and add statistics
|
|
364
|
-
return Array.from(stateVars.entries())
|
|
365
|
-
.map(([name, info]) => ({
|
|
366
|
-
name,
|
|
367
|
-
declarationCount: info.declarations.length,
|
|
368
|
-
files: Array.from(info.files),
|
|
369
|
-
isShared: info.files.size > 1
|
|
370
|
-
}))
|
|
371
|
-
.sort((a, b) => b.declarationCount - a.declarationCount);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Format analysis results for console output
|
|
376
|
-
*/
|
|
377
|
-
function formatConsoleOutput(analysis, verbose = false) {
|
|
378
|
-
const lines = [];
|
|
379
|
-
|
|
380
|
-
// Header
|
|
381
|
-
lines.push('');
|
|
382
|
-
lines.push('═'.repeat(60));
|
|
383
|
-
lines.push(' PULSE BUNDLE ANALYSIS');
|
|
384
|
-
lines.push('═'.repeat(60));
|
|
385
|
-
|
|
386
|
-
// Summary
|
|
387
|
-
lines.push('');
|
|
388
|
-
lines.push(' SUMMARY');
|
|
389
|
-
lines.push(' ' + '─'.repeat(40));
|
|
390
|
-
lines.push(` Total files: ${analysis.summary.totalFiles}`);
|
|
391
|
-
lines.push(` .pulse files: ${analysis.summary.pulseFiles}`);
|
|
392
|
-
lines.push(` .js files: ${analysis.summary.jsFiles}`);
|
|
393
|
-
lines.push(` Total size: ${analysis.summary.totalSizeFormatted}`);
|
|
394
|
-
|
|
395
|
-
// Complexity (top 5)
|
|
396
|
-
if (analysis.complexity.length > 0) {
|
|
397
|
-
lines.push('');
|
|
398
|
-
lines.push(' COMPLEXITY (Top 5)');
|
|
399
|
-
lines.push(' ' + '─'.repeat(40));
|
|
400
|
-
const top5 = analysis.complexity.slice(0, 5);
|
|
401
|
-
for (const comp of top5) {
|
|
402
|
-
const bar = '█'.repeat(Math.min(comp.complexity, 20));
|
|
403
|
-
lines.push(` ${comp.componentName.padEnd(20)} ${String(comp.complexity).padStart(3)} ${bar}`);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Dead code
|
|
408
|
-
if (analysis.deadCode.length > 0) {
|
|
409
|
-
lines.push('');
|
|
410
|
-
lines.push(' DEAD CODE');
|
|
411
|
-
lines.push(' ' + '─'.repeat(40));
|
|
412
|
-
for (const dead of analysis.deadCode) {
|
|
413
|
-
lines.push(` ⚠ ${dead.file}`);
|
|
414
|
-
lines.push(` ${dead.message}`);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// File breakdown (verbose)
|
|
419
|
-
if (verbose && analysis.fileBreakdown.length > 0) {
|
|
420
|
-
lines.push('');
|
|
421
|
-
lines.push(' FILE BREAKDOWN');
|
|
422
|
-
lines.push(' ' + '─'.repeat(40));
|
|
423
|
-
for (const file of analysis.fileBreakdown) {
|
|
424
|
-
const size = file.sizeFormatted.padStart(10);
|
|
425
|
-
lines.push(` ${size} ${file.path}`);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Import graph (verbose)
|
|
430
|
-
if (verbose && analysis.importGraph.edges.length > 0) {
|
|
431
|
-
lines.push('');
|
|
432
|
-
lines.push(' IMPORT GRAPH');
|
|
433
|
-
lines.push(' ' + '─'.repeat(40));
|
|
434
|
-
for (const edge of analysis.importGraph.edges.slice(0, 20)) {
|
|
435
|
-
lines.push(` ${edge.from} → ${edge.to}`);
|
|
436
|
-
}
|
|
437
|
-
if (analysis.importGraph.edges.length > 20) {
|
|
438
|
-
lines.push(` ... and ${analysis.importGraph.edges.length - 20} more`);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// State usage (verbose)
|
|
443
|
-
if (verbose && analysis.stateUsage.length > 0) {
|
|
444
|
-
lines.push('');
|
|
445
|
-
lines.push(' STATE VARIABLES');
|
|
446
|
-
lines.push(' ' + '─'.repeat(40));
|
|
447
|
-
for (const state of analysis.stateUsage.slice(0, 10)) {
|
|
448
|
-
const shared = state.isShared ? ' (shared)' : '';
|
|
449
|
-
lines.push(` ${state.name}${shared}`);
|
|
450
|
-
for (const file of state.files) {
|
|
451
|
-
lines.push(` └─ ${file}`);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
lines.push('');
|
|
457
|
-
lines.push('═'.repeat(60));
|
|
458
|
-
|
|
459
|
-
return lines.join('\n');
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Main analyze command handler
|
|
464
|
-
*/
|
|
465
|
-
export async function runAnalyze(args) {
|
|
466
|
-
const { options, patterns } = parseArgs(args);
|
|
467
|
-
const json = options.json || false;
|
|
468
|
-
const verbose = options.verbose || options.v || false;
|
|
469
|
-
|
|
470
|
-
// Use current directory if no patterns specified
|
|
471
|
-
const root = patterns[0] || '.';
|
|
472
|
-
|
|
473
|
-
// Check if src directory exists
|
|
474
|
-
if (!existsSync(join(root, 'src'))) {
|
|
475
|
-
console.error('Error: No src/ directory found.');
|
|
476
|
-
console.log('Run this command from your Pulse project root.');
|
|
477
|
-
process.exit(1);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
console.log('Analyzing bundle...\n');
|
|
481
|
-
|
|
482
|
-
try {
|
|
483
|
-
const analysis = await analyzeBundle(root);
|
|
484
|
-
|
|
485
|
-
if (json) {
|
|
486
|
-
console.log(JSON.stringify(analysis, null, 2));
|
|
487
|
-
} else {
|
|
488
|
-
console.log(formatConsoleOutput(analysis, verbose));
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Exit with error if dead code found
|
|
492
|
-
if (analysis.deadCode.length > 0 && !json) {
|
|
493
|
-
console.log(`\nWarning: ${analysis.deadCode.length} potentially unused file(s) found.`);
|
|
494
|
-
}
|
|
495
|
-
} catch (error) {
|
|
496
|
-
console.error('Analysis failed:', error.message);
|
|
497
|
-
process.exit(1);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Pulse CLI - Analyze Command
|
|
3
|
+
* Analyzes bundle size and dependencies
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, statSync, existsSync } from 'fs';
|
|
7
|
+
import { join, dirname, basename, relative } from 'path';
|
|
8
|
+
import { findPulseFiles, parseArgs, formatBytes, relativePath, resolveImportPath } from './utils/file-utils.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Analyze the bundle/project
|
|
12
|
+
* @param {string} root - Project root directory
|
|
13
|
+
* @returns {object} Analysis results
|
|
14
|
+
*/
|
|
15
|
+
export async function analyzeBundle(root = '.') {
|
|
16
|
+
const { parse } = await import('../compiler/index.js');
|
|
17
|
+
|
|
18
|
+
// Find all source files
|
|
19
|
+
const pulseFiles = findPulseFiles([join(root, 'src')], { extensions: ['.pulse'] });
|
|
20
|
+
const jsFiles = findPulseFiles([join(root, 'src')], { extensions: ['.js'] });
|
|
21
|
+
const allFiles = [...pulseFiles, ...jsFiles];
|
|
22
|
+
|
|
23
|
+
// Calculate summary
|
|
24
|
+
const summary = calculateSummary(allFiles, pulseFiles, jsFiles);
|
|
25
|
+
|
|
26
|
+
// Analyze each file
|
|
27
|
+
const fileBreakdown = await analyzeFiles(allFiles, parse);
|
|
28
|
+
|
|
29
|
+
// Build import graph
|
|
30
|
+
const importGraph = await buildImportGraph(allFiles, parse);
|
|
31
|
+
|
|
32
|
+
// Calculate complexity metrics
|
|
33
|
+
const complexity = await calculateComplexity(pulseFiles, parse);
|
|
34
|
+
|
|
35
|
+
// Detect dead code
|
|
36
|
+
const deadCode = detectDeadCode(allFiles, importGraph, root);
|
|
37
|
+
|
|
38
|
+
// Analyze state usage
|
|
39
|
+
const stateUsage = await analyzeStateUsage(pulseFiles, parse);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
summary,
|
|
43
|
+
fileBreakdown,
|
|
44
|
+
importGraph,
|
|
45
|
+
complexity,
|
|
46
|
+
deadCode,
|
|
47
|
+
stateUsage
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Calculate summary statistics
|
|
53
|
+
*/
|
|
54
|
+
function calculateSummary(allFiles, pulseFiles, jsFiles) {
|
|
55
|
+
let totalSize = 0;
|
|
56
|
+
|
|
57
|
+
for (const file of allFiles) {
|
|
58
|
+
try {
|
|
59
|
+
const stats = statSync(file);
|
|
60
|
+
totalSize += stats.size;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
// Skip inaccessible files
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
totalFiles: allFiles.length,
|
|
68
|
+
pulseFiles: pulseFiles.length,
|
|
69
|
+
jsFiles: jsFiles.length,
|
|
70
|
+
totalSize,
|
|
71
|
+
totalSizeFormatted: formatBytes(totalSize)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Analyze individual files
|
|
77
|
+
*/
|
|
78
|
+
async function analyzeFiles(files, parse) {
|
|
79
|
+
const results = [];
|
|
80
|
+
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
try {
|
|
83
|
+
const stats = statSync(file);
|
|
84
|
+
const source = readFileSync(file, 'utf-8');
|
|
85
|
+
|
|
86
|
+
const info = {
|
|
87
|
+
path: relativePath(file),
|
|
88
|
+
size: stats.size,
|
|
89
|
+
sizeFormatted: formatBytes(stats.size),
|
|
90
|
+
lines: source.split('\n').length
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Additional analysis for .pulse files
|
|
94
|
+
if (file.endsWith('.pulse')) {
|
|
95
|
+
try {
|
|
96
|
+
const ast = parse(source);
|
|
97
|
+
info.type = 'pulse';
|
|
98
|
+
info.componentName = ast.page?.name || basename(file, '.pulse');
|
|
99
|
+
info.stateCount = ast.state?.properties?.length || 0;
|
|
100
|
+
info.actionCount = ast.actions?.functions?.length || 0;
|
|
101
|
+
info.importCount = ast.imports?.length || 0;
|
|
102
|
+
info.hasStyles = !!ast.style;
|
|
103
|
+
} catch (e) {
|
|
104
|
+
info.type = 'pulse';
|
|
105
|
+
info.parseError = e.message;
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
info.type = 'js';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
results.push(info);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
// Skip inaccessible files
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return results.sort((a, b) => b.size - a.size);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Build import dependency graph
|
|
122
|
+
*/
|
|
123
|
+
export async function buildImportGraph(files, parse) {
|
|
124
|
+
const nodes = new Set();
|
|
125
|
+
const edges = [];
|
|
126
|
+
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const relPath = relativePath(file);
|
|
129
|
+
nodes.add(relPath);
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const source = readFileSync(file, 'utf-8');
|
|
133
|
+
|
|
134
|
+
if (file.endsWith('.pulse')) {
|
|
135
|
+
const ast = parse(source);
|
|
136
|
+
for (const imp of ast.imports || []) {
|
|
137
|
+
const resolved = resolveImportPath(file, imp.source);
|
|
138
|
+
if (resolved) {
|
|
139
|
+
const resolvedRel = relativePath(resolved);
|
|
140
|
+
nodes.add(resolvedRel);
|
|
141
|
+
edges.push({ from: relPath, to: resolvedRel });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} else if (file.endsWith('.js')) {
|
|
145
|
+
// Parse JS imports with regex
|
|
146
|
+
const importRegex = /import\s+(?:.*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
147
|
+
let match;
|
|
148
|
+
while ((match = importRegex.exec(source)) !== null) {
|
|
149
|
+
const resolved = resolveImportPath(file, match[1]);
|
|
150
|
+
if (resolved) {
|
|
151
|
+
const resolvedRel = relativePath(resolved);
|
|
152
|
+
nodes.add(resolvedRel);
|
|
153
|
+
edges.push({ from: relPath, to: resolvedRel });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (e) {
|
|
158
|
+
// Skip files with errors
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
nodes: Array.from(nodes).sort(),
|
|
164
|
+
edges
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Calculate component complexity metrics
|
|
170
|
+
*/
|
|
171
|
+
async function calculateComplexity(pulseFiles, parse) {
|
|
172
|
+
const results = [];
|
|
173
|
+
|
|
174
|
+
for (const file of pulseFiles) {
|
|
175
|
+
try {
|
|
176
|
+
const source = readFileSync(file, 'utf-8');
|
|
177
|
+
const ast = parse(source);
|
|
178
|
+
|
|
179
|
+
const metrics = {
|
|
180
|
+
file: relativePath(file),
|
|
181
|
+
componentName: ast.page?.name || basename(file, '.pulse'),
|
|
182
|
+
stateCount: ast.state?.properties?.length || 0,
|
|
183
|
+
actionCount: ast.actions?.functions?.length || 0,
|
|
184
|
+
viewDepth: calculateViewDepth(ast.view),
|
|
185
|
+
directiveCount: countDirectives(ast.view),
|
|
186
|
+
styleRuleCount: countStyleRules(ast.style),
|
|
187
|
+
importCount: ast.imports?.length || 0
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Calculate weighted complexity score
|
|
191
|
+
metrics.complexity = Math.round(
|
|
192
|
+
metrics.stateCount * 1 +
|
|
193
|
+
metrics.actionCount * 2 +
|
|
194
|
+
metrics.viewDepth * 1.5 +
|
|
195
|
+
metrics.directiveCount * 1 +
|
|
196
|
+
metrics.styleRuleCount * 0.5 +
|
|
197
|
+
metrics.importCount * 0.5
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
results.push(metrics);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
// Skip files with parse errors
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return results.sort((a, b) => b.complexity - a.complexity);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Calculate maximum view depth
|
|
211
|
+
*/
|
|
212
|
+
function calculateViewDepth(view, depth = 0) {
|
|
213
|
+
if (!view || !view.children) return depth;
|
|
214
|
+
|
|
215
|
+
let maxDepth = depth;
|
|
216
|
+
for (const child of view.children) {
|
|
217
|
+
if (child.type === 'Element' && child.children) {
|
|
218
|
+
maxDepth = Math.max(maxDepth, calculateViewDepth(child, depth + 1));
|
|
219
|
+
}
|
|
220
|
+
if (child.type === 'IfDirective') {
|
|
221
|
+
if (child.consequent) {
|
|
222
|
+
maxDepth = Math.max(maxDepth, calculateViewDepth(child.consequent, depth + 1));
|
|
223
|
+
}
|
|
224
|
+
if (child.alternate) {
|
|
225
|
+
maxDepth = Math.max(maxDepth, calculateViewDepth(child.alternate, depth + 1));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (child.type === 'EachDirective' && child.body) {
|
|
229
|
+
maxDepth = Math.max(maxDepth, calculateViewDepth(child.body, depth + 1));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return maxDepth;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Count directives in view
|
|
237
|
+
*/
|
|
238
|
+
function countDirectives(view, count = 0) {
|
|
239
|
+
if (!view || !view.children) return count;
|
|
240
|
+
|
|
241
|
+
for (const child of view.children) {
|
|
242
|
+
if (child.type === 'IfDirective') count++;
|
|
243
|
+
if (child.type === 'EachDirective') count++;
|
|
244
|
+
if (child.directives) count += child.directives.length;
|
|
245
|
+
|
|
246
|
+
if (child.children) {
|
|
247
|
+
count = countDirectives(child, count);
|
|
248
|
+
}
|
|
249
|
+
if (child.consequent) {
|
|
250
|
+
count = countDirectives(child.consequent, count);
|
|
251
|
+
}
|
|
252
|
+
if (child.alternate) {
|
|
253
|
+
count = countDirectives(child.alternate, count);
|
|
254
|
+
}
|
|
255
|
+
if (child.body) {
|
|
256
|
+
count = countDirectives(child.body, count);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return count;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Count style rules
|
|
265
|
+
*/
|
|
266
|
+
function countStyleRules(style) {
|
|
267
|
+
if (!style || !style.rules) return 0;
|
|
268
|
+
|
|
269
|
+
let count = 0;
|
|
270
|
+
function countRules(rules) {
|
|
271
|
+
for (const rule of rules) {
|
|
272
|
+
count++;
|
|
273
|
+
if (rule.rules) {
|
|
274
|
+
countRules(rule.rules);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
countRules(style.rules);
|
|
279
|
+
return count;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Detect dead code (unreachable files)
|
|
284
|
+
*/
|
|
285
|
+
function detectDeadCode(allFiles, importGraph, root) {
|
|
286
|
+
const deadCode = [];
|
|
287
|
+
|
|
288
|
+
// Find entry points (main.js, index.js, App.pulse)
|
|
289
|
+
const entryPoints = new Set();
|
|
290
|
+
for (const file of allFiles) {
|
|
291
|
+
const name = basename(file);
|
|
292
|
+
if (name === 'main.js' || name === 'index.js' || name === 'App.pulse' || name === 'app.js') {
|
|
293
|
+
entryPoints.add(relativePath(file));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Build reachability set
|
|
298
|
+
const reachable = new Set();
|
|
299
|
+
const queue = Array.from(entryPoints);
|
|
300
|
+
|
|
301
|
+
while (queue.length > 0) {
|
|
302
|
+
const current = queue.shift();
|
|
303
|
+
if (reachable.has(current)) continue;
|
|
304
|
+
reachable.add(current);
|
|
305
|
+
|
|
306
|
+
// Find all edges from this node
|
|
307
|
+
for (const edge of importGraph.edges) {
|
|
308
|
+
if (edge.from === current && !reachable.has(edge.to)) {
|
|
309
|
+
queue.push(edge.to);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Find unreachable files
|
|
315
|
+
for (const file of allFiles) {
|
|
316
|
+
const relPath = relativePath(file);
|
|
317
|
+
|
|
318
|
+
// Skip entry points and non-source files
|
|
319
|
+
if (entryPoints.has(relPath)) continue;
|
|
320
|
+
|
|
321
|
+
if (!reachable.has(relPath)) {
|
|
322
|
+
deadCode.push({
|
|
323
|
+
file: relPath,
|
|
324
|
+
reason: 'unreachable',
|
|
325
|
+
message: 'File is not imported from any entry point'
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return deadCode;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Analyze state variable usage across files
|
|
335
|
+
*/
|
|
336
|
+
async function analyzeStateUsage(pulseFiles, parse) {
|
|
337
|
+
const stateVars = new Map(); // name -> { declarations: [], references: [] }
|
|
338
|
+
|
|
339
|
+
for (const file of pulseFiles) {
|
|
340
|
+
try {
|
|
341
|
+
const source = readFileSync(file, 'utf-8');
|
|
342
|
+
const ast = parse(source);
|
|
343
|
+
const relPath = relativePath(file);
|
|
344
|
+
|
|
345
|
+
// Collect state declarations
|
|
346
|
+
if (ast.state && ast.state.properties) {
|
|
347
|
+
for (const prop of ast.state.properties) {
|
|
348
|
+
if (!stateVars.has(prop.name)) {
|
|
349
|
+
stateVars.set(prop.name, { declarations: [], files: new Set() });
|
|
350
|
+
}
|
|
351
|
+
stateVars.get(prop.name).declarations.push({
|
|
352
|
+
file: relPath,
|
|
353
|
+
line: prop.line || 1
|
|
354
|
+
});
|
|
355
|
+
stateVars.get(prop.name).files.add(relPath);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} catch (e) {
|
|
359
|
+
// Skip files with parse errors
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Convert to array and add statistics
|
|
364
|
+
return Array.from(stateVars.entries())
|
|
365
|
+
.map(([name, info]) => ({
|
|
366
|
+
name,
|
|
367
|
+
declarationCount: info.declarations.length,
|
|
368
|
+
files: Array.from(info.files),
|
|
369
|
+
isShared: info.files.size > 1
|
|
370
|
+
}))
|
|
371
|
+
.sort((a, b) => b.declarationCount - a.declarationCount);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Format analysis results for console output
|
|
376
|
+
*/
|
|
377
|
+
function formatConsoleOutput(analysis, verbose = false) {
|
|
378
|
+
const lines = [];
|
|
379
|
+
|
|
380
|
+
// Header
|
|
381
|
+
lines.push('');
|
|
382
|
+
lines.push('═'.repeat(60));
|
|
383
|
+
lines.push(' PULSE BUNDLE ANALYSIS');
|
|
384
|
+
lines.push('═'.repeat(60));
|
|
385
|
+
|
|
386
|
+
// Summary
|
|
387
|
+
lines.push('');
|
|
388
|
+
lines.push(' SUMMARY');
|
|
389
|
+
lines.push(' ' + '─'.repeat(40));
|
|
390
|
+
lines.push(` Total files: ${analysis.summary.totalFiles}`);
|
|
391
|
+
lines.push(` .pulse files: ${analysis.summary.pulseFiles}`);
|
|
392
|
+
lines.push(` .js files: ${analysis.summary.jsFiles}`);
|
|
393
|
+
lines.push(` Total size: ${analysis.summary.totalSizeFormatted}`);
|
|
394
|
+
|
|
395
|
+
// Complexity (top 5)
|
|
396
|
+
if (analysis.complexity.length > 0) {
|
|
397
|
+
lines.push('');
|
|
398
|
+
lines.push(' COMPLEXITY (Top 5)');
|
|
399
|
+
lines.push(' ' + '─'.repeat(40));
|
|
400
|
+
const top5 = analysis.complexity.slice(0, 5);
|
|
401
|
+
for (const comp of top5) {
|
|
402
|
+
const bar = '█'.repeat(Math.min(comp.complexity, 20));
|
|
403
|
+
lines.push(` ${comp.componentName.padEnd(20)} ${String(comp.complexity).padStart(3)} ${bar}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Dead code
|
|
408
|
+
if (analysis.deadCode.length > 0) {
|
|
409
|
+
lines.push('');
|
|
410
|
+
lines.push(' DEAD CODE');
|
|
411
|
+
lines.push(' ' + '─'.repeat(40));
|
|
412
|
+
for (const dead of analysis.deadCode) {
|
|
413
|
+
lines.push(` ⚠ ${dead.file}`);
|
|
414
|
+
lines.push(` ${dead.message}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// File breakdown (verbose)
|
|
419
|
+
if (verbose && analysis.fileBreakdown.length > 0) {
|
|
420
|
+
lines.push('');
|
|
421
|
+
lines.push(' FILE BREAKDOWN');
|
|
422
|
+
lines.push(' ' + '─'.repeat(40));
|
|
423
|
+
for (const file of analysis.fileBreakdown) {
|
|
424
|
+
const size = file.sizeFormatted.padStart(10);
|
|
425
|
+
lines.push(` ${size} ${file.path}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Import graph (verbose)
|
|
430
|
+
if (verbose && analysis.importGraph.edges.length > 0) {
|
|
431
|
+
lines.push('');
|
|
432
|
+
lines.push(' IMPORT GRAPH');
|
|
433
|
+
lines.push(' ' + '─'.repeat(40));
|
|
434
|
+
for (const edge of analysis.importGraph.edges.slice(0, 20)) {
|
|
435
|
+
lines.push(` ${edge.from} → ${edge.to}`);
|
|
436
|
+
}
|
|
437
|
+
if (analysis.importGraph.edges.length > 20) {
|
|
438
|
+
lines.push(` ... and ${analysis.importGraph.edges.length - 20} more`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// State usage (verbose)
|
|
443
|
+
if (verbose && analysis.stateUsage.length > 0) {
|
|
444
|
+
lines.push('');
|
|
445
|
+
lines.push(' STATE VARIABLES');
|
|
446
|
+
lines.push(' ' + '─'.repeat(40));
|
|
447
|
+
for (const state of analysis.stateUsage.slice(0, 10)) {
|
|
448
|
+
const shared = state.isShared ? ' (shared)' : '';
|
|
449
|
+
lines.push(` ${state.name}${shared}`);
|
|
450
|
+
for (const file of state.files) {
|
|
451
|
+
lines.push(` └─ ${file}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
lines.push('');
|
|
457
|
+
lines.push('═'.repeat(60));
|
|
458
|
+
|
|
459
|
+
return lines.join('\n');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Main analyze command handler
|
|
464
|
+
*/
|
|
465
|
+
export async function runAnalyze(args) {
|
|
466
|
+
const { options, patterns } = parseArgs(args);
|
|
467
|
+
const json = options.json || false;
|
|
468
|
+
const verbose = options.verbose || options.v || false;
|
|
469
|
+
|
|
470
|
+
// Use current directory if no patterns specified
|
|
471
|
+
const root = patterns[0] || '.';
|
|
472
|
+
|
|
473
|
+
// Check if src directory exists
|
|
474
|
+
if (!existsSync(join(root, 'src'))) {
|
|
475
|
+
console.error('Error: No src/ directory found.');
|
|
476
|
+
console.log('Run this command from your Pulse project root.');
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
console.log('Analyzing bundle...\n');
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
const analysis = await analyzeBundle(root);
|
|
484
|
+
|
|
485
|
+
if (json) {
|
|
486
|
+
console.log(JSON.stringify(analysis, null, 2));
|
|
487
|
+
} else {
|
|
488
|
+
console.log(formatConsoleOutput(analysis, verbose));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Exit with error if dead code found
|
|
492
|
+
if (analysis.deadCode.length > 0 && !json) {
|
|
493
|
+
console.log(`\nWarning: ${analysis.deadCode.length} potentially unused file(s) found.`);
|
|
494
|
+
}
|
|
495
|
+
} catch (error) {
|
|
496
|
+
console.error('Analysis failed:', error.message);
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
}
|