@ape-egg/vibe 2.3.0 → 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 (57) hide show
  1. package/README.md +14 -4
  2. package/boot.js +4 -4
  3. package/component.js +27 -29
  4. package/hot-module-refresh.js +4 -4
  5. package/index.js +10 -15
  6. package/llms.txt +8 -6
  7. package/package.json +19 -14
  8. package/runtime/affected.js +159 -36
  9. package/runtime/cleanup.js +45 -1
  10. package/runtime/component.js +312 -99
  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 +251 -111
  15. package/runtime/index.js +180 -71
  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 +69 -5
  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 +77 -14
  27. package/vibe.css +8 -4
  28. package/CHANGELOG.md +0 -1196
  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 -2880
  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 -16
  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/spa.rs +0 -477
  47. package/compiler/src/compiler/state_extractor.rs +0 -263
  48. package/compiler/src/compiler/value_stamper.rs +0 -921
  49. package/compiler/src/compiler/watcher.rs +0 -1278
  50. package/compiler/src/config.rs +0 -279
  51. package/compiler/src/main.rs +0 -358
  52. package/compiler/src/parser/element.rs +0 -96
  53. package/compiler/src/parser/html.rs +0 -1004
  54. package/compiler/src/parser/mod.rs +0 -8
  55. package/runtime/pre-compiled-manifest.test.mjs +0 -58
  56. package/runtime/scope.js +0 -50
  57. package/test-results/.last-run.json +0 -4
