@ape-egg/vibe 2.3.0 → 3.0.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/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,469 +0,0 @@
|
|
|
1
|
-
use html5ever::parse_document;
|
|
2
|
-
use html5ever::tendril::TendrilSink;
|
|
3
|
-
use html5ever::serialize::{serialize, SerializeOpts};
|
|
4
|
-
use markup5ever_rcdom::{RcDom, NodeData, Handle, Node, SerializableHandle};
|
|
5
|
-
use markup5ever::{QualName, LocalName, Namespace};
|
|
6
|
-
use regex::Regex;
|
|
7
|
-
use serde_json::Value;
|
|
8
|
-
use std::collections::HashSet;
|
|
9
|
-
use std::path::PathBuf;
|
|
10
|
-
use std::cell::RefCell;
|
|
11
|
-
use std::sync::OnceLock;
|
|
12
|
-
use crate::compiler::state_extractor::StateExtractor;
|
|
13
|
-
|
|
14
|
-
/// Matches a `component(...)` state-registration call in a component's inline
|
|
15
|
-
/// script. The `\b` keeps it from firing on `subcomponent(`; resolvable or not,
|
|
16
|
-
/// its presence is what tells the tagger this wrapper owns component-local state.
|
|
17
|
-
fn component_call_regex() -> &'static Regex {
|
|
18
|
-
static RE: OnceLock<Regex> = OnceLock::new();
|
|
19
|
-
RE.get_or_init(|| Regex::new(r"\bcomponent\s*\(").unwrap())
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
pub struct ComponentTagger;
|
|
23
|
-
|
|
24
|
-
pub struct TaggedResult {
|
|
25
|
-
pub html: String,
|
|
26
|
-
pub state: Value,
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
impl ComponentTagger {
|
|
30
|
-
/// Find component wrappers, add deterministic IDs, and structure state
|
|
31
|
-
pub fn tag_components(html: &str, base_path: &PathBuf) -> Result<TaggedResult, String> {
|
|
32
|
-
// Extract ALL <template> content to protect it from HTML parser
|
|
33
|
-
// HTML parsers can strip content from <template> tags during serialization
|
|
34
|
-
let template_regex = regex::Regex::new(r"(?s)<template[^>]*>.*?</template>").unwrap();
|
|
35
|
-
let mut template_placeholders: Vec<String> = Vec::new();
|
|
36
|
-
let mut html_with_placeholders = html.to_string();
|
|
37
|
-
|
|
38
|
-
for (i, mat) in template_regex.find_iter(html).enumerate() {
|
|
39
|
-
let content = mat.as_str();
|
|
40
|
-
let placeholder = format!("<!--VIBE_TEMPLATE_PLACEHOLDER_{}-->", i);
|
|
41
|
-
template_placeholders.push(content.to_string());
|
|
42
|
-
html_with_placeholders = html_with_placeholders.replace(content, &placeholder);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Snapshot original-cased `@[...]` bindings before html5ever lowercases
|
|
46
|
-
// any that live in attribute-name position (name bindings); restored after
|
|
47
|
-
// serialization below.
|
|
48
|
-
let binding_cases = crate::compiler::binding_case::capture(&html_with_placeholders);
|
|
49
|
-
|
|
50
|
-
// Parse HTML (with placeholders instead of actual vibe-dehydrate content)
|
|
51
|
-
let dom = parse_document(RcDom::default(), Default::default())
|
|
52
|
-
.from_utf8()
|
|
53
|
-
.read_from(&mut html_with_placeholders.as_bytes())
|
|
54
|
-
.map_err(|e| format!("Failed to parse HTML: {:?}", e))?;
|
|
55
|
-
|
|
56
|
-
// Find all component wrappers and extract their state
|
|
57
|
-
let component_counter = RefCell::new(0);
|
|
58
|
-
let component_states = RefCell::new(Vec::new());
|
|
59
|
-
Self::walk_tag_and_extract(&dom.document, &component_counter, &component_states, html);
|
|
60
|
-
|
|
61
|
-
// Serialize back to HTML
|
|
62
|
-
let mut modified_html_bytes = Vec::new();
|
|
63
|
-
serialize(
|
|
64
|
-
&mut modified_html_bytes,
|
|
65
|
-
&SerializableHandle::from(dom.document.clone()),
|
|
66
|
-
SerializeOpts::default()
|
|
67
|
-
).map_err(|e| format!("Failed to serialize HTML: {:?}", e))?;
|
|
68
|
-
|
|
69
|
-
let mut modified_html = String::from_utf8(modified_html_bytes)
|
|
70
|
-
.map_err(|e| format!("Failed to convert HTML to UTF-8: {}", e))?;
|
|
71
|
-
|
|
72
|
-
// Restore original casing of name-binding expressions html5ever lowercased.
|
|
73
|
-
modified_html = crate::compiler::binding_case::restore(&modified_html, &binding_cases);
|
|
74
|
-
|
|
75
|
-
// Restore template content from placeholders
|
|
76
|
-
for (i, content) in template_placeholders.iter().enumerate() {
|
|
77
|
-
let placeholder = format!("<!--VIBE_TEMPLATE_PLACEHOLDER_{}-->", i);
|
|
78
|
-
modified_html = modified_html.replace(&placeholder, content);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Extract all state from HTML
|
|
82
|
-
let all_state = StateExtractor::extract_from_html(html, base_path)?;
|
|
83
|
-
|
|
84
|
-
// Build a set of all keys that are in component states
|
|
85
|
-
let mut component_keys = HashSet::new();
|
|
86
|
-
for (_, comp_state) in component_states.borrow().iter() {
|
|
87
|
-
if let Value::Object(comp_map) = comp_state {
|
|
88
|
-
for key in comp_map.keys() {
|
|
89
|
-
component_keys.insert(key.clone());
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Filter global state to only include keys NOT in component states
|
|
95
|
-
let mut merged_state = if let Value::Object(all_map) = all_state {
|
|
96
|
-
let mut global_only = serde_json::Map::new();
|
|
97
|
-
for (key, value) in all_map {
|
|
98
|
-
// Only include in global if it's not a component state key
|
|
99
|
-
if !component_keys.contains(&key) {
|
|
100
|
-
global_only.insert(key, value);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
global_only
|
|
104
|
-
} else {
|
|
105
|
-
serde_json::Map::new()
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// Add component states under their IDs
|
|
109
|
-
for (component_id, comp_state) in component_states.borrow().iter() {
|
|
110
|
-
if let Value::Object(comp_map) = comp_state {
|
|
111
|
-
// Only add if component has non-empty state
|
|
112
|
-
if !comp_map.is_empty() {
|
|
113
|
-
merged_state.insert(component_id.clone(), comp_state.clone());
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let merged_state = Value::Object(merged_state);
|
|
119
|
-
|
|
120
|
-
Ok(TaggedResult {
|
|
121
|
-
html: modified_html,
|
|
122
|
-
state: merged_state,
|
|
123
|
-
})
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/// Recursively walk DOM, add data-vibe-component-id, and extract component state
|
|
127
|
-
fn walk_tag_and_extract(
|
|
128
|
-
node: &Handle,
|
|
129
|
-
counter: &RefCell<usize>,
|
|
130
|
-
component_states: &RefCell<Vec<(String, Value)>>,
|
|
131
|
-
original_html: &str,
|
|
132
|
-
) {
|
|
133
|
-
// Check if this is a component wrapper
|
|
134
|
-
if let NodeData::Element { name, attrs, .. } = &node.data {
|
|
135
|
-
let tag_name = name.local.as_ref();
|
|
136
|
-
let borrowed_attrs = attrs.borrow();
|
|
137
|
-
|
|
138
|
-
// Check if it's <component> (without src) or <div class="component"> (without src)
|
|
139
|
-
let is_component = tag_name == "component" && !Self::has_src_attr(&borrowed_attrs);
|
|
140
|
-
let is_div_component = tag_name == "div"
|
|
141
|
-
&& Self::has_class_component(&borrowed_attrs)
|
|
142
|
-
&& !Self::has_src_attr(&borrowed_attrs);
|
|
143
|
-
|
|
144
|
-
if is_component || is_div_component {
|
|
145
|
-
drop(borrowed_attrs); // Release borrow before checking innerHTML
|
|
146
|
-
|
|
147
|
-
// Only extract state from DIRECT <script> children — not the full subtree.
|
|
148
|
-
// Using the full subtree caused the Layout wrapper (a <component> created by
|
|
149
|
-
// inline_component_elements that contains all nested component scripts) to be
|
|
150
|
-
// treated as a stateful component with the merged state of all its descendants.
|
|
151
|
-
// That made rewrite_this_to_component_id rewrite every @[this.xxx] in the page
|
|
152
|
-
// to @[_c0.xxx] before child components could claim their own bindings.
|
|
153
|
-
let direct_scripts_html = Self::direct_scripts_html(node);
|
|
154
|
-
|
|
155
|
-
// A component must be tagged whenever it REGISTERS local state via a
|
|
156
|
-
// `component(...)` call — that is what makes `this.X` in its markup
|
|
157
|
-
// resolvable at runtime. Whether the initial state VALUES are statically
|
|
158
|
-
// resolvable is a separate concern (it only governs FOUC value-stamping).
|
|
159
|
-
// A state built from a runtime expression — e.g.
|
|
160
|
-
// `component({ levels: Array.from({ length: 25 }, ...) })` — resolves to
|
|
161
|
-
// an empty object here, but the component still needs its id + `this.`
|
|
162
|
-
// rewrite, or a compiled `<!-- each this.levels -->` ships a raw `this.`
|
|
163
|
-
// the runtime can't resolve when a restored conditional branch re-parses
|
|
164
|
-
// the template (no wrapper-tag ancestor to fall back to).
|
|
165
|
-
let registers_state = component_call_regex().is_match(&direct_scripts_html);
|
|
166
|
-
|
|
167
|
-
if registers_state {
|
|
168
|
-
let comp_state = match StateExtractor::extract_from_html(
|
|
169
|
-
&direct_scripts_html,
|
|
170
|
-
&PathBuf::from(".")
|
|
171
|
-
) {
|
|
172
|
-
Ok(Value::Object(state)) => state,
|
|
173
|
-
_ => serde_json::Map::new(),
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
let component_id = format!("_c{}", *counter.borrow());
|
|
177
|
-
*counter.borrow_mut() += 1;
|
|
178
|
-
|
|
179
|
-
// Add data-vibe-component-id attribute
|
|
180
|
-
let mut attrs_mut = attrs.borrow_mut();
|
|
181
|
-
attrs_mut.push(markup5ever::Attribute {
|
|
182
|
-
name: QualName::new(
|
|
183
|
-
None,
|
|
184
|
-
Namespace::from(""),
|
|
185
|
-
LocalName::from("data-vibe-component-id"),
|
|
186
|
-
),
|
|
187
|
-
value: component_id.clone().into(),
|
|
188
|
-
});
|
|
189
|
-
drop(attrs_mut);
|
|
190
|
-
|
|
191
|
-
// Rewrite this.property to componentId.property in the node's children
|
|
192
|
-
// This allows ValueStamper to properly evaluate component-scoped expressions
|
|
193
|
-
Self::rewrite_this_to_component_id(node, &component_id);
|
|
194
|
-
|
|
195
|
-
// Register any statically-resolvable initial state under the id. May be
|
|
196
|
-
// empty (runtime-built state); the runtime fills it in when the inlined
|
|
197
|
-
// `component(...)` call runs on mount.
|
|
198
|
-
component_states.borrow_mut().push((component_id, Value::Object(comp_state)));
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Recurse into children
|
|
204
|
-
for child in node.children.borrow().iter() {
|
|
205
|
-
Self::walk_tag_and_extract(child, counter, component_states, original_html);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/// Check if element has src attribute
|
|
210
|
-
fn has_src_attr(attrs: &[markup5ever::Attribute]) -> bool {
|
|
211
|
-
attrs.iter().any(|attr| attr.name.local.as_ref() == "src")
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/// Check if element has class="component"
|
|
215
|
-
fn has_class_component(attrs: &[markup5ever::Attribute]) -> bool {
|
|
216
|
-
attrs.iter().any(|attr| {
|
|
217
|
-
attr.name.local.as_ref() == "class"
|
|
218
|
-
&& attr.value.as_ref().split_whitespace().any(|c| c == "component")
|
|
219
|
-
})
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/// Concatenate a node's DIRECT `<script>` children as `<script>…</script>`.
|
|
223
|
-
/// Reads raw text (not serialized) so JS operators (`>`, `&&`) aren't
|
|
224
|
-
/// HTML-escaped — the escaped source would fail the AST parse and drop a
|
|
225
|
-
/// `component(stateVar)` call to the regex fallback. Used both to extract a
|
|
226
|
-
/// wrapper's own state and to detect nested component boundaries.
|
|
227
|
-
fn direct_scripts_html(node: &Handle) -> String {
|
|
228
|
-
let mut out = String::new();
|
|
229
|
-
for child in node.children.borrow().iter() {
|
|
230
|
-
if let NodeData::Element { name: ref child_name, .. } = child.data {
|
|
231
|
-
if child_name.local.as_ref() == "script" {
|
|
232
|
-
let mut script_text = String::new();
|
|
233
|
-
for grandchild in child.children.borrow().iter() {
|
|
234
|
-
if let NodeData::Text { ref contents } = grandchild.data {
|
|
235
|
-
script_text.push_str(&contents.borrow());
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
out.push_str("<script>");
|
|
239
|
-
out.push_str(&script_text);
|
|
240
|
-
out.push_str("</script>");
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
out
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/// Is this node a component wrapper that registers its OWN local state? Such
|
|
248
|
-
/// a node gets its own `_cN` id and `this.`→id pass when `walk_tag_and_extract`
|
|
249
|
-
/// reaches it, so an ancestor's rewrite must stop here — descending would let
|
|
250
|
-
/// the ancestor claim the nested component's bindings with the wrong id (the
|
|
251
|
-
/// DebugContent→ScalingModal empty-legend bug).
|
|
252
|
-
fn is_state_registering_component(node: &Handle) -> bool {
|
|
253
|
-
if let NodeData::Element { name, attrs, .. } = &node.data {
|
|
254
|
-
let tag_name = name.local.as_ref();
|
|
255
|
-
let borrowed = attrs.borrow();
|
|
256
|
-
let is_component = tag_name == "component" && !Self::has_src_attr(&borrowed);
|
|
257
|
-
let is_div_component = tag_name == "div"
|
|
258
|
-
&& Self::has_class_component(&borrowed)
|
|
259
|
-
&& !Self::has_src_attr(&borrowed);
|
|
260
|
-
if !(is_component || is_div_component) {
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
drop(borrowed);
|
|
264
|
-
return component_call_regex().is_match(&Self::direct_scripts_html(node));
|
|
265
|
-
}
|
|
266
|
-
false
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/// Rewrite `this.X` to `componentId.X` throughout a component's subtree:
|
|
270
|
-
/// inside `@[...]` bindings (text + attributes, nested paths and multi-ref
|
|
271
|
-
/// expressions) and inside if/each/else-if directive comments. Resolving the
|
|
272
|
-
/// directives at build time is what lets a component-local `<!-- if this.x -->`
|
|
273
|
-
/// work on compiled (manifest-restored) pages, where the runtime can't fall
|
|
274
|
-
/// back to a `data-vibe-component-id` ancestor lookup.
|
|
275
|
-
fn rewrite_this_to_component_id(node: &Handle, component_id: &str) {
|
|
276
|
-
use regex::Regex;
|
|
277
|
-
// The whole `@[...]` binding; `this.` is resolved within it so nested
|
|
278
|
-
// paths (this.x.y) and expressions (this.a + this.b) are all covered.
|
|
279
|
-
let binding_regex = Regex::new(r"@\[[^\]]*\]").unwrap();
|
|
280
|
-
let this_prop = Regex::new(r"\bthis\.").unwrap();
|
|
281
|
-
// `$.this.X` writes that live in event-handler bodies (onclick="$.this.mode
|
|
282
|
-
// = 'edit'"), outside any `@[...]`. The runtime lowers these via its
|
|
283
|
-
// STATE_THIS_PROP_REGEX pass (runtime/component.js); the compiler must do
|
|
284
|
-
// the same or compiled pages ship a literal `$.this.X` that resolves to
|
|
285
|
-
// undefined when the native handler fires.
|
|
286
|
-
let state_this_prop = Regex::new(r"\$\.this\.(\w+)").unwrap();
|
|
287
|
-
Self::rewrite_node_recursive(node, &binding_regex, &this_prop, &state_this_prop, component_id);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/// Resolve `this.` to `componentId.` inside every `@[...]` binding in a string.
|
|
291
|
-
fn rewrite_bindings(s: &str, binding_regex: &Regex, this_prop: &Regex, component_id: &str) -> String {
|
|
292
|
-
let replacement = format!("{}.", component_id);
|
|
293
|
-
binding_regex
|
|
294
|
-
.replace_all(s, |caps: ®ex::Captures| {
|
|
295
|
-
this_prop.replace_all(&caps[0], replacement.as_str()).to_string()
|
|
296
|
-
})
|
|
297
|
-
.to_string()
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/// Recursively rewrite this.property in text, attributes, and directive comments
|
|
301
|
-
fn rewrite_node_recursive(node: &Handle, binding_regex: &Regex, this_prop: &Regex, state_this_prop: &Regex, component_id: &str) {
|
|
302
|
-
// Rewrite text content bindings
|
|
303
|
-
if let NodeData::Text { ref contents } = node.data {
|
|
304
|
-
let mut text = contents.borrow_mut();
|
|
305
|
-
let new_text = Self::rewrite_bindings(&text, binding_regex, this_prop, component_id);
|
|
306
|
-
*text = new_text.into();
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Rewrite attribute bindings (`@[...]`) and event-handler `$.this.X` writes.
|
|
310
|
-
if let NodeData::Element { ref attrs, .. } = node.data {
|
|
311
|
-
let mut attrs_mut = attrs.borrow_mut();
|
|
312
|
-
for attr in attrs_mut.iter_mut() {
|
|
313
|
-
let mut new_value = Self::rewrite_bindings(&attr.value, binding_regex, this_prop, component_id);
|
|
314
|
-
if new_value.contains("$.this.") {
|
|
315
|
-
new_value = state_this_prop
|
|
316
|
-
.replace_all(&new_value, |c: ®ex::Captures| format!("$.{}.{}", component_id, &c[1]))
|
|
317
|
-
.to_string();
|
|
318
|
-
}
|
|
319
|
-
attr.value = new_value.into();
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Directive comments are immutable in the RcDom, so swap any if/each/else-if
|
|
324
|
-
// comment carrying a `this.` expression for a freshly-built comment node.
|
|
325
|
-
{
|
|
326
|
-
let mut children = node.children.borrow_mut();
|
|
327
|
-
for child in children.iter_mut() {
|
|
328
|
-
if let NodeData::Comment { ref contents } = child.data {
|
|
329
|
-
let text = contents.to_string();
|
|
330
|
-
let trimmed = text.trim_start();
|
|
331
|
-
let is_directive = trimmed.starts_with("if ")
|
|
332
|
-
|| trimmed.starts_with("each ")
|
|
333
|
-
|| trimmed.starts_with("else if ");
|
|
334
|
-
if is_directive && text.contains("this.") {
|
|
335
|
-
let new_text = this_prop
|
|
336
|
-
.replace_all(&text, format!("{}.", component_id).as_str())
|
|
337
|
-
.to_string();
|
|
338
|
-
*child = Node::new(NodeData::Comment { contents: new_text.into() });
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Recurse into children — but STOP at a nested component that registers
|
|
345
|
-
// its own state. It owns the `this.` inside it and gets its own id +
|
|
346
|
-
// rewrite when walk_tag_and_extract reaches it; descending here would
|
|
347
|
-
// claim its bindings with this ancestor's id (the empty-legend bug).
|
|
348
|
-
for child in node.children.borrow().iter() {
|
|
349
|
-
if Self::is_state_registering_component(child) {
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
Self::rewrite_node_recursive(child, binding_regex, this_prop, state_this_prop, component_id);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
#[cfg(test)]
|
|
358
|
-
mod tests {
|
|
359
|
-
use super::*;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
#[test]
|
|
366
|
-
fn tag_single_component() {
|
|
367
|
-
let html = r#"<!DOCTYPE html><html><body><component><script>component({ count: 0 })</script><div>@[this.count]</div></component></body></html>"#;
|
|
368
|
-
|
|
369
|
-
let result = ComponentTagger::tag_components(html, &PathBuf::from(".")).unwrap();
|
|
370
|
-
|
|
371
|
-
// Should have data-vibe-component-id in output
|
|
372
|
-
assert!(result.html.contains("data-vibe-component-id=\"_c0\""));
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
#[test]
|
|
376
|
-
fn rewrites_this_in_directive_comments_and_nested_bindings() {
|
|
377
|
-
// Conditionals/iterations keep `this.` in their directive comments and
|
|
378
|
-
// bindings can be nested (this.x.y). Compiled (manifest-restored) pages
|
|
379
|
-
// can't resolve `this.` via a runtime wrapper-tag lookup, so the compiler
|
|
380
|
-
// must resolve it to the component id at build time.
|
|
381
|
-
let html = r#"<!DOCTYPE html><html><body><component><script>const s = { x: null }; component(s);</script><page-content><!-- if this.x --><span>@[this.x.name]</span><!-- /if --><!-- each this.items as it --><b>@[it]</b><!-- /each --></page-content></component></body></html>"#;
|
|
382
|
-
|
|
383
|
-
let result = ComponentTagger::tag_components(html, &PathBuf::from(".")).unwrap();
|
|
384
|
-
|
|
385
|
-
assert!(result.html.contains("if _c0.x"), "if comment not rewritten: {}", result.html);
|
|
386
|
-
assert!(result.html.contains("@[_c0.x.name]"), "nested binding not rewritten: {}", result.html);
|
|
387
|
-
assert!(result.html.contains("each _c0.items as it"), "each comment not rewritten: {}", result.html);
|
|
388
|
-
// A non-this global expression must be left alone.
|
|
389
|
-
assert!(!result.html.contains("_c0.items as _c0"), "over-rewrote loop alias: {}", result.html);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
#[test]
|
|
393
|
-
fn tag_component_called_with_variable() {
|
|
394
|
-
// BrawlerDetailContent shape: state held in a `const`, passed to
|
|
395
|
-
// component(state) as a bare identifier, with a top-level
|
|
396
|
-
// `<!-- if this.X -->`. The wrapper must still be tagged so the
|
|
397
|
-
// conditional can resolve `this.` at runtime.
|
|
398
|
-
let html = r#"<!DOCTYPE html><html><body><component><script>const state = { currentCharacter: null }; component(state);</script><page-content><!-- if this.currentCharacter --><span>@[this.currentCharacter]</span><!-- /if --></page-content></component></body></html>"#;
|
|
399
|
-
|
|
400
|
-
let result = ComponentTagger::tag_components(html, &PathBuf::from(".")).unwrap();
|
|
401
|
-
|
|
402
|
-
assert!(
|
|
403
|
-
result.html.contains("data-vibe-component-id=\"_c0\""),
|
|
404
|
-
"wrapper not tagged; html: {}",
|
|
405
|
-
result.html
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
#[test]
|
|
410
|
-
fn tags_component_with_runtime_built_state() {
|
|
411
|
-
// AccountProgression shape: the ONLY state key is built from a runtime
|
|
412
|
-
// expression (`Array.from`), so static extraction resolves to an empty
|
|
413
|
-
// object. The wrapper must still be tagged and its `<!-- each this.X -->`
|
|
414
|
-
// rewritten — otherwise a restored conditional branch re-parses a raw
|
|
415
|
-
// `this.levels` the runtime can't resolve, and the loop renders nothing.
|
|
416
|
-
let html = r#"<!DOCTYPE html><html><body><component><script>component({ levels: Array.from({ length: 25 }, (_, i) => i + 1) });</script><slides><!-- each this.levels as lvl --><slide>@[lvl]</slide><!-- /each --></slides></component></body></html>"#;
|
|
417
|
-
|
|
418
|
-
let result = ComponentTagger::tag_components(html, &PathBuf::from(".")).unwrap();
|
|
419
|
-
|
|
420
|
-
assert!(
|
|
421
|
-
result.html.contains("data-vibe-component-id=\"_c0\""),
|
|
422
|
-
"runtime-built-state component not tagged: {}",
|
|
423
|
-
result.html
|
|
424
|
-
);
|
|
425
|
-
assert!(
|
|
426
|
-
result.html.contains("each _c0.levels as lvl"),
|
|
427
|
-
"each comment not rewritten: {}",
|
|
428
|
-
result.html
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
#[test]
|
|
433
|
-
fn nested_component_this_resolves_to_own_id() {
|
|
434
|
-
// DebugContent → ScalingModal shape: a stateful component nested inside
|
|
435
|
-
// another stateful component, each owning its own `this.`. The outer's
|
|
436
|
-
// this.→id rewrite must STOP at the inner component boundary — otherwise
|
|
437
|
-
// it claims the inner's bindings/directives with the OUTER id before the
|
|
438
|
-
// inner is reached, and the inner's live state (registered under its own
|
|
439
|
-
// runtime id) never reaches the markup → empty each-loops, dead bindings.
|
|
440
|
-
let html = r#"<!DOCTYPE html><html><body><component><script>component({ outer: 1 })</script><outer-mark>@[this.outer]</outer-mark><component><script>component({ inner: 2, items: [] })</script><modal-root open="@[this.open]"><!-- each this.items as it --><inner-mark>@[this.inner]</inner-mark><!-- /each --></modal-root></component></component></body></html>"#;
|
|
441
|
-
|
|
442
|
-
let result = ComponentTagger::tag_components(html, &PathBuf::from(".")).unwrap();
|
|
443
|
-
|
|
444
|
-
// Outer is _c0 (visited first), inner is _c1 (reached on recursion).
|
|
445
|
-
assert!(result.html.contains("@[_c0.outer]"), "outer binding wrong: {}", result.html);
|
|
446
|
-
assert!(result.html.contains("@[_c1.inner]"), "inner binding not rewritten to own id: {}", result.html);
|
|
447
|
-
assert!(result.html.contains("each _c1.items as it"), "inner each not rewritten to own id: {}", result.html);
|
|
448
|
-
assert!(result.html.contains("open=\"@[_c1.open]\""), "inner attr binding not rewritten to own id: {}", result.html);
|
|
449
|
-
// The outer must NOT have claimed any of the inner's bindings/directives.
|
|
450
|
-
assert!(!result.html.contains("@[_c0.inner]"), "outer claimed inner text binding: {}", result.html);
|
|
451
|
-
assert!(!result.html.contains("each _c0.items"), "outer claimed inner each: {}", result.html);
|
|
452
|
-
assert!(!result.html.contains("@[_c0.open]"), "outer claimed inner attr binding: {}", result.html);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
#[test]
|
|
456
|
-
fn tag_multiple_components() {
|
|
457
|
-
// Each wrapper registers component-local state, so all three are tagged
|
|
458
|
-
// with sequential ids. (A bare `<component></component>` with no state
|
|
459
|
-
// call is intentionally NOT tagged — it owns no `this.` to resolve.)
|
|
460
|
-
let html = r#"<!DOCTYPE html><html><body><component><script>component({ a: 1 })</script></component><component><script>component({ b: 2 })</script></component><component><script>component({ c: 3 })</script></component></body></html>"#;
|
|
461
|
-
|
|
462
|
-
let result = ComponentTagger::tag_components(html, &PathBuf::from(".")).unwrap();
|
|
463
|
-
|
|
464
|
-
// Should have _c0, _c1, _c2
|
|
465
|
-
assert!(result.html.contains("data-vibe-component-id=\"_c0\""));
|
|
466
|
-
assert!(result.html.contains("data-vibe-component-id=\"_c1\""));
|
|
467
|
-
assert!(result.html.contains("data-vibe-component-id=\"_c2\""));
|
|
468
|
-
}
|
|
469
|
-
}
|