@ape-egg/vibe 2.1.22 → 3.0.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.
Files changed (56) hide show
  1. package/README.md +112 -5
  2. package/boot.js +4 -4
  3. package/component.js +27 -29
  4. package/hot-module-refresh.js +4 -4
  5. package/index.js +26 -17
  6. package/llms.txt +36 -5
  7. package/package.json +20 -14
  8. package/runtime/affected.js +159 -36
  9. package/runtime/cleanup.js +45 -1
  10. package/runtime/component.js +360 -98
  11. package/runtime/conditionals.js +111 -14
  12. package/runtime/debug.js +24 -0
  13. package/runtime/dispatch.js +172 -0
  14. package/runtime/hydrate.js +277 -110
  15. package/runtime/index.js +189 -65
  16. package/runtime/iterate.js +125 -50
  17. package/runtime/iteration-utils.js +59 -8
  18. package/runtime/manifest.js +77 -2
  19. package/runtime/parse.js +81 -11
  20. package/runtime/pre-compiled-iterations.js +19 -6
  21. package/runtime/pre-compiled-manifest.js +13 -4
  22. package/runtime/staging.js +153 -0
  23. package/runtime/state.js +31 -0
  24. package/runtime/tracking.js +173 -0
  25. package/runtime/utils.js +155 -78
  26. package/spa.js +206 -0
  27. package/vibe.css +8 -4
  28. package/CHANGELOG.md +0 -1159
  29. package/ROADMAP.md +0 -397
  30. package/compiler/bin/vibe-compile.js +0 -121
  31. package/compiler/native/.gitkeep +0 -0
  32. package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
  33. package/compiler/native/vibe-compiler-linux-x64 +0 -0
  34. package/compiler/src/Cargo.lock +0 -2023
  35. package/compiler/src/Cargo.toml +0 -38
  36. package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +0 -241
  37. package/compiler/src/compiler/binding_case.rs +0 -88
  38. package/compiler/src/compiler/compile.rs +0 -2522
  39. package/compiler/src/compiler/component_tagger.rs +0 -469
  40. package/compiler/src/compiler/iteration_optimizer.rs +0 -455
  41. package/compiler/src/compiler/js_analyzer.rs +0 -715
  42. package/compiler/src/compiler/manifest_builder.rs +0 -693
  43. package/compiler/src/compiler/mod.rs +0 -15
  44. package/compiler/src/compiler/name_binding_protect.rs +0 -207
  45. package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
  46. package/compiler/src/compiler/state_extractor.rs +0 -263
  47. package/compiler/src/compiler/value_stamper.rs +0 -921
  48. package/compiler/src/compiler/watcher.rs +0 -1147
  49. package/compiler/src/config.rs +0 -239
  50. package/compiler/src/main.rs +0 -347
  51. package/compiler/src/parser/element.rs +0 -96
  52. package/compiler/src/parser/html.rs +0 -1004
  53. package/compiler/src/parser/mod.rs +0 -8
  54. package/runtime/pre-compiled-manifest.test.mjs +0 -58
  55. package/runtime/scope.js +0 -50
  56. package/test-results/.last-run.json +0 -4