@@ -1,263 +0,0 @@
1
- use regex::Regex;
2
- use serde_json::{Value, Map};
3
- use std::path::PathBuf;
4
- use crate::compiler::js_analyzer::JsAnalyzer;
5
-
6
- pub struct StateExtractor;
7
-
8
- impl StateExtractor {
9
- /// Extract state from all vibe() and component() calls in HTML
10
- pub fn extract_from_html(html: &str, base_path: &PathBuf) -> Result<Value, String> {
11
- // Try modern approach: parse script blocks with JsAnalyzer
12
- if let Some(state) = Self::extract_from_scripts(html, base_path) {
13
- return Ok(state);
14
- }
15
-
16
- // Fallback: legacy regex-based extraction for inline object literals
17
- Self::extract_with_regex(html)
18
- }
19
-
20
- /// Extract state from <script> blocks using JavaScript AST analysis
21
- fn extract_from_scripts(html: &str, base_path: &PathBuf) -> Option<Value> {
22
- let script_regex = Regex::new(r#"(?s)<script[^>]*>(.*?)</script>"#).unwrap();
23
- let mut analyzer = JsAnalyzer::new(base_path.clone());
24
-
25
- let mut merged_state = Map::new();
26
-
27
- for cap in script_regex.captures_iter(html) {
28
- let script = cap.get(1)?.as_str();
29
-
30
- // Parse and extract state with AST analysis
31
- if let Some(Value::Object(state)) = analyzer.extract_state(script) {
32
- for (k, v) in state {
33
- merged_state.insert(k, v);
34
- }
35
- }
36
- }
37
-
38
- if merged_state.is_empty() {
39
- None
40
- } else {
41
- Some(Value::Object(merged_state))
42
- }
43
- }
44
-
45
- /// Legacy regex-based extraction (fallback)
46
- fn extract_with_regex(html: &str) -> Result<Value, String> {
47
- let mut merged_state = Map::new();
48
-
49
- // Find vibe({ ... }) and state({ ... }) patterns using brace counting
50
- let start_regex = Regex::new(r"(?:vibe|state)\s*\(\s*\{").unwrap();
51
-
52
- for start_match in start_regex.find_iter(html) {
53
- if let Some(obj_literal) = Self::extract_balanced_object(&html[start_match.end()..]) {
54
- match Self::parse_object_literal(&obj_literal) {
55
- Ok(state) => Self::merge_into(&mut merged_state, state),
56
- Err(_) => continue, // Skip unparseable state
57
- }
58
- }
59
- }
60
-
61
- // Find component({ ... }) patterns
62
- let component_start_regex = Regex::new(r"component\s*\(\s*\{").unwrap();
63
-
64
- for start_match in component_start_regex.find_iter(html) {
65
- if let Some(obj_literal) = Self::extract_balanced_object(&html[start_match.end()..]) {
66
- match Self::parse_object_literal(&obj_literal) {
67
- Ok(component_state) => Self::merge_into(&mut merged_state, component_state),
68
- Err(_) => continue,
69
- }
70
- }
71
- }
72
-
73
- Ok(Value::Object(merged_state))
74
- }
75
-
76
- /// Extract balanced object literal content (everything between { and matching })
77
- /// Input should start right after the opening {
78
- fn extract_balanced_object(s: &str) -> Option<String> {
79
- let mut depth = 1;
80
- let mut in_string = false;
81
- let mut escape_next = false;
82
- let mut quote_char = '\0';
83
- let mut result = String::new();
84
-
85
- for ch in s.chars() {
86
- if escape_next {
87
- result.push(ch);
88
- escape_next = false;
89
- continue;
90
- }
91
-
92
- if ch == '\\' {
93
- result.push(ch);
94
- escape_next = true;
95
- continue;
96
- }
97
-
98
- if !in_string {
99
- if ch == '"' || ch == '\'' || ch == '`' {
100
- in_string = true;
101
- quote_char = ch;
102
- result.push(ch);
103
- } else if ch == '{' || ch == '[' {
104
- depth += 1;
105
- result.push(ch);
106
- } else if ch == '}' || ch == ']' {
107
- depth -= 1;
108
- if depth == 0 {
109
- return Some(result);
110
- }
111
- result.push(ch);
112
- } else {
113
- result.push(ch);
114
- }
115
- } else {
116
- result.push(ch);
117
- if ch == quote_char {
118
- in_string = false;
119
- }
120
- }
121
- }
122
-
123
- None
124
- }
125
-
126
- /// Parse JavaScript object literal to JSON Value
127
- fn parse_object_literal(js: &str) -> Result<Map<String, Value>, String> {
128
- // 1. Normalize to valid JSON
129
- let normalized = Self::normalize_js_to_json(js);
130
-
131
- // 2. Parse as JSON
132
- serde_json::from_str::<Value>(&normalized)
133
- .map_err(|e| format!("Failed to parse state: {}", e))?
134
- .as_object()
135
- .cloned()
136
- .ok_or_else(|| "Expected object".to_string())
137
- }
138
-
139
- /// Convert JS object literal syntax to valid JSON
140
- fn normalize_js_to_json(js: &str) -> String {
141
- let mut json = js.trim().to_string();
142
-
143
- // Convert single quotes to double quotes (before key quoting to avoid conflicts)
144
- json = json.replace("'", "\"");
145
-
146
- // Quote unquoted keys: name: value -> "name": value
147
- // Match at start of string or after { , or whitespace
148
- let key_regex = Regex::new(r#"(^|[\{,]\s*)(\w+)(\s*:)"#).unwrap();
149
- json = key_regex.replace_all(&json, r#"$1"$2"$3"#).to_string();
150
-
151
- // Wrap in braces
152
- let wrapped = format!("{{{}}}", json);
153
-
154
- // Remove trailing commas before } and ] (after wrapping)
155
- let trailing_comma_regex = Regex::new(r#",(\s*[}\]])"#).unwrap();
156
- trailing_comma_regex.replace_all(&wrapped, "$1").to_string()
157
- }
158
-
159
- /// Merge source into target (mutates target)
160
- fn merge_into(target: &mut Map<String, Value>, source: Map<String, Value>) {
161
- for (key, value) in source {
162
- target.insert(key, value);
163
- }
164
- }
165
- }
166
-
167
- #[cfg(test)]
168
- mod tests {
169
- use super::*;
170
-
171
- #[test]
172
- fn extract_simple_state() {
173
- let html = r#"<script>vibe({ count: 0 })</script>"#;
174
- let state = StateExtractor::extract_from_html(html, &PathBuf::from(".")).unwrap();
175
- assert_eq!(state["count"], 0);
176
- }
177
-
178
- #[test]
179
- fn extract_nested_state() {
180
- let html = r#"vibe({ user: { name: 'John', age: 30 } })"#;
181
- let state = StateExtractor::extract_from_html(html, &PathBuf::from(".")).unwrap();
182
- assert_eq!(state["user"]["name"], "John");
183
- assert_eq!(state["user"]["age"], 30);
184
- }
185
-
186
- #[test]
187
- fn extract_array_state() {
188
- let html = r#"vibe({ items: [1, 2, 3] })"#;
189
- let state = StateExtractor::extract_from_html(html, &PathBuf::from(".")).unwrap();
190
- assert_eq!(state["items"][0], 1);
191
- assert_eq!(state["items"][1], 2);
192
- assert_eq!(state["items"][2], 3);
193
- }
194
-
195
- #[test]
196
- fn extract_multiple_calls() {
197
- let html = r#"
198
- <script>vibe({ count: 0 })</script>
199
- <script>vibe({ name: 'Test' })</script>
200
- "#;
201
- let state = StateExtractor::extract_from_html(html, &PathBuf::from(".")).unwrap();
202
- assert_eq!(state["count"], 0);
203
- assert_eq!(state["name"], "Test");
204
- }
205
-
206
- #[test]
207
- fn extract_with_trailing_comma() {
208
- let html = r#"vibe({ count: 0, })"#;
209
- let state = StateExtractor::extract_from_html(html, &PathBuf::from(".")).unwrap();
210
- assert_eq!(state["count"], 0);
211
- }
212
-
213
- #[test]
214
- fn extract_component_state() {
215
- let html = r#"component({ title: 'Hello' })"#;
216
- let state = StateExtractor::extract_from_html(html, &PathBuf::from(".")).unwrap();
217
- assert_eq!(state["title"], "Hello");
218
- }
219
-
220
- #[test]
221
- fn normalize_unquoted_keys() {
222
- let js = "count: 0, name: 'Test'";
223
- let normalized = StateExtractor::normalize_js_to_json(js);
224
- eprintln!("Normalized: {}", normalized);
225
- assert!(normalized.contains(r#""count""#));
226
- assert!(normalized.contains(r#""name""#));
227
- }
228
-
229
- #[test]
230
- fn normalize_single_quotes() {
231
- let js = "name: 'John'";
232
- let normalized = StateExtractor::normalize_js_to_json(js);
233
- assert!(normalized.contains(r#""John""#));
234
- }
235
-
236
- #[test]
237
- fn extract_array_of_objects() {
238
- let html = r#"vibe({
239
- categories: [
240
- { name: 'Fruits', items: ['Apple', 'Banana'] },
241
- { name: 'Veggies', items: ['Carrot'] }
242
- ]
243
- })"#;
244
- let state = StateExtractor::extract_from_html(html, &PathBuf::from(".")).unwrap();
245
- assert_eq!(state["categories"][0]["name"], "Fruits");
246
- assert_eq!(state["categories"][0]["items"][0], "Apple");
247
- assert_eq!(state["categories"][1]["name"], "Veggies");
248
- }
249
-
250
- #[test]
251
- fn extract_balanced_object_simple() {
252
- let input = "count: 0 }";
253
- let result = StateExtractor::extract_balanced_object(input).unwrap();
254
- assert_eq!(result, "count: 0 ");
255
- }
256
-
257
- #[test]
258
- fn extract_balanced_object_nested() {
259
- let input = "user: { name: 'John', items: [1, 2, 3] } }";
260
- let result = StateExtractor::extract_balanced_object(input).unwrap();
261
- assert_eq!(result, "user: { name: 'John', items: [1, 2, 3] } ");
262
- }
263
- }