@ape-egg/vibe 1.1.2 → 1.3.0

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,145 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.0] - 2025-02-06
4
+
5
+ ### Added
6
+
7
+ - **Static Analysis Compiler**: Replaced browser-based manifest generation with pure Rust static analysis
8
+ - New compiler modules: `state_extractor.rs`, `manifest_builder.rs`, `value_stamper.rs`
9
+ - ~3000x performance improvement (10-100 seconds → ~6ms for manifest generation)
10
+ - Pre-rendering support for iterations with initial state values
11
+ - Graceful handling of unparseable state (skips files instead of failing entire compilation)
12
+
13
+ - **Enhanced vibe() and component() API**:
14
+ - Added `config` parameter (second argument) for runtime configuration
15
+ - Added `targetSelector` parameter (third argument) for custom root element selection
16
+ - Multiple calls accumulate state, config/targetSelector use "first wins" strategy
17
+ - Example: `vibe({ count: 0 }, { debug: true }, 'body')`
18
+
19
+ - **Expanded Compilation Scope**: Compiler now processes all HTML files recursively
20
+ - Compiles all `<source-root>/**/*.html` (excluding `components/` directory)
21
+ - Generates manifests for all `<output-dir>/**/*.html` (excluding `components/`)
22
+ - Previously limited to `<source-root>/pages/` only
23
+
24
+ ### Changed
25
+
26
+ - **Iteration Restoration**: Improved DOM restoration algorithm
27
+ - Uses TreeWalker to find iteration comment pairs (more robust)
28
+ - Avoids index-based lookup that breaks after DOM structure changes
29
+ - Runtime now fully controls iteration nodes (skipped during manifest merge)
30
+
31
+ - **Compiler Output**: Enhanced user experience with better formatting
32
+ - Manifest generation occurs before "Compilation successful!" message
33
+ - Verbose mode (`--verbose`) shows only warnings/errors for manifest generation
34
+ - Clean title: "Generating manifests (static analysis)"
35
+ - Summary format: `Generated manifests (X files, Y skipped) in Zms`
36
+ - Positioned between "Compiled HTML" and "Copied files" in output
37
+
38
+ - **Debug Logging**: Refined hyperspeed detection messages
39
+ - Removed redundant path-specific log
40
+ - Changed to: "Detected vibe-hyperspeed. Applying pre-compiled manifest."
41
+
42
+ ### Removed
43
+
44
+ - **Browser Automation Dependencies**: Eliminated heavy runtime dependencies
45
+ - Removed: chromiumoxide, tiny_http, tokio, futures
46
+ - Deleted 414 lines of browser automation code (`manifest.rs`)
47
+ - Pure Rust implementation with no external processes or async complexity
48
+
49
+ ### Fixed
50
+
51
+ - Manifest merge conflicts between hyperspeed (pre-compiled) and runtime trees
52
+ - Index shifting bugs during restoration phase that caused key mismatches
53
+ - Iteration rendering producing duplicate items (3x3 instead of 3)
54
+ - Pre-rendered iteration content not being properly replaced with reactive templates
55
+
56
+ ### Performance
57
+
58
+ - Manifest generation: 10-100 seconds → ~6ms (~3000x faster)
59
+ - Total compilation time: Typically completes in 200-300ms for medium projects
60
+ - Zero browser startup overhead
61
+ - Reduced memory footprint (no Chromium instance)
62
+
63
+ ---
64
+
65
+ ## [1.2.0] - 2026-02-03
66
+
67
+ ### Added
68
+
69
+ - **Component state isolation**: Components now have their own isolated state using `<script type="component">` blocks
70
+ - Each component gets a unique ID (`_cTIMESTAMP_RANDOM`) automatically generated and stored in `data-vibe-component-id` attribute
71
+ - Component state lives at `$[componentId].property` in the global state object
72
+ - Variable declarations (`let count = 0`) are automatically transformed to property assignments (`this.count = 0`)
73
+ - Clean separation between global state and component-specific state
74
+ - Example component (subject to change!):
75
+ ```html
76
+ <script type="component">
77
+ let count = 0;
78
+ let increment = () => { this.count++; };
79
+ </script>
80
+ <button onclick="this.increment()">Clicked @[this.count] times</button>
81
+ ```
82
+ - **`this.property` syntax**: Reference component-scoped state from anywhere inside a component
83
+ - Works in bindings: `@[this.count]`
84
+ - Works in attributes: `value="@[this.inputValue]"`
85
+ - Works in event handlers: `onclick="this.increment()"`
86
+ - Works in name bindings: `@[this.iconName]`
87
+ - Works in conditionals: `<!-- if this.isVisible -->`
88
+ - Works in iterations: `<!-- each this.items as item -->`
89
+ - Runtime automatically resolves `this.property` → `$['componentId'].property`
90
+ - **Component script execution**: `<script type="component">` blocks execute in their own scope
91
+ - Scripts run when component HTML is fetched (runtime `<component>` resolution)
92
+ - State is registered in global `$` object under component ID
93
+ - All siblings after the script tag inherit the component ID via `data-vibe-component-id` attribute
94
+ - Multiple component scripts in same HTML create separate component instances with unique IDs
95
+ - **Props and slots integration**: Component state works seamlessly with existing `<component>` features
96
+ - Props can set component state: `<component src="/card.html" theme="@[userTheme]">`
97
+ - Props work with `this.` references: `@[this.theme]` inside card.html
98
+ - Slots work inside component-scoped HTML
99
+ - Components can be nested with isolated state at each level
100
+
101
+ ### Changed
102
+
103
+ - **Component ID tagging**: DOM elements are now tagged with `data-vibe-component-id` during component processing (previously only scripts had this attribute)
104
+ - All siblings after a `<script type="component">` get tagged with the same component ID
105
+ - Tagging stops when hitting another component script or end of HTML
106
+ - Enables `this.property` resolution in any context (bindings, events, conditionals, iterations)
107
+ - **Event handler rewriting**: Event handlers with `this.property` are now rewritten at parse time
108
+ - `onclick="this.increment()"` → `onclick="$['_c123_abc'].increment()"`
109
+ - DOM properties (like `this.value`, `this.checked`) are preserved and not rewritten
110
+ - Prevents conflicts between component state access and native DOM properties
111
+
112
+ ### Technical Details
113
+
114
+ - **Component state lifecycle**:
115
+ 1. `<component src="/path.html">` fetches HTML
116
+ 2. HTML is parsed in temporary container
117
+ 3. `<script type="component">` blocks are found and executed
118
+ 4. Each script generates unique component ID
119
+ 5. Script and following siblings are tagged with `data-vibe-component-id`
120
+ 6. Component state is registered at `$[componentId]`
121
+ 7. Props are applied (with `this.` reference rewriting)
122
+ 8. Slots are replaced
123
+ 9. Transformed HTML replaces `<component>` element
124
+ 10. MutationObserver triggers reactive hydration with component context
125
+ - **`this.property` resolution**: Helper function `resolveThisPath()` in utils.js walks up DOM tree to find nearest `data-vibe-component-id`, then rewrites path from `this.property` → `componentId.property`
126
+ - **Expression evaluation**: `evalInScope()` in utils.js handles both global (`$.property`) and component-scoped (`$['componentId'].property`) state access, with case-insensitive fallback for HTML-lowercased attribute names
127
+
128
+ ---
129
+
130
+ ## [1.1.3] - 2026-02-02
131
+
132
+ ### Added
133
+
134
+ - **MutationObserver performance optimization**: Fast filter with short-circuit evaluation
135
+ - New `shouldProcessNode()` function checks for Vibe syntax before expensive processing
136
+ - Short-circuits on first match: most Vibe nodes contain `@[`, so check exits immediately
137
+ - Filters out third-party framework mutations (React, Vue, etc.) with cheap string operations
138
+ - Only walks DOM tree for nodes that actually contain Vibe syntax (`@[`, `<!-- each`, `<!-- if`, `<component>`)
139
+ - Enables efficient coexistence with other frameworks on the same page
140
+
141
+ ---
142
+
3
143
  ## [1.1.2] - 2026-02-01
