pulse-js-framework 1.7.4 → 1.7.5
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 +127 -46
- package/cli/build.js +51 -13
- package/cli/format.js +64 -8
- package/cli/lint.js +112 -27
- package/cli/utils/cli-ui.js +452 -0
- package/compiler/parser.js +19 -2
- package/core/errors.js +281 -6
- package/package.json +7 -2
- package/runtime/async.js +282 -14
- package/runtime/dom-adapter.js +920 -0
- package/runtime/dom.js +331 -162
- package/runtime/logger.js +144 -69
- package/runtime/logger.prod.js +43 -18
- package/runtime/pulse.js +202 -80
- package/runtime/router.js +27 -39
- package/runtime/store.js +10 -7
- package/runtime/utils.js +279 -18
package/cli/analyze.js
CHANGED
|
@@ -7,6 +7,7 @@ import { readFileSync, statSync, existsSync } from 'fs';
|
|
|
7
7
|
import { join, dirname, basename, relative } from 'path';
|
|
8
8
|
import { findPulseFiles, parseArgs, formatBytes, relativePath, resolveImportPath } from './utils/file-utils.js';
|
|
9
9
|
import { log } from './logger.js';
|
|
10
|
+
import { createTimer, formatDuration, createSpinner, createBarChart, createTree, createTable, printSection } from './utils/cli-ui.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Analyze the bundle/project
|
|
@@ -384,74 +385,111 @@ function formatConsoleOutput(analysis, verbose = false) {
|
|
|
384
385
|
lines.push(' PULSE BUNDLE ANALYSIS');
|
|
385
386
|
lines.push('═'.repeat(60));
|
|
386
387
|
|
|
387
|
-
// Summary
|
|
388
|
+
// Summary table
|
|
388
389
|
lines.push('');
|
|
389
|
-
lines.push(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
390
|
+
lines.push(createTable(
|
|
391
|
+
['Metric', 'Value'],
|
|
392
|
+
[
|
|
393
|
+
['Total files', String(analysis.summary.totalFiles)],
|
|
394
|
+
['.pulse files', String(analysis.summary.pulseFiles)],
|
|
395
|
+
['.js files', String(analysis.summary.jsFiles)],
|
|
396
|
+
['Total size', analysis.summary.totalSizeFormatted]
|
|
397
|
+
],
|
|
398
|
+
{ align: ['left', 'right'] }
|
|
399
|
+
));
|
|
400
|
+
|
|
401
|
+
// Complexity bar chart (top 5)
|
|
397
402
|
if (analysis.complexity.length > 0) {
|
|
398
|
-
|
|
399
|
-
lines.push(' COMPLEXITY (Top 5)');
|
|
400
|
-
lines.push(' ' + '─'.repeat(40));
|
|
403
|
+
printSection('COMPONENT COMPLEXITY');
|
|
401
404
|
const top5 = analysis.complexity.slice(0, 5);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
+
const chartData = top5.map(comp => ({
|
|
406
|
+
label: comp.componentName.slice(0, 15).padEnd(15),
|
|
407
|
+
value: comp.complexity,
|
|
408
|
+
color: comp.complexity > 20 ? 'red' : comp.complexity > 10 ? 'yellow' : 'green'
|
|
409
|
+
}));
|
|
410
|
+
lines.push(createBarChart(chartData, { maxWidth: 30, showValues: true }));
|
|
411
|
+
|
|
412
|
+
// Detailed metrics table for verbose mode
|
|
413
|
+
if (verbose) {
|
|
414
|
+
lines.push('');
|
|
415
|
+
lines.push(createTable(
|
|
416
|
+
['Component', 'State', 'Actions', 'Depth', 'Directives', 'Score'],
|
|
417
|
+
top5.map(c => [
|
|
418
|
+
c.componentName,
|
|
419
|
+
String(c.stateCount),
|
|
420
|
+
String(c.actionCount),
|
|
421
|
+
String(c.viewDepth),
|
|
422
|
+
String(c.directiveCount),
|
|
423
|
+
String(c.complexity)
|
|
424
|
+
]),
|
|
425
|
+
{ align: ['left', 'right', 'right', 'right', 'right', 'right'] }
|
|
426
|
+
));
|
|
405
427
|
}
|
|
406
428
|
}
|
|
407
429
|
|
|
408
|
-
// Dead code
|
|
430
|
+
// Dead code warnings
|
|
409
431
|
if (analysis.deadCode.length > 0) {
|
|
410
|
-
|
|
411
|
-
lines.push(' DEAD CODE');
|
|
412
|
-
lines.push(' ' + '─'.repeat(40));
|
|
432
|
+
printSection('DEAD CODE DETECTED');
|
|
413
433
|
for (const dead of analysis.deadCode) {
|
|
414
434
|
lines.push(` ⚠ ${dead.file}`);
|
|
415
|
-
lines.push(` ${dead.message}`);
|
|
435
|
+
lines.push(` └─ ${dead.message}`);
|
|
416
436
|
}
|
|
417
437
|
}
|
|
418
438
|
|
|
419
|
-
// File breakdown (verbose)
|
|
439
|
+
// File size breakdown with bar chart (verbose)
|
|
420
440
|
if (verbose && analysis.fileBreakdown.length > 0) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
441
|
+
printSection('FILE SIZE BREAKDOWN');
|
|
442
|
+
const top10Files = analysis.fileBreakdown.slice(0, 10);
|
|
443
|
+
const sizeChartData = top10Files.map(file => ({
|
|
444
|
+
label: basename(file.path).slice(0, 20).padEnd(20),
|
|
445
|
+
value: file.size,
|
|
446
|
+
color: file.type === 'pulse' ? 'cyan' : 'blue'
|
|
447
|
+
}));
|
|
448
|
+
lines.push(createBarChart(sizeChartData, { maxWidth: 25, showValues: true, unit: 'B' }));
|
|
428
449
|
}
|
|
429
450
|
|
|
430
|
-
// Import graph (verbose)
|
|
451
|
+
// Import graph as tree (verbose)
|
|
431
452
|
if (verbose && analysis.importGraph.edges.length > 0) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (analysis.importGraph.edges.length > 20) {
|
|
439
|
-
lines.push(` ... and ${analysis.importGraph.edges.length - 20} more`);
|
|
453
|
+
printSection('IMPORT DEPENDENCY TREE');
|
|
454
|
+
|
|
455
|
+
// Build tree structure from edges
|
|
456
|
+
const tree = buildDependencyTree(analysis.importGraph);
|
|
457
|
+
if (tree) {
|
|
458
|
+
lines.push(createTree(tree));
|
|
440
459
|
}
|
|
460
|
+
|
|
461
|
+
// Show edge count summary
|
|
462
|
+
lines.push(` Total dependencies: ${analysis.importGraph.edges.length}`);
|
|
441
463
|
}
|
|
442
464
|
|
|
443
465
|
// State usage (verbose)
|
|
444
466
|
if (verbose && analysis.stateUsage.length > 0) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
467
|
+
printSection('STATE VARIABLES');
|
|
468
|
+
|
|
469
|
+
// Show shared state as a table
|
|
470
|
+
const sharedState = analysis.stateUsage.filter(s => s.isShared);
|
|
471
|
+
if (sharedState.length > 0) {
|
|
472
|
+
lines.push(' Shared state (used in multiple files):');
|
|
473
|
+
lines.push(createTable(
|
|
474
|
+
['Variable', 'Files'],
|
|
475
|
+
sharedState.slice(0, 5).map(s => [s.name, String(s.files.length)]),
|
|
476
|
+
{ align: ['left', 'right'] }
|
|
477
|
+
));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// State tree visualization
|
|
481
|
+
for (const state of analysis.stateUsage.slice(0, 5)) {
|
|
482
|
+
const shared = state.isShared ? ' \x1b[33m(shared)\x1b[0m' : '';
|
|
450
483
|
lines.push(` ${state.name}${shared}`);
|
|
451
|
-
for (
|
|
452
|
-
|
|
484
|
+
for (let i = 0; i < state.files.length; i++) {
|
|
485
|
+
const isLast = i === state.files.length - 1;
|
|
486
|
+
const prefix = isLast ? '└── ' : '├── ';
|
|
487
|
+
lines.push(` ${prefix}${state.files[i]}`);
|
|
453
488
|
}
|
|
454
489
|
}
|
|
490
|
+
if (analysis.stateUsage.length > 5) {
|
|
491
|
+
lines.push(` ... and ${analysis.stateUsage.length - 5} more state variables`);
|
|
492
|
+
}
|
|
455
493
|
}
|
|
456
494
|
|
|
457
495
|
lines.push('');
|
|
@@ -460,6 +498,44 @@ function formatConsoleOutput(analysis, verbose = false) {
|
|
|
460
498
|
return lines.join('\n');
|
|
461
499
|
}
|
|
462
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Build a dependency tree structure for visualization
|
|
503
|
+
*/
|
|
504
|
+
function buildDependencyTree(importGraph) {
|
|
505
|
+
if (!importGraph.edges.length) return null;
|
|
506
|
+
|
|
507
|
+
// Find entry points (files that aren't imported by others)
|
|
508
|
+
const imported = new Set(importGraph.edges.map(e => e.to));
|
|
509
|
+
const entryPoints = importGraph.nodes.filter(n =>
|
|
510
|
+
!imported.has(n) && (n.includes('main') || n.includes('index') || n.includes('App'))
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
if (entryPoints.length === 0 && importGraph.nodes.length > 0) {
|
|
514
|
+
entryPoints.push(importGraph.nodes[0]);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Build tree recursively (limit depth to avoid infinite loops)
|
|
518
|
+
function buildNode(name, visited = new Set(), depth = 0) {
|
|
519
|
+
if (depth > 5 || visited.has(name)) {
|
|
520
|
+
return { name: basename(name) + (visited.has(name) ? ' (circular)' : ' ...'), children: [] };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
visited.add(name);
|
|
524
|
+
const children = importGraph.edges
|
|
525
|
+
.filter(e => e.from === name)
|
|
526
|
+
.slice(0, 5) // Limit children
|
|
527
|
+
.map(e => buildNode(e.to, new Set(visited), depth + 1));
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
name: basename(name),
|
|
531
|
+
children
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Return first entry point's tree
|
|
536
|
+
return buildNode(entryPoints[0]);
|
|
537
|
+
}
|
|
538
|
+
|
|
463
539
|
/**
|
|
464
540
|
* Main analyze command handler
|
|
465
541
|
*/
|
|
@@ -478,10 +554,14 @@ export async function runAnalyze(args) {
|
|
|
478
554
|
process.exit(1);
|
|
479
555
|
}
|
|
480
556
|
|
|
481
|
-
|
|
557
|
+
const timer = createTimer();
|
|
558
|
+
const spinner = createSpinner('Analyzing bundle...');
|
|
482
559
|
|
|
483
560
|
try {
|
|
484
561
|
const analysis = await analyzeBundle(root);
|
|
562
|
+
const elapsed = timer.elapsed();
|
|
563
|
+
|
|
564
|
+
spinner.success(`Analysis complete (${formatDuration(elapsed)})`);
|
|
485
565
|
|
|
486
566
|
if (json) {
|
|
487
567
|
log.info(JSON.stringify(analysis, null, 2));
|
|
@@ -494,7 +574,8 @@ export async function runAnalyze(args) {
|
|
|
494
574
|
log.warn(`\nWarning: ${analysis.deadCode.length} potentially unused file(s) found.`);
|
|
495
575
|
}
|
|
496
576
|
} catch (error) {
|
|
497
|
-
|
|
577
|
+
spinner.fail('Analysis failed');
|
|
578
|
+
log.error(error.message);
|
|
498
579
|
process.exit(1);
|
|
499
580
|
}
|
|
500
581
|
}
|
package/cli/build.js
CHANGED
|
@@ -8,6 +8,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSy
|
|
|
8
8
|
import { join, extname, relative, dirname } from 'path';
|
|
9
9
|
import { compile } from '../compiler/index.js';
|
|
10
10
|
import { log } from './logger.js';
|
|
11
|
+
import { createTimer, createProgressBar, formatDuration, createSpinner } from './utils/cli-ui.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Build project for production
|
|
@@ -15,21 +16,23 @@ import { log } from './logger.js';
|
|
|
15
16
|
export async function buildProject(args) {
|
|
16
17
|
const root = process.cwd();
|
|
17
18
|
const outDir = join(root, 'dist');
|
|
19
|
+
const timer = createTimer();
|
|
18
20
|
|
|
19
21
|
// Check if vite is available
|
|
20
22
|
try {
|
|
21
23
|
const viteConfig = join(root, 'vite.config.js');
|
|
22
24
|
if (existsSync(viteConfig)) {
|
|
23
|
-
|
|
25
|
+
const spinner = createSpinner('Building with Vite...');
|
|
24
26
|
const { build } = await import('vite');
|
|
25
27
|
await build({ root });
|
|
28
|
+
spinner.success(`Built with Vite in ${timer.format()}`);
|
|
26
29
|
return;
|
|
27
30
|
}
|
|
28
31
|
} catch (e) {
|
|
29
32
|
// Vite not available, use built-in build
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
log.info('Building with Pulse compiler
|
|
35
|
+
log.info('Building with Pulse compiler...\n');
|
|
33
36
|
|
|
34
37
|
// Create output directory
|
|
35
38
|
if (!existsSync(outDir)) {
|
|
@@ -42,10 +45,19 @@ export async function buildProject(args) {
|
|
|
42
45
|
copyDir(publicDir, outDir);
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
//
|
|
48
|
+
// Count files for progress bar
|
|
46
49
|
const srcDir = join(root, 'src');
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
const fileCount = existsSync(srcDir) ? countFiles(srcDir) : 0;
|
|
51
|
+
|
|
52
|
+
// Process source files with progress bar
|
|
53
|
+
if (existsSync(srcDir) && fileCount > 0) {
|
|
54
|
+
const progress = createProgressBar({
|
|
55
|
+
total: fileCount,
|
|
56
|
+
label: 'Compiling',
|
|
57
|
+
width: 25
|
|
58
|
+
});
|
|
59
|
+
processDirectory(srcDir, join(outDir, 'assets'), progress);
|
|
60
|
+
progress.done();
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
// Copy and process index.html
|
|
@@ -68,20 +80,45 @@ export async function buildProject(args) {
|
|
|
68
80
|
// Bundle runtime
|
|
69
81
|
bundleRuntime(outDir);
|
|
70
82
|
|
|
83
|
+
const elapsed = timer.elapsed();
|
|
71
84
|
log.success(`
|
|
72
|
-
Build complete
|
|
85
|
+
✓ Build complete in ${formatDuration(elapsed)}
|
|
73
86
|
|
|
74
|
-
Output
|
|
87
|
+
Output: ${relative(root, outDir)}/
|
|
88
|
+
Files: ${fileCount} processed
|
|
75
89
|
|
|
76
|
-
To preview
|
|
77
|
-
|
|
90
|
+
To preview:
|
|
91
|
+
pulse preview
|
|
78
92
|
`);
|
|
79
93
|
}
|
|
80
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Count files in a directory recursively
|
|
97
|
+
*/
|
|
98
|
+
function countFiles(dir) {
|
|
99
|
+
let count = 0;
|
|
100
|
+
const files = readdirSync(dir);
|
|
101
|
+
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
const fullPath = join(dir, file);
|
|
104
|
+
const stat = statSync(fullPath);
|
|
105
|
+
if (stat.isDirectory()) {
|
|
106
|
+
count += countFiles(fullPath);
|
|
107
|
+
} else {
|
|
108
|
+
count++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return count;
|
|
113
|
+
}
|
|
114
|
+
|
|
81
115
|
/**
|
|
82
116
|
* Process a directory of source files
|
|
117
|
+
* @param {string} srcDir - Source directory
|
|
118
|
+
* @param {string} outDir - Output directory
|
|
119
|
+
* @param {Object} [progress] - Progress bar instance
|
|
83
120
|
*/
|
|
84
|
-
function processDirectory(srcDir, outDir) {
|
|
121
|
+
function processDirectory(srcDir, outDir, progress = null) {
|
|
85
122
|
if (!existsSync(outDir)) {
|
|
86
123
|
mkdirSync(outDir, { recursive: true });
|
|
87
124
|
}
|
|
@@ -93,7 +130,7 @@ function processDirectory(srcDir, outDir) {
|
|
|
93
130
|
const stat = statSync(srcPath);
|
|
94
131
|
|
|
95
132
|
if (stat.isDirectory()) {
|
|
96
|
-
processDirectory(srcPath, join(outDir, file));
|
|
133
|
+
processDirectory(srcPath, join(outDir, file), progress);
|
|
97
134
|
} else if (file.endsWith('.pulse')) {
|
|
98
135
|
// Compile .pulse files
|
|
99
136
|
const source = readFileSync(srcPath, 'utf-8');
|
|
@@ -105,13 +142,13 @@ function processDirectory(srcDir, outDir) {
|
|
|
105
142
|
if (result.success) {
|
|
106
143
|
const outPath = join(outDir, file.replace('.pulse', '.js'));
|
|
107
144
|
writeFileSync(outPath, result.code);
|
|
108
|
-
log.info(` Compiled: ${file}`);
|
|
109
145
|
} else {
|
|
110
146
|
log.error(` Error compiling ${file}:`);
|
|
111
147
|
for (const error of result.errors) {
|
|
112
148
|
log.error(` ${error.message}`);
|
|
113
149
|
}
|
|
114
150
|
}
|
|
151
|
+
if (progress) progress.tick();
|
|
115
152
|
} else if (file.endsWith('.js') || file.endsWith('.mjs')) {
|
|
116
153
|
// Process JS files - rewrite imports
|
|
117
154
|
let content = readFileSync(srcPath, 'utf-8');
|
|
@@ -130,11 +167,12 @@ function processDirectory(srcDir, outDir) {
|
|
|
130
167
|
|
|
131
168
|
const outPath = join(outDir, file);
|
|
132
169
|
writeFileSync(outPath, content);
|
|
133
|
-
|
|
170
|
+
if (progress) progress.tick();
|
|
134
171
|
} else {
|
|
135
172
|
// Copy other files
|
|
136
173
|
const outPath = join(outDir, file);
|
|
137
174
|
copyFileSync(srcPath, outPath);
|
|
175
|
+
if (progress) progress.tick();
|
|
138
176
|
}
|
|
139
177
|
}
|
|
140
178
|
}
|
package/cli/format.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* Formats .pulse files consistently
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
6
|
+
import { readFileSync, writeFileSync, watch } from 'fs';
|
|
7
|
+
import { dirname } from 'path';
|
|
7
8
|
import { findPulseFiles, parseArgs, relativePath } from './utils/file-utils.js';
|
|
8
9
|
import { log } from './logger.js';
|
|
10
|
+
import { createTimer, formatDuration } from './utils/cli-ui.js';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Default format options
|
|
@@ -641,6 +643,7 @@ export async function runFormat(args) {
|
|
|
641
643
|
const { options, patterns } = parseArgs(args);
|
|
642
644
|
const check = options.check || false;
|
|
643
645
|
const write = !check; // Default to write unless --check is specified
|
|
646
|
+
const watchMode = options.watch || options.w || false;
|
|
644
647
|
|
|
645
648
|
// Find files to format
|
|
646
649
|
const files = findPulseFiles(patterns);
|
|
@@ -650,7 +653,57 @@ export async function runFormat(args) {
|
|
|
650
653
|
return;
|
|
651
654
|
}
|
|
652
655
|
|
|
653
|
-
|
|
656
|
+
// Run initial format
|
|
657
|
+
const result = await runFormatOnFiles(files, { check, write, options });
|
|
658
|
+
|
|
659
|
+
// If watch mode, set up file watchers
|
|
660
|
+
if (watchMode) {
|
|
661
|
+
if (check) {
|
|
662
|
+
log.warn('--watch mode is not available with --check');
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
log.info('\nWatching for changes... (Ctrl+C to stop)\n');
|
|
667
|
+
|
|
668
|
+
// Get unique directories to watch
|
|
669
|
+
const watchedDirs = new Set(files.map(f => dirname(f)));
|
|
670
|
+
|
|
671
|
+
// Debounce timer
|
|
672
|
+
let debounceTimer = null;
|
|
673
|
+
const debounceDelay = 100;
|
|
674
|
+
|
|
675
|
+
for (const dir of watchedDirs) {
|
|
676
|
+
watch(dir, { recursive: false }, (_eventType, filename) => {
|
|
677
|
+
if (!filename || !filename.endsWith('.pulse')) return;
|
|
678
|
+
|
|
679
|
+
// Debounce rapid changes
|
|
680
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
681
|
+
debounceTimer = setTimeout(() => {
|
|
682
|
+
const changedFiles = findPulseFiles(patterns);
|
|
683
|
+
runFormatOnFiles(changedFiles, { check: false, write: true, options, isRerun: true })
|
|
684
|
+
.then(() => {
|
|
685
|
+
log.info('Watching for changes...\n');
|
|
686
|
+
});
|
|
687
|
+
}, debounceDelay);
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Keep process alive
|
|
692
|
+
process.stdin.resume();
|
|
693
|
+
} else if (check && result.changedCount > 0) {
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Run format on a list of files
|
|
700
|
+
*/
|
|
701
|
+
async function runFormatOnFiles(files, { check, write, options, isRerun = false }) {
|
|
702
|
+
const timer = createTimer();
|
|
703
|
+
|
|
704
|
+
if (!isRerun) {
|
|
705
|
+
log.info(`${check ? 'Checking' : 'Formatting'} ${files.length} file(s)...\n`);
|
|
706
|
+
}
|
|
654
707
|
|
|
655
708
|
let changedCount = 0;
|
|
656
709
|
let errorCount = 0;
|
|
@@ -675,12 +728,14 @@ export async function runFormat(args) {
|
|
|
675
728
|
log.info(` ${relPath} - formatted`);
|
|
676
729
|
}
|
|
677
730
|
} else {
|
|
678
|
-
if (!check) {
|
|
731
|
+
if (!check && !isRerun) {
|
|
679
732
|
log.info(` ${relPath} - unchanged`);
|
|
680
733
|
}
|
|
681
734
|
}
|
|
682
735
|
}
|
|
683
736
|
|
|
737
|
+
const elapsed = timer.elapsed();
|
|
738
|
+
|
|
684
739
|
// Summary
|
|
685
740
|
log.info('\n' + '─'.repeat(60));
|
|
686
741
|
|
|
@@ -690,16 +745,17 @@ export async function runFormat(args) {
|
|
|
690
745
|
|
|
691
746
|
if (check) {
|
|
692
747
|
if (changedCount > 0) {
|
|
693
|
-
log.error(`✗ ${changedCount} file(s) need formatting`);
|
|
694
|
-
process.exit(1);
|
|
748
|
+
log.error(`✗ ${changedCount} file(s) need formatting (${formatDuration(elapsed)})`);
|
|
695
749
|
} else {
|
|
696
|
-
log.success(`✓ All ${files.length} file(s) are properly formatted`);
|
|
750
|
+
log.success(`✓ All ${files.length} file(s) are properly formatted (${formatDuration(elapsed)})`);
|
|
697
751
|
}
|
|
698
752
|
} else {
|
|
699
753
|
if (changedCount > 0) {
|
|
700
|
-
log.success(`✓ ${changedCount} file(s) formatted`);
|
|
754
|
+
log.success(`✓ ${changedCount} file(s) formatted (${formatDuration(elapsed)})`);
|
|
701
755
|
} else {
|
|
702
|
-
log.success(`✓ All ${files.length} file(s) were already formatted`);
|
|
756
|
+
log.success(`✓ All ${files.length} file(s) were already formatted (${formatDuration(elapsed)})`);
|
|
703
757
|
}
|
|
704
758
|
}
|
|
759
|
+
|
|
760
|
+
return { changedCount, errorCount };
|
|
705
761
|
}
|
package/cli/lint.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* Validates .pulse files for errors and style issues
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
6
|
+
import { readFileSync, writeFileSync, watch } from 'fs';
|
|
7
|
+
import { dirname } from 'path';
|
|
7
8
|
import { findPulseFiles, parseArgs, relativePath } from './utils/file-utils.js';
|
|
8
9
|
import { log } from './logger.js';
|
|
10
|
+
import { createTimer, formatDuration } from './utils/cli-ui.js';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Lint rules configuration
|
|
@@ -701,21 +703,14 @@ export async function lintFile(filePath, options = {}) {
|
|
|
701
703
|
}
|
|
702
704
|
|
|
703
705
|
/**
|
|
704
|
-
*
|
|
706
|
+
* Lint files and return summary
|
|
707
|
+
* @param {string[]} files - Files to lint
|
|
708
|
+
* @param {Object} options - Lint options
|
|
709
|
+
* @returns {Object} Summary with totals
|
|
705
710
|
*/
|
|
706
|
-
|
|
707
|
-
const {
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
// Find files to lint
|
|
711
|
-
const files = findPulseFiles(patterns);
|
|
712
|
-
|
|
713
|
-
if (files.length === 0) {
|
|
714
|
-
log.info('No .pulse files found to lint.');
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
log.info(`Linting ${files.length} file(s)...\n`);
|
|
711
|
+
async function lintFiles(files, options = {}) {
|
|
712
|
+
const { fix = false, quiet = false } = options;
|
|
713
|
+
const timer = createTimer();
|
|
719
714
|
|
|
720
715
|
let totalErrors = 0;
|
|
721
716
|
let totalWarnings = 0;
|
|
@@ -725,7 +720,7 @@ export async function runLint(args) {
|
|
|
725
720
|
const result = await lintFile(file, { fix });
|
|
726
721
|
const relPath = relativePath(file);
|
|
727
722
|
|
|
728
|
-
if (result.diagnostics.length > 0) {
|
|
723
|
+
if (result.diagnostics.length > 0 && !quiet) {
|
|
729
724
|
log.info(`\n${relPath}`);
|
|
730
725
|
|
|
731
726
|
for (const diag of result.diagnostics) {
|
|
@@ -740,21 +735,111 @@ export async function runLint(args) {
|
|
|
740
735
|
}
|
|
741
736
|
}
|
|
742
737
|
|
|
743
|
-
|
|
738
|
+
return {
|
|
739
|
+
errors: totalErrors,
|
|
740
|
+
warnings: totalWarnings,
|
|
741
|
+
info: totalInfo,
|
|
742
|
+
elapsed: timer.elapsed()
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Main lint command handler
|
|
748
|
+
*/
|
|
749
|
+
export async function runLint(args) {
|
|
750
|
+
const { options, patterns } = parseArgs(args);
|
|
751
|
+
const fix = options.fix || false;
|
|
752
|
+
const watchMode = options.watch || options.w || false;
|
|
753
|
+
|
|
754
|
+
// Find files to lint
|
|
755
|
+
const files = findPulseFiles(patterns);
|
|
756
|
+
|
|
757
|
+
if (files.length === 0) {
|
|
758
|
+
log.info('No .pulse files found to lint.');
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Initial lint run
|
|
763
|
+
log.info(`Linting ${files.length} file(s)...\n`);
|
|
764
|
+
const summary = await lintFiles(files, { fix });
|
|
765
|
+
|
|
766
|
+
// Print summary
|
|
767
|
+
printLintSummary(summary, files.length);
|
|
768
|
+
|
|
769
|
+
// Watch mode
|
|
770
|
+
if (watchMode) {
|
|
771
|
+
log.info('\nWatching for changes... (Ctrl+C to stop)\n');
|
|
772
|
+
|
|
773
|
+
const watchedDirs = new Set();
|
|
774
|
+
const debounceTimers = new Map();
|
|
775
|
+
|
|
776
|
+
// Collect directories to watch
|
|
777
|
+
for (const file of files) {
|
|
778
|
+
watchedDirs.add(dirname(file));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Watch each directory
|
|
782
|
+
for (const dir of watchedDirs) {
|
|
783
|
+
watch(dir, { recursive: false }, (_eventType, filename) => {
|
|
784
|
+
if (!filename || !filename.endsWith('.pulse')) return;
|
|
785
|
+
|
|
786
|
+
const filePath = files.find(f => f.endsWith(filename));
|
|
787
|
+
if (!filePath) return;
|
|
788
|
+
|
|
789
|
+
// Debounce rapid changes
|
|
790
|
+
if (debounceTimers.has(filePath)) {
|
|
791
|
+
clearTimeout(debounceTimers.get(filePath));
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
debounceTimers.set(filePath, setTimeout(() => {
|
|
795
|
+
debounceTimers.delete(filePath);
|
|
796
|
+
|
|
797
|
+
log.info(`\n[${new Date().toLocaleTimeString()}] File changed: ${relativePath(filePath)}`);
|
|
798
|
+
lintFiles([filePath], { fix }).then(result => {
|
|
799
|
+
printLintSummary(result, 1, true);
|
|
800
|
+
});
|
|
801
|
+
}, 100));
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Keep process running
|
|
806
|
+
return new Promise(() => {});
|
|
807
|
+
} else {
|
|
808
|
+
// Exit with error code if errors found
|
|
809
|
+
if (summary.errors > 0) {
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Print lint summary
|
|
817
|
+
*/
|
|
818
|
+
function printLintSummary(summary, fileCount, compact = false) {
|
|
819
|
+
const { errors, warnings, info, elapsed } = summary;
|
|
820
|
+
const timeStr = formatDuration(elapsed);
|
|
821
|
+
|
|
822
|
+
if (compact) {
|
|
823
|
+
const parts = [];
|
|
824
|
+
if (errors > 0) parts.push(`${errors} error(s)`);
|
|
825
|
+
if (warnings > 0) parts.push(`${warnings} warning(s)`);
|
|
826
|
+
if (parts.length === 0) {
|
|
827
|
+
log.success(`✓ Passed (${timeStr})`);
|
|
828
|
+
} else {
|
|
829
|
+
log.error(`✗ ${parts.join(', ')} (${timeStr})`);
|
|
830
|
+
}
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
744
834
|
log.info('\n' + '─'.repeat(60));
|
|
745
835
|
const parts = [];
|
|
746
|
-
if (
|
|
747
|
-
if (
|
|
748
|
-
if (
|
|
836
|
+
if (errors > 0) parts.push(`${errors} error(s)`);
|
|
837
|
+
if (warnings > 0) parts.push(`${warnings} warning(s)`);
|
|
838
|
+
if (info > 0) parts.push(`${info} info`);
|
|
749
839
|
|
|
750
840
|
if (parts.length === 0) {
|
|
751
|
-
log.success(`✓ ${
|
|
841
|
+
log.success(`✓ ${fileCount} file(s) passed (${timeStr})`);
|
|
752
842
|
} else {
|
|
753
|
-
log.error(`✗ ${parts.join(', ')} in ${
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// Exit with error code if errors found
|
|
757
|
-
if (totalErrors > 0) {
|
|
758
|
-
process.exit(1);
|
|
843
|
+
log.error(`✗ ${parts.join(', ')} in ${fileCount} file(s) (${timeStr})`);
|
|
759
844
|
}
|
|
760
845
|
}
|