@ape-egg/vibe 1.2.0 → 1.3.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,77 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.1] - 2025-02-06
4
+
5
+ ### Fixed
6
+
7
+ - **Package exports**: Added missing `./component` export to package.json
8
+ - Enables proper import: `import component from '@ape-egg/vibe/component'`
9
+ - Previously `component.js` was included in package files but not exposed via exports field
10
+
11
+ ---
12
+
13
+ ## [1.3.0] - 2025-02-06
14
+
15
+ ### Added
16
+
17
+ - **Static Analysis Compiler**: Replaced browser-based manifest generation with pure Rust static analysis
18
+ - New compiler modules: `state_extractor.rs`, `manifest_builder.rs`, `value_stamper.rs`
19
+ - ~3000x performance improvement (10-100 seconds → ~6ms for manifest generation)
20
+ - Pre-rendering support for iterations with initial state values
21
+ - Graceful handling of unparseable state (skips files instead of failing entire compilation)
22
+
23
+ - **Enhanced vibe() and component() API**:
24
+ - Added `config` parameter (second argument) for runtime configuration
25
+ - Added `targetSelector` parameter (third argument) for custom root element selection
26
+ - Multiple calls accumulate state, config/targetSelector use "first wins" strategy
27
+ - Example: `vibe({ count: 0 }, { debug: true }, 'body')`
28
+
29
+ - **Expanded Compilation Scope**: Compiler now processes all HTML files recursively
30
+ - Compiles all `<source-root>/**/*.html` (excluding `components/` directory)
31
+ - Generates manifests for all `<output-dir>/**/*.html` (excluding `components/`)
32
+ - Previously limited to `<source-root>/pages/` only
33
+
34
+ ### Changed
35
+
36
+ - **Iteration Restoration**: Improved DOM restoration algorithm
37
+ - Uses TreeWalker to find iteration comment pairs (more robust)
38
+ - Avoids index-based lookup that breaks after DOM structure changes
39
+ - Runtime now fully controls iteration nodes (skipped during manifest merge)
40
+
41
+ - **Compiler Output**: Enhanced user experience with better formatting
42
+ - Manifest generation occurs before "Compilation successful!" message
43
+ - Verbose mode (`--verbose`) shows only warnings/errors for manifest generation
44
+ - Clean title: "Generating manifests (static analysis)"
45
+ - Summary format: `Generated manifests (X files, Y skipped) in Zms`
46
+ - Positioned between "Compiled HTML" and "Copied files" in output
47
+
48
+ - **Debug Logging**: Refined hyperspeed detection messages
49
+ - Removed redundant path-specific log
50
+ - Changed to: "Detected vibe-hyperspeed. Applying pre-compiled manifest."
51
+
52
+ ### Removed
53
+
54
+ - **Browser Automation Dependencies**: Eliminated heavy runtime dependencies
55
+ - Removed: chromiumoxide, tiny_http, tokio, futures
56
+ - Deleted 414 lines of browser automation code (`manifest.rs`)
57
+ - Pure Rust implementation with no external processes or async complexity
58
+
59
+ ### Fixed
60
+
61
+ - Manifest merge conflicts between hyperspeed (pre-compiled) and runtime trees
62
+ - Index shifting bugs during restoration phase that caused key mismatches
63
+ - Iteration rendering producing duplicate items (3x3 instead of 3)
64
+ - Pre-rendered iteration content not being properly replaced with reactive templates
65
+
66
+ ### Performance
67
+
68
+ - Manifest generation: 10-100 seconds → ~6ms (~3000x faster)
69
+ - Total compilation time: Typically completes in 200-300ms for medium projects
70
+ - Zero browser startup overhead
71
+ - Reduced memory footprint (no Chromium instance)
72
+
73
+ ---
74
+
3
75
  ## [1.2.0] - 2026-02-03
4
76
 
5
77
  ### Added