4
144
 
5
145
  ### Fixed
package/README.md CHANGED
@@ -18,20 +18,18 @@ The core reactive runtime. Works directly in the browser without any build tools
18
18
 
19
19
  ```html
20
20
  <html>
21
- <head>
22
- <link rel="stylesheet" href="@ape-egg/vibe/vibe.css">
23
- <script type="module">
24
- import state from "@ape-egg/vibe";
25
- // Attaches to element with attribute "vibe" by default
26
- window.$ = state({ name: "World", count: 0 });
27
- </script>
28
- </head>
29
- <body vibe>
30
-
31
- <h1>Hello, @[name]!</h1>
32
- <button onclick="$.count++">Clicked @[count] times</button>
33
-
34
- </body>
21
+ <head>
22
+ <link rel="stylesheet" href="./node_modules/@ape-egg/vibe/vibe.css" />
23
+ <script type="module">
24
+ import state from './node_modules/@ape-egg/vibe/runtime/index.js';
25
+ // Attaches to element with attribute "vibe" by default
26
+ window.$ = state({ name: 'World', count: 0 });
27
+ </script>
28
+ </head>
29
+ <body vibe-fouc>
30
+ <h1>Hello, @[name]!</h1>
31
+ <button onclick="$.count++">Clicked @[count] times</button>
32
+ </body>
35
33
  </html>
36
34
  ```
