@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.
- package/README.md +14 -4
- package/boot.js +4 -4
- package/component.js +27 -29
- package/hot-module-refresh.js +4 -4
- package/index.js +10 -15
- package/llms.txt +8 -6
- package/package.json +19 -14
- package/runtime/affected.js +159 -36
- package/runtime/cleanup.js +45 -1
- package/runtime/component.js +312 -99
- package/runtime/conditionals.js +111 -14
- package/runtime/debug.js +24 -0
- package/runtime/dispatch.js +172 -0
- package/runtime/hydrate.js +251 -111
- package/runtime/index.js +180 -71
- package/runtime/iterate.js +125 -50
- package/runtime/iteration-utils.js +59 -8
- package/runtime/manifest.js +77 -2
- package/runtime/parse.js +69 -5
- package/runtime/pre-compiled-iterations.js +19 -6
- package/runtime/pre-compiled-manifest.js +13 -4
- package/runtime/staging.js +153 -0
- package/runtime/state.js +31 -0
- package/runtime/tracking.js +173 -0
- package/runtime/utils.js +155 -78
- package/spa.js +77 -14
- package/vibe.css +8 -4
- package/CHANGELOG.md +0 -1196
- package/ROADMAP.md +0 -397
- package/compiler/bin/vibe-compile.js +0 -121
- package/compiler/native/.gitkeep +0 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/Cargo.lock +0 -2023
- package/compiler/src/Cargo.toml +0 -38
- package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +0 -241
- package/compiler/src/compiler/binding_case.rs +0 -88
- package/compiler/src/compiler/compile.rs +0 -2880
- package/compiler/src/compiler/component_tagger.rs +0 -469
- package/compiler/src/compiler/iteration_optimizer.rs +0 -455
- package/compiler/src/compiler/js_analyzer.rs +0 -715
- package/compiler/src/compiler/manifest_builder.rs +0 -693
- package/compiler/src/compiler/mod.rs +0 -16
- package/compiler/src/compiler/name_binding_protect.rs +0 -207
- package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
- package/compiler/src/compiler/spa.rs +0 -477
- package/compiler/src/compiler/state_extractor.rs +0 -263
- package/compiler/src/compiler/value_stamper.rs +0 -921
- package/compiler/src/compiler/watcher.rs +0 -1278
- package/compiler/src/config.rs +0 -279
- package/compiler/src/main.rs +0 -358
- package/compiler/src/parser/element.rs +0 -96
- package/compiler/src/parser/html.rs +0 -1004
- package/compiler/src/parser/mod.rs +0 -8
- package/runtime/pre-compiled-manifest.test.mjs +0 -58
- package/runtime/scope.js +0 -50
- 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
|
-
}
|