package/README.md CHANGED
@@ -26,7 +26,7 @@ The core reactive runtime. Works directly in the browser without any build tools
26
26
  window.$ = state({ name: 'World', count: 0 });
27
27
  </script>
28
28
  </head>
29
- <body vibe>
29
+ <body vibe-fouc>
30
30
  <h1>Hello, @[name]!</h1>
31
31
  <button onclick="$.count++">Clicked @[count] times</button>
32
32
  </body>
@@ -100,6 +100,12 @@ pub struct CompileStats {
100
100
  pub components_as_is: bool,
101
101
  }
102
102
 
103
+ pub struct ManifestStats {
104
+ pub pages_processed: usize,
105
+ pub pages_skipped: usize,
106
+ pub total_time_ms: f64,
107
+ }
108
+
103
109
  #[derive(Debug, Clone, PartialEq)]
104
110
  enum FileOperation {
105
111
  Compiled,
@@ -420,7 +426,13 @@ impl Compiler {
420
426
 
421
427
  // Track compile and copy times separately
422
428
  let compile_start = Instant::now();
423
- self.process_directory(&self.config.source.clone(), &parser, "", &mut stats)?;
429
+
430
+ // Process all HTML files in source (excluding components directory)
431
+ self.process_directory_html_only(&self.config.source.clone(), &parser, "", &mut stats)?;
432
+
433
+ // Process source directory for assets (CSS, JS, images, etc.)
434
+ self.process_directory_assets_only(&self.config.source.clone(), "", &mut stats)?;
435
+
424
436
  let process_time = compile_start.elapsed();
425
437
 
426
438
  // Print verbose output after processing (components first, then HTML, then copied)
@@ -493,8 +505,112 @@ impl Compiler {
493
505
  Ok(stats)
494
506
  }
495
507
 
496
- /// Recursively walk source, compile HTML, copy CSS/JS (MIRROR_MODE)
497
- fn process_directory(
508
+ /// Generate manifest for a single file
509
+ fn generate_file_manifest(
510
+ html: &str,
511
+ html_path: &Path,
512
+ output_dir: &Path,
513
+ relative_path: &str,
514
+ _verbose: bool,
515
+ ) -> Result<(), String> {
516
+ use crate::compiler::state_extractor::StateExtractor;
517
+ use crate::compiler::manifest_builder::ManifestBuilder;
518
+ use crate::compiler::value_stamper::ValueStamper;
519
+
520
+ // Extract state
521
+ let state = StateExtractor::extract_from_html(html)?;
522
+
523
+ // Build manifest
524
+ let manifest_builder = ManifestBuilder::new();
525
+ let manifest = manifest_builder.build_from_html(html, &state)?;
526
+
527
+ // Stamp values (pre-render)
528
+ let stamper = ValueStamper::new(&state);
529
+ let pre_rendered = stamper.stamp_html(html.to_string())?;
530
+
531
+ // Write pre-rendered HTML
532
+ fs::write(html_path, pre_rendered)
533
+ .map_err(|e| format!("Failed to write HTML: {}", e))?;
534
+
535
+ // Write manifest
536
+ let manifest_path = output_dir
537
+ .join("vibe-hyperspeed")
538
+ .join(format!("{}.manifest.js", relative_path));
539
+
540
+ // Ensure directory exists
541
+ if let Some(parent) = manifest_path.parent() {
542
+ fs::create_dir_all(parent)
543
+ .map_err(|e| format!("Failed to create manifest directory: {}", e))?;
544
+ }
545
+
546
+ let manifest_json = serde_json::to_string(&manifest)
547
+ .map_err(|e| format!("Failed to serialize manifest: {}", e))?;
548
+
549
+ let manifest_js = format!(
550
+ "// Pre-compiled manifest for /{}\n// Generated by Vibe compiler\n\nexport default {};\n",
551
+ relative_path,
552
+ manifest_json
553
+ );
554
+
555
+ fs::write(&manifest_path, manifest_js)
556
+ .map_err(|e| format!("Failed to write manifest: {}", e))?;
557
+
558
+ Ok(())
559
+ }
560
+
561
+ /// Generate manifests (called separately from main.rs if needed)
562
+ pub fn generate_manifests(&self) -> Result<ManifestStats, CompileError> {
563
+
564
+ let start = Instant::now();
565
+ let mut pages_processed = 0;
566
+ let mut pages_skipped = 0;
567
+
568
+ if self.verbose {
569
+ println!("\nGenerating manifests (static analysis)");
570
+ }
571
+
572
+ // Find all HTML files in compiled output (excluding components directory)
573
+ let html_files = self.find_all_html_files(&self.config.output)?;
574
+
575
+ for html_path in html_files {
576
+ let relative_path = html_path.strip_prefix(&self.config.output)
577
+ .unwrap()
578
+ .to_str()
579
+ .unwrap();
580
+
581
+ // Read compiled HTML
582
+ let html = fs::read_to_string(&html_path)
583
+ .map_err(|e| CompileError::ReadError {
584
+ path: html_path.display().to_string(),
585
+ source: e,
586
+ })?;
587
+
588
+ // Try to generate manifest for this file (skip on error)
589
+ match Self::generate_file_manifest(&html, &html_path, &self.config.output, relative_path, self.verbose) {
590
+ Ok(()) => {
591
+ pages_processed += 1;
592
+ }
593
+ Err(e) => {
594
+ pages_skipped += 1;
595
+ if self.verbose {
596
+ println!(" Skipped ({}): {}", e, relative_path);
597
+ }
598
+ // Don't fail compilation, just skip this file's manifest
599
+ }
600
+ }
601
+ }
602
+
603
+ let total_time_ms = start.elapsed().as_millis() as f64;
604
+
605
+ Ok(ManifestStats {
606
+ pages_processed,
607
+ pages_skipped,
608
+ total_time_ms,
609
+ })
610
+ }
611
+
612
+ /// Process directory for HTML files only (used for pages directory)
613
+ fn process_directory_html_only(
498
614
  &mut self,
499
615
  dir: &Path,
500
616
  parser: &HtmlParser,
@@ -516,7 +632,84 @@ impl Compiler {
516
632
  continue;
517
633
  }
518
634
 
519
- // Skip node_modules (handled separately after compilation)
635
+ // Skip components directory
636
+ if file_name == self.config.components {
637
+ continue;
638
+ }
639
+
640
+ // Recurse into subdirectory
641
+ let new_relative = if relative_path.is_empty() {
642
+ file_name.to_string()
643
+ } else {
644
+ format!("{}/{}", relative_path, file_name)
645
+ };
646
+
647
+ self.process_directory_html_only(&path, parser, &new_relative, stats)?;
648
+ } else if path.extension().and_then(|e| e.to_str()) == Some("html") {
649
+ let (internal, external, component_srcs) = self.compile_html_file(&path, parser, relative_path)?;
650
+ stats.files_compiled += 1;
651
+ stats.internal_components_total += internal;
652
+ stats.external_components_total += external;
653
+
654
+ // Track unique components
655
+ for src in &component_srcs {
656
+ self.unique_components.insert(src.clone());
657
+ }
658
+
659
+ // Build component relationships and count occurrences before borrowing logger
660
+ let (component_relationships, all_srcs) = if self.logger.is_some() && !component_srcs.is_empty() {
661
+ let relationships = self.build_component_relationships(&component_srcs);
662
+ (Some(relationships), Some(component_srcs))
663
+ } else {
664
+ (None, None)
665
+ };
666
+
667
+ if let Some(ref mut logger) = self.logger {
668
+ logger.log(&path, FileOperation::Compiled, &self.config.source);
669
+
670
+ // Log each component occurrence (for counting)
671
+ if let Some(all_srcs) = all_srcs {
672
+ for src in all_srcs {
673
+ logger.log_component_occurrence(src);
674
+ }
675
+ }
676
+
677
+ // Log unique relationships (for tree structure)
678
+ if let Some(relationships) = component_relationships {
679
+ for (src, children) in relationships {
680
+ logger.log_component_children(src, children);
681
+ }
682
+ }
683
+ }
684
+ }
685
+ }
686
+
687
+ Ok(())
688
+ }
689
+
690
+ /// Process directory for assets only (CSS, JS, images, etc.) - skip HTML
691
+ fn process_directory_assets_only(
692
+ &mut self,
693
+ dir: &Path,
694
+ relative_path: &str,
695
+ stats: &mut CompileStats,
696
+ ) -> Result<(), CompileError> {
697
+ let entries = fs::read_dir(dir).map_err(|e| CompileError::ReadError {
698
+ path: dir.display().to_string(),
699
+ source: e,
700
+ })?;
701
+
702
+ for entry in entries.flatten() {
703
+ let path = entry.path();
704
+ let file_name = path.file_name().unwrap().to_str().unwrap();
705
+
706
+ if path.is_dir() {
707
+ // Skip special directories
708
+ if SKIP_DIRECTORIES.contains(&file_name) {
709
+ continue;
710
+ }
711
+
712
+ // Skip node_modules (handled separately)
520
713
  if file_name == "node_modules" {
521
714
  continue;
522
715
  }
@@ -543,55 +736,15 @@ impl Compiler {
543
736
  format!("{}/{}", relative_path, file_name)
544
737
  };
545
738
 
546
- self.process_directory(&path, parser, &new_relative, stats)?;
739
+ self.process_directory_assets_only(&path, &new_relative, stats)?;
547
740
  } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
548
- match ext {
549
- "html" => {
550
- let (internal, external, component_srcs) = self.compile_html_file(&path, parser, relative_path)?;
551
- stats.files_compiled += 1;
552
- stats.internal_components_total += internal;
553
- stats.external_components_total += external;
554
-
555
- // Track unique components
556
- for src in &component_srcs {
557
- self.unique_components.insert(src.clone());
558
- }
559
-
560
- // Build component relationships and count occurrences before borrowing logger
561
- let (component_relationships, all_srcs) = if self.logger.is_some() && !component_srcs.is_empty() {
562
- let relationships = self.build_component_relationships(&component_srcs);
563
- (Some(relationships), Some(component_srcs))
564
- } else {
565
- (None, None)
566
- };
567
-
568
- if let Some(ref mut logger) = self.logger {
569
- logger.log(&path, FileOperation::Compiled, &self.config.source);
570
-
571
- // Log each component occurrence (for counting)
572
- if let Some(all_srcs) = all_srcs {
573
- for src in all_srcs {
574
- logger.log_component_occurrence(src);
575
- }
576
- }
577
-
578
- // Log unique relationships (for tree structure)
579
- if let Some(relationships) = component_relationships {
580
- for (src, children) in relationships {
581
- logger.log_component_children(src, children);
582
- }
583
- }
584
- }
585
- }
586
- // MIRROR_MODE: Copy CSS/JS as-is
587
- ext if MIRROR_EXTENSIONS.contains(&ext) => {
588
- self.copy_file(&path, relative_path)?;
589
- stats.files_copied += 1;
590
- if let Some(ref mut logger) = self.logger {
591
- logger.log(&path, FileOperation::Copied, &self.config.source);
592
- }
741
+ // Only copy non-HTML assets
742
+ if ext != "html" && MIRROR_EXTENSIONS.contains(&ext) {
743
+ self.copy_file(&path, relative_path)?;
744
+ stats.files_copied += 1;
745
+ if let Some(ref mut logger) = self.logger {
746
+ logger.log(&path, FileOperation::Copied, &self.config.source);
593
747
  }
594
- _ => {}
595
748
  }
596
749
  }
597
750
  }