37
35
 
@@ -49,12 +47,14 @@ The CSS targets `[vibe]` and hides it until hydration completes. Once Vibe finis
49
47
  Vibe supports bindings in three positions:
50
48
 
51
49
  **Text content** — Inside element tags:
50
+
52
51
  ```html
53
52
  <div>@[firstName] @[lastName]</div>
54
53
  <h1>Hello, @[name]!</h1>
55
54
  ```
56
55
 
57
56
  **Attribute values** — In attribute value position:
57
+
58
58
  ```html
59
59
  <input value="@[username]" />
60
60
  <div class="@[theme]" style="color: @[color]"></div>
@@ -62,15 +62,19 @@ Vibe supports bindings in three positions:
62
62
  ```
63
63
 
64
64
  **Attribute names** — In attribute name position (useful for dynamic attributes):
65
+
65
66
  ```html
66
67
  <icon @[iconName]></icon>
67
68
  <button @[state]>Click me</button>
68
69
  ```
69
70
 
70
71
  **CSS** — Bindings also work in style tags:
72
+
71
73
  ```html
72
74
  <style>
73
- .box { background: @[themeColor]; }
75
+ .box {
76
+ background: @[themeColor];
77
+ }
74
78
  </style>
75
79
  ```
76
80
 
@@ -78,7 +82,7 @@ Vibe supports bindings in three positions:
78
82
 
79
83
  ```html
80
84
  <!-- each items as item, index -->
81
- <li>@[index]: @[item]</li>
85
+ <li>@[index]: @[item]</li>
82
86
  <!-- /each -->
83
87
  ```
84
88
 
@@ -86,9 +90,9 @@ Nested iteration with dot paths:
86
90
 
87
91
  ```html
88
92
  <!-- each categories as category -->
89
- <!-- each category.items as item -->
90
- <span>@[item.name]</span>
91
- <!-- /each -->
93
+ <!-- each category.items as item -->
94
+ <span>@[item.name]</span>
95
+ <!-- /each -->
92
96
  <!-- /each -->
93
97
  ```
94
98
 
@@ -96,9 +100,9 @@ Nested iteration with dot paths:
96
100
 
97
101
  ```html
98
102
  <!-- if user.isAdmin -->
99
- <admin-badge>Admin</admin-badge>
103
+ <admin-badge>Admin</admin-badge>
100
104
  <!-- else -->
101
- <span>User</span>
105
+ <span>User</span>
102
106
  <!-- /if -->
103
107
  ```
104
108
 
@@ -126,9 +130,9 @@ Vibe uses recursive proxies to detect changes at any nesting level:
126
130
 
127
131
  ```javascript
128
132
  // All of these trigger reactive updates:
129
- $.user.name = "Alice";
133
+ $.user.name = 'Alice';
130
134
  $.todos[2].completed = true;
131
- $.config.theme.colors.primary = "#007bff";
135
+ $.config.theme.colors.primary = '#007bff';
132
136
  ```
133
137
 
134
138
  No need for immutable update patterns or spread operators. Just mutate and Vibe handles the rest.
@@ -211,6 +215,7 @@ Add to your `package.json`:
211
215
  ```
212
216
 
213
217
  **Defaults** (when no config):
218
+
214
219
  - `source`: `./`
215
220
  - `output`: `./compiled`
216
221
  - `components`: `<source>/components`
@@ -238,6 +243,7 @@ By default, the compiler creates deployable output by installing production depe
238
243
  3. Removes `package.json` and lockfile from output (cleanup)
239
244
 
240
245
  This ensures:
246
+
241
247
  - Compiled output only includes runtime dependencies (from `dependencies`, not `devDependencies`)
242
248
  - Local `node_modules` is never modified
243
249
  - Faster than copying (no intermediate copy step)
@@ -294,6 +300,7 @@ cp target/release/vibe-compiler ../native/vibe-compiler-darwin-arm64
294
300
  ```
295
301
 
296
302
  Supported platforms:
303
+
297
304
  - `vibe-compiler-darwin-arm64` (macOS Apple Silicon) ✅ Included
298
305
  - `vibe-compiler-darwin-x64` (macOS Intel)
299
306
  - `vibe-compiler-linux-x64`
@@ -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
  }