pulse-js-framework 1.7.3 → 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 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(' SUMMARY');
390
- lines.push(' ' + ''.repeat(40));
391
- lines.push(` Total files: ${analysis.summary.totalFiles}`);
392
- lines.push(` .pulse files: ${analysis.summary.pulseFiles}`);
393
- lines.push(` .js files: ${analysis.summary.jsFiles}`);
394
- lines.push(` Total size: ${analysis.summary.totalSizeFormatted}`);
395
-
396
- // Complexity (top 5)
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
- lines.push('');
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
- for (const comp of top5) {
403
- const bar = '█'.repeat(Math.min(comp.complexity, 20));
404
- lines.push(` ${comp.componentName.padEnd(20)} ${String(comp.complexity).padStart(3)} ${bar}`);
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
- lines.push('');
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
- lines.push('');
422
- lines.push(' FILE BREAKDOWN');
423
- lines.push(' ' + '─'.repeat(40));
424
- for (const file of analysis.fileBreakdown) {
425
- const size = file.sizeFormatted.padStart(10);
426
- lines.push(` ${size} ${file.path}`);
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
- lines.push('');
433
- lines.push(' IMPORT GRAPH');
434
- lines.push(' ' + '─'.repeat(40));
435
- for (const edge of analysis.importGraph.edges.slice(0, 20)) {
436
- lines.push(` ${edge.from} → ${edge.to}`);
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
- lines.push('');
446
- lines.push(' STATE VARIABLES');
447
- lines.push(' ' + '─'.repeat(40));
448
- for (const state of analysis.stateUsage.slice(0, 10)) {
449
- const shared = state.isShared ? ' (shared)' : '';
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 (const file of state.files) {
452
- lines.push(` └─ ${file}`);
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
- log.info('Analyzing bundle...\n');
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
- log.error('Analysis failed:', error.message);
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
- log.info('Vite config detected, using Vite build...');
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
- // Process source files
48
+ // Count files for progress bar
46
49
  const srcDir = join(root, 'src');
47
- if (existsSync(srcDir)) {
48
- processDirectory(srcDir, join(outDir, 'assets'));
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 directory: ${relative(root, outDir)}
87
+ Output: ${relative(root, outDir)}/
88
+ Files: ${fileCount} processed
75
89
 
76
- To preview the build:
77
- npx serve dist
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
- log.info(` Processed & minified: ${file}`);
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
  }
@@ -185,28 +223,104 @@ function readRuntimeFile(filename) {
185
223
 
186
224
  /**
187
225
  * Minify JavaScript code (simple minification)
188
- * String-aware: preserves content inside string literals
226
+ * String and regex-aware: preserves content inside string and regex literals
189
227
  */
190
228
  export function minifyJS(code) {
191
- // Extract strings to protect them from minification
192
- const strings = [];
193
- const placeholder = '\x00STR';
194
-
195
- // Replace strings with placeholders (handles ", ', and ` with escape sequences)
196
- const withPlaceholders = code.replace(
197
- /(["'`])(?:\\.|(?!\1)[^\\])*\1/g,
198
- (match) => {
199
- strings.push(match);
200
- return placeholder + (strings.length - 1) + '\x00';
229
+ // Extract strings and regexes to protect them from minification
230
+ const preserved = [];
231
+ const placeholder = '\x00PRE';
232
+
233
+ // State machine to properly handle strings and regexes
234
+ let result = '';
235
+ let i = 0;
236
+
237
+ while (i < code.length) {
238
+ const char = code[i];
239
+ const next = code[i + 1];
240
+
241
+ // Skip single-line comments
242
+ if (char === '/' && next === '/') {
243
+ while (i < code.length && code[i] !== '\n') i++;
244
+ continue;
201
245
  }
202
- );
203
246
 
204
- // Apply minification to non-string parts
205
- let minified = withPlaceholders
206
- // Remove single-line comments
207
- .replace(/\/\/.*$/gm, '')
208
- // Remove multi-line comments
209
- .replace(/\/\*[\s\S]*?\*\//g, '')
247
+ // Skip multi-line comments
248
+ if (char === '/' && next === '*') {
249
+ i += 2;
250
+ while (i < code.length - 1 && !(code[i] === '*' && code[i + 1] === '/')) i++;
251
+ i += 2;
252
+ continue;
253
+ }
254
+
255
+ // Handle string literals
256
+ if (char === '"' || char === "'" || char === '`') {
257
+ const quote = char;
258
+ let str = char;
259
+ i++;
260
+ while (i < code.length) {
261
+ const c = code[i];
262
+ str += c;
263
+ if (c === '\\' && i + 1 < code.length) {
264
+ i++;
265
+ str += code[i];
266
+ } else if (c === quote) {
267
+ break;
268
+ }
269
+ i++;
270
+ }
271
+ i++;
272
+ preserved.push(str);
273
+ result += placeholder + (preserved.length - 1) + '\x00';
274
+ continue;
275
+ }
276
+
277
+ // Handle regex literals (after = : ( , [ ! & | ? ; { or keywords)
278
+ if (char === '/') {
279
+ // Look back to determine if this is a regex
280
+ const lookback = result.slice(-20).trim();
281
+ const isRegexContext = lookback === '' ||
282
+ /[=:(\[,!&|?;{]$/.test(lookback) ||
283
+ /\breturn$/.test(lookback) ||
284
+ /\bthrow$/.test(lookback) ||
285
+ /\btypeof$/.test(lookback);
286
+
287
+ if (isRegexContext && next !== '/' && next !== '*') {
288
+ let regex = char;
289
+ i++;
290
+ let inCharClass = false;
291
+ while (i < code.length) {
292
+ const c = code[i];
293
+ regex += c;
294
+ if (c === '\\' && i + 1 < code.length) {
295
+ i++;
296
+ regex += code[i];
297
+ } else if (c === '[') {
298
+ inCharClass = true;
299
+ } else if (c === ']') {
300
+ inCharClass = false;
301
+ } else if (c === '/' && !inCharClass) {
302
+ // End of regex, collect flags
303
+ i++;
304
+ while (i < code.length && /[gimsuvy]/.test(code[i])) {
305
+ regex += code[i];
306
+ i++;
307
+ }
308
+ break;
309
+ }
310
+ i++;
311
+ }
312
+ preserved.push(regex);
313
+ result += placeholder + (preserved.length - 1) + '\x00';
314
+ continue;
315
+ }
316
+ }
317
+
318
+ result += char;
319
+ i++;
320
+ }
321
+
322
+ // Apply minification to non-preserved parts
323
+ let minified = result
210
324
  // Remove leading/trailing whitespace per line
211
325
  .split('\n')
212
326
  .map(line => line.trim())
@@ -214,7 +328,7 @@ export function minifyJS(code) {
214
328
  .join('\n')
215
329
  // Collapse multiple newlines
216
330
  .replace(/\n{2,}/g, '\n')
217
- // Remove spaces around operators (simple)
331
+ // Remove spaces around operators
218
332
  .replace(/\s*([{};,:])\s*/g, '$1')
219
333
  .replace(/\s*=\s*/g, '=')
220
334
  .replace(/\s*\(\s*/g, '(')
@@ -223,10 +337,10 @@ export function minifyJS(code) {
223
337
  .replace(/\s+/g, ' ')
224
338
  .trim();
225
339
 
226
- // Restore strings
340
+ // Restore preserved strings and regexes
227
341
  minified = minified.replace(
228
342
  new RegExp(placeholder.replace('\x00', '\\x00') + '(\\d+)\\x00', 'g'),
229
- (_, index) => strings[parseInt(index)]
343
+ (_, index) => preserved[parseInt(index)]
230
344
  );
231
345
 
232
346
  return minified;
package/cli/dev.js CHANGED
@@ -128,7 +128,10 @@ export async function startDevServer(args) {
128
128
  });
129
129
 
130
130
  if (result.success) {
131
- res.writeHead(200, { 'Content-Type': 'application/javascript' });
131
+ res.writeHead(200, {
132
+ 'Content-Type': 'application/javascript',
133
+ 'Cache-Control': 'no-cache, no-store, must-revalidate'
134
+ });
132
135
  res.end(result.code);
133
136
  } else {
134
137
  res.writeHead(500, { 'Content-Type': 'text/plain' });
@@ -146,7 +149,10 @@ export async function startDevServer(args) {
146
149
  if (pathname.endsWith('.js') || pathname.endsWith('.mjs')) {
147
150
  if (existsSync(filePath)) {
148
151
  const content = readFileSync(filePath, 'utf-8');
149
- res.writeHead(200, { 'Content-Type': 'application/javascript' });
152
+ res.writeHead(200, {
153
+ 'Content-Type': 'application/javascript',
154
+ 'Cache-Control': 'no-cache, no-store, must-revalidate'
155
+ });
150
156
  res.end(content);
151
157
  return;
152
158
  }
@@ -164,7 +170,10 @@ export async function startDevServer(args) {
164
170
  });
165
171
 
166
172
  if (result.success) {
167
- res.writeHead(200, { 'Content-Type': 'application/javascript' });
173
+ res.writeHead(200, {
174
+ 'Content-Type': 'application/javascript',
175
+ 'Cache-Control': 'no-cache, no-store, must-revalidate'
176
+ });
168
177
  res.end(result.code);
169
178
  } else {
170
179
  res.writeHead(500, { 'Content-Type': 'text/plain' });
@@ -183,7 +192,10 @@ export async function startDevServer(args) {
183
192
  const modulePath = join(root, '..', 'pulse', pathname.replace('/node_modules/pulse-js-framework/', ''));
184
193
  if (existsSync(modulePath)) {
185
194
  const content = readFileSync(modulePath, 'utf-8');
186
- res.writeHead(200, { 'Content-Type': 'application/javascript' });
195
+ res.writeHead(200, {
196
+ 'Content-Type': 'application/javascript',
197
+ 'Cache-Control': 'no-cache, no-store, must-revalidate'
198
+ });
187
199
  res.end(content);
188
200
  return;
189
201
  }
@@ -203,7 +215,10 @@ export async function startDevServer(args) {
203
215
  for (const runtimePath of possiblePaths) {
204
216
  if (existsSync(runtimePath)) {
205
217
  const content = readFileSync(runtimePath, 'utf-8');
206
- res.writeHead(200, { 'Content-Type': 'application/javascript' });
218
+ res.writeHead(200, {
219
+ 'Content-Type': 'application/javascript',
220
+ 'Cache-Control': 'no-cache, no-store, must-revalidate'
221
+ });
207
222
  res.end(content);
208
223
  return;
209
224
  }