@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 +72 -0
- package/README.md +1 -1
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/compiler/compile.rs +204 -51
- package/compiler/src/compiler/manifest_builder.rs +432 -0
- package/compiler/src/compiler/mod.rs +3 -0
- package/compiler/src/compiler/state_extractor.rs +148 -0
- package/compiler/src/compiler/value_stamper.rs +222 -0
- package/compiler/src/config.rs +0 -5
- package/compiler/src/main.rs +45 -20
- package/compiler/src/parser/html.rs +22 -8
- package/component.js +59 -0
- package/index.js +30 -86
- package/package.json +3 -1
- package/runtime/affected.js +12 -6
- package/runtime/cleanup.js +45 -24
- package/runtime/component-state.js +1 -1
- package/runtime/component.js +20 -13
- package/runtime/constants.js +41 -10
- package/runtime/debug.js +1 -0
- package/runtime/hydrate.js +6 -1
- package/runtime/hyperspeed.js +425 -0
- package/runtime/index.js +236 -49
- package/runtime/iterate.js +3 -0
- package/runtime/parse.js +21 -29
- package/runtime/scope.js +5 -25
- package/runtime/utils.js +4 -13
- package/vibe.css +4 -2
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>
|
|
Binary file
|
|
@@ -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
|
-
|
|
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
|
-
///
|
|
497
|
-
fn
|
|
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
|
|
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.
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
}
|