pulse-js-framework 1.4.0 → 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/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
+ }