@@ -1,38 +0,0 @@
1
- [package]
2
- name = "vibe-compiler"
3
- version = "2.0.3"
4
- edition = "2021"
5
- description = "Vibe framework compiler - compiles Vibe source files into optimized output"
6
- authors = ["Kim Korte"]
7
- license = "ISC"
8
-
9
- [[bin]]
10
- name = "vibe-compiler"
11
- path = "main.rs"
12
-
13
- [dependencies]
14
- clap = { version = "4.4", features = ["derive"] }
15
- serde = { version = "=1.0.210", features = ["derive"] }
16
- serde_json = "1.0"
17
- html5ever = "0.27"
18
- markup5ever_rcdom = "0.3"
19
- markup5ever = "0.12"
20
- thiserror = "1.0"
21
- regex = "1.10"
22
- colored = "2.1"
23
- ureq = { version = "2", features = ["tls"] }
24
- glob = "0.3"
25
- rquickjs = "0.6"
26
- swc_common = "=0.40.1"
27
- swc_ecma_parser = "=0.152.0"
28
- swc_ecma_ast = "=0.121.0"
29
- swc_ecma_visit = "=0.107.0"
30
- notify = "6.1"
31
- notify-debouncer-full = "0.3"
32
- rayon = "1.10"
33
-
34
- [profile.release]
35
- opt-level = 3
36
- lto = true
37
- codegen-units = 1
38
- strip = true
@@ -1,241 +0,0 @@
1
- # Pre-Rendering Implementation: swc, QuickJS, and Manifest Restoration
2
-
3
- ## Overview
4
-
5
- This document describes how pre-rendering works in Vibe's compiler, the technical challenges encountered, and the solutions implemented. The goal is to pre-render HTML with actual state values (FOUC prevention) while preserving markers for runtime reactivity.
6
-
7
- ## Technologies Used
8
-
9
- ### swc (JavaScript AST Parser)
10
- - **Purpose**: Parse JavaScript code to extract state variables and resolve imports
11
- - **Use Case**: When HTML contains `import { menuSections } from '/index.js'`, we need to know what variables exist
12
- - **Implementation**: `js_analyzer.rs` - traverses AST to find import declarations and build scope
13
-
14
- ### QuickJS (JavaScript Runtime)
15
- - **Purpose**: Execute JavaScript to evaluate state values for pre-rendering
16
- - **Use Case**: Transform `@[firstName]` → `"John"` by evaluating the state object
17
- - **Implementation**: `value_stamper.rs` - creates JS context with state, evaluates expressions
18
-
19
- ## The Pre-Rendering Flow
20
-
21
- ```
22
- 1. Parse HTML → 2. Extract State → 3. Build Manifest → 4. Stamp Values
23
- (html5ever) (swc + QuickJS) (before stamping) (QuickJS)
24
- ```
25
-
26
- **Critical**: Manifest must be built BEFORE value stamping, otherwise templates contain pre-rendered values instead of markers.
27
-
28
- ## Problems Encountered and Solutions
29
-
30
- ### Problem 1: Manifest Build Order
31
- **Symptom**: Manifest templates contained `"Vibe Compiled"` instead of `@[section.label]`
32
-
33
- **Root Cause**: Manifest was being built AFTER value stamping:
34
- ```rust
35
- // WRONG ORDER (old code)
36
- let pre_rendered = stamper.stamp_html(html.to_string())?;
37
- let manifest = manifest_builder.build_from_html(&pre_rendered, &state, iterations_as_is)?;
38
- ```
39
-
40
- **Solution**: Reorder operations - build manifest first:
41
- ```rust
42
- // CORRECT ORDER (compile.rs lines 556-565)
43
- let state = StateExtractor::extract_from_html(html, &source_root.to_path_buf())?;
44
- let manifest = manifest_builder.build_from_html(html, &state, iterations_as_is)?;
45
- let pre_rendered = stamper.stamp_html(html.to_string())?;
46
- ```
47
-
48
- **Files Modified**: `compile.rs`
49
-
50
- ---
51
-
52
- ### Problem 2: Multiline Script Tag Parsing
53
- **Symptom**: State extraction logged "found 0-length scripts"
54
-
55
- **Root Cause**: Regex didn't match newlines in multiline `<script>` blocks:
56
- ```rust
57
- // WRONG (old code)
58
- let script_regex = Regex::new(r#"<script[^>]*>(.*?)</script>"#).unwrap();
59
- ```
60
-
61
- **Solution**: Add `(?s)` flag to make `.` match newlines:
62
- ```rust
63
- // CORRECT (state_extractor.rs line 22)
64
- let script_regex = Regex::new(r#"(?s)<script[^>]*>(.*?)</script>"#).unwrap();
65
- ```
66
-
67
- **Files Modified**: `state_extractor.rs`
68
-
69
- ---
70
-
71
- ### Problem 3: Invalid Name Binding HTML Syntax
72
- **Symptom**: Elements inserted via `insertBefore()` but immediately disappeared from DOM
73
-
74
- **Root Cause**: html5ever serializer normalized name bindings to invalid HTML:
75
- - Input: `<icon @[section.icon]>`
76
- - html5ever output: `<icon @[section.icon]="">`
77
- - Browsers reject attributes with dynamic names followed by `=""`
78
-
79
- **Solution**: Post-process serialized HTML to strip `=""` from name bindings:
80
- ```rust
81
- // manifest_builder.rs (serialize_nodes function)
82
- let name_binding_fix = Regex::new(r#"(@\[[^\]]+\])="+"#).unwrap();
83
- html = name_binding_fix.replace_all(&html, "$1").to_string();
84
- ```
85
-
86
- **Shortcut Taken**: This is a post-processing hack rather than fixing html5ever's serialization logic. It works for standard cases but could theoretically fail if there are edge cases with quotes inside bindings.
87
-
88
- **Files Modified**: `manifest_builder.rs`
89
-
90
- ---
91
-
92
- ### Problem 4: Wrong Iteration Comment Matching
93
- **Symptom**: Restoration found nested `<!-- each section.items as item -->` instead of target `<!-- each menuSections as section -->`
94
-
95
- **Root Cause**: Searched for first `<!-- each -->` comment without specificity
96
-
97
- **Solution**: Store and match the exact iteration expression:
98
- ```rust
99
- // manifest_builder.rs (lines 142-161)
100
- if trimmed.starts_with("each ") {
101
- let expression = trimmed.strip_prefix("each ").unwrap_or("").to_string();
102
- // ... store expression in RestorationData
103
- }
104
- ```
105
-
106
- ```javascript
107
- // pre-compiled-manifest.js
108
- const expectedComment = `each ${restoration.expression}`;
109
- if (trimmed === expectedComment && !startComment) {
110
- startComment = comment;
111
- }
112
- ```
113
-
114
- **Files Modified**: `manifest_builder.rs`, `pre-compiled-manifest.js`
115
-
116
- ---
117
-
118
- ### Problem 5: Wrong End Comment - Nested Iteration Depth (THE CRITICAL FIX)
119
- **Symptom**:
120
- - Menu rendered 25 times (5 × 5) instead of 5
121
- - Restoration found wrong parent: `ACCORDION-CONTENT` instead of correct parent
122
- - Console showed duplicate conditional comments
123
-
124
- **Root Cause**:
125
- HTML structure has nested iterations:
126
- ```html
127
- <!-- each menuSections as section --> (line 57)
128
- <nav-section>
129
- <accordion>
130
- <!-- each section.items as item -->
131
- ...
132
- <!-- /each --> (line 98 - closes section.items, NOT menuSections!)
133
- </accordion>
134
- </nav-section>
135
- <!-- 4 more nav-sections with their own nested iterations -->
136
- <!-- /each --> (line 483 - actually closes menuSections)
137
- ```
138
-
139
- The restoration code found the correct start comment but matched the FIRST `<!-- /each -->` (line 98), which closed a nested iteration. This caused:
140
- 1. Only removing the first nav-section (5 items total, leaving 4 = 20 items)
141
- 2. Inserting template 5 times (once per menuSection)
142
- 3. Each insertion added 5 items = 25 total
143
-
144
- **Solution**: Track nesting depth and only match `/each` at depth 0:
145
- ```javascript
146
- // pre-compiled-manifest.js
147
- let depth = 0;
148
- const walker = document.createTreeWalker(element, NodeFilter.SHOW_COMMENT);
149
- while (walker.nextNode()) {
150
- const comment = walker.currentNode;
151
- const trimmed = comment.textContent.trim();
152
-
153
- if (trimmed === expectedComment && !startComment) {
154
- startComment = comment;
155
- depth = 1; // We're now inside this iteration
156
- } else if (startComment) {
157
- if (trimmed.startsWith('each ')) {
158
- depth++; // Entering nested iteration
159
- } else if (trimmed === '/each') {
160
- depth--; // Exiting an iteration
161
- if (depth === 0) {
162
- endComment = comment; // This closes OUR iteration
163
- break;
164
- }
165
- }
166
- }
167
- }
168
- ```
169
-
170
- **Why This Works**:
171
- - `depth = 1` when we enter `menuSections`
172
- - `depth = 2` when we enter nested `section.items`
173
- - `depth = 1` when we exit nested `section.items` (line 98)
174
- - `depth = 0` when we exit `menuSections` (line 483) - **this is our match**
175
-
176
- **Files Modified**: `pre-compiled-manifest.js`
177
-
178
- ---
179
-
180
- ## Edge Cases and Limitations
181
-
182
- ### Known Shortcuts
183
- 1. **Name binding fix**: Post-processing regex instead of proper html5ever configuration
184
- 2. **Comment parsing**: Assumes comments are well-formed (no nested comments in strings)
185
-
186
- ### Will This Work for Most Cases?
187
- **Yes**, the solution is robust for standard use cases:
188
-
189
- ✅ **Works For**:
190
- - Nested iterations at any depth
191
- - Multiple iterations at the same level
192
- - Conditionals inside iterations
193
- - Iterations inside conditionals
194
- - Mixed nesting patterns
195
-
196
- ⚠️ **Edge Cases**:
197
- - Identical iteration expressions at multiple nesting levels (e.g., `<!-- each items as item -->` appears twice in nested structure)
198
- - Solution: Expression matching will find the first one, depth tracking ensures correct pairing
199
- - This is acceptable because the outer iteration would have a different parent element
200
-
201
- - Comments inside JavaScript strings that look like iteration comments
202
- - Example: `const x = "<!-- each items as item -->"` in a `<script>` tag
203
- - This would confuse the comment walker
204
- - Mitigation: Unlikely in practice, as state is extracted separately
205
-
206
- - Name bindings with quotes inside: `<icon @[items["key"]]>`
207
- - The regex fix might not handle all quote variations
208
- - Mitigation: Rare in practice, typically use dot notation
209
-
210
- ### Future Improvements
211
- 1. **Proper html5ever integration**: Configure serializer to not add `=""` for dynamic attributes
212
- 2. **AST-based comment parsing**: Use html5ever's comment nodes instead of regex/text matching
213
- 3. **Unique iteration IDs**: Generate unique IDs for each iteration during compilation, use those for matching instead of expressions
214
-
215
- ## Testing Checklist
216
- When modifying this code, verify:
217
- - [ ] Simple iterations render correctly
218
- - [ ] Nested iterations (2+ levels deep) render correctly
219
- - [ ] Multiple iterations at same level render correctly
220
- - [ ] Pre-rendered HTML shows actual values (no FOUC)
221
- - [ ] Runtime finds markers and makes content reactive
222
- - [ ] Menu navigation works (indicates correct event handlers)
223
- - [ ] Console shows no errors during restoration
224
-
225
- ## File Summary
226
-
227
- | File | Purpose | Key Changes |
228
- |------|---------|-------------|
229
- | `compile.rs` | Main compilation orchestration | Reordered manifest build before value stamping |
230
- | `state_extractor.rs` | Extract state from `<script>` tags | Fixed multiline regex with `(?s)` flag |
231
- | `manifest_builder.rs` | Build restoration manifest | Added expression field, fixed name binding serialization |
232
- | `pre-compiled-manifest.js` | Runtime restoration logic | Added expression matching and depth tracking |
233
-
234
- ## Conclusion
235
-
236
- The pre-rendering system now correctly handles nested iterations by:
237
- 1. Storing iteration expressions for precise matching
238
- 2. Tracking depth to find the correct closing comment
239
- 3. Building manifests before value stamping to preserve markers
240
-
241
- The depth tracking solution is the critical piece that makes nested iterations work correctly. Without it, the restoration would match the wrong closing comment and corrupt the DOM structure.
@@ -1,88 +0,0 @@
1
- use regex::Regex;
2
- use std::collections::HashMap;
3
- use std::sync::OnceLock;
4
-
5
- /// The canonical `@[...]` binding matcher, shared with manifest_builder /
6
- /// value_stamper. A binding body is any run of non-bracket chars, optionally
7
- /// containing a single bracketed group (`items[0]`).
8
- fn binding_regex() -> &'static Regex {
9
- static RE: OnceLock<Regex> = OnceLock::new();
10
- RE.get_or_init(|| Regex::new(r"@\[((?:[^\[\]]|\[[^\]]*\])+)\]").unwrap())
11
- }
12
-
13
- /// html5ever lowercases attribute NAMES per the HTML spec. A name-binding lives
14
- /// in attribute-name position (`<icon @[selectedEquipProps(uuid).element]>`), so
15
- /// its expression is lowercased by every parse/serialize round-trip — the
16
- /// identifier `selectedEquipProps` becomes `selectedequipprops`, undefined at
17
- /// runtime. Attribute VALUES keep their case, so value-bindings are unaffected.
18
- ///
19
- /// We can't tell html5ever to preserve case, so we restore it ourselves: snapshot
20
- /// the original-cased `@[...]` bindings from the input BEFORE the round-trip, then
21
- /// map them back over the output AFTER. The map is keyed by the lowercased binding
22
- /// (what the round-trip produces) → original binding.
23
- ///
24
- /// First-wins on a lowercase collision (two differently-cased bindings that
25
- /// lowercase to the same key): rare, and either casing is a defensible restore.
26
- pub fn capture(input: &str) -> HashMap<String, String> {
27
- let mut map = HashMap::new();
28
- for caps in binding_regex().captures_iter(input) {
29
- let original = caps.get(0).unwrap().as_str();
30
- let lower = original.to_ascii_lowercase();
31
- if lower != original {
32
- map.entry(lower).or_insert_with(|| original.to_string());
33
- }
34
- }
35
- map
36
- }
37
-
38
- /// Restore original casing of `@[...]` bindings in `output` using a map built by
39
- /// [`capture`]. Only bindings whose lowercased form is a known key are rewritten;
40
- /// everything else is left verbatim, so this is a no-op when nothing was lowered.
41
- pub fn restore(output: &str, map: &HashMap<String, String>) -> String {
42
- if map.is_empty() {
43
- return output.to_string();
44
- }
45
- binding_regex()
46
- .replace_all(output, |caps: &regex::Captures| {
47
- let matched = caps.get(0).unwrap().as_str();
48
- match map.get(&matched.to_ascii_lowercase()) {
49
- Some(original) => original.clone(),
50
- None => matched.to_string(),
51
- }
52
- })
53
- .to_string()
54
- }
55
-
56
- #[cfg(test)]
57
- mod tests {
58
- use super::*;
59
-
60
- #[test]
61
- fn restores_lowercased_name_binding() {
62
- // Post-prop-substitution name binding (parens added by substitute_props),
63
- // as it enters an html5ever round-trip. html5ever lowercases the whole
64
- // attr name; restore puts the original casing back.
65
- let input = "<icon @[(selectedEquipProps(uuid)).element]></icon>";
66
- let map = capture(input);
67
- let lowered = "<icon @[(selectedequipprops(uuid)).element]></icon>";
68
- let restored = restore(lowered, &map);
69
- assert_eq!(restored, input);
70
- }
71
-
72
- #[test]
73
- fn leaves_value_bindings_untouched_when_already_correct() {
74
- let input = r#"<card tinted="@[selectedEquipProps(uuid).element]"></card>"#;
75
- let map = capture(input);
76
- // Value bindings keep case through html5ever, so output already matches.
77
- let restored = restore(input, &map);
78
- assert_eq!(restored, input);
79
- }
80
-
81
- #[test]
82
- fn noop_when_no_uppercase() {
83
- let input = "<icon @[tooltip.props.element]></icon>";
84
- let map = capture(input);
85
- assert!(map.is_empty());
86
- assert_eq!(restore(input, &map), input);
87
- }
88
- }