@ape-egg/vibe 1.2.0 → 1.3.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.
@@ -0,0 +1,222 @@
1
+ use regex::{Regex, Captures};
2
+ use serde_json::Value;
3
+
4
+ pub struct ValueStamper<'a> {
5
+ state: &'a Value,
6
+ binding_regex: Regex,
7
+ iteration_regex: Regex,
8
+ }
9
+
10
+ impl<'a> ValueStamper<'a> {
11
+ pub fn new(state: &'a Value) -> Self {
12
+ Self {
13
+ state,
14
+ binding_regex: Regex::new(r"@\[((?:[^\[\]]|\[[^\]]*\])+)\]").unwrap(),
15
+ iteration_regex: Regex::new(r"(?s)<!--\s*each\s+(\w+(?:\.\w+)*)\s+as\s+(\w+)(?:\s*,\s*(\w+))?\s*-->(.*?)<!--\s*/each\s*-->").unwrap(),
16
+ }
17
+ }
18
+
19
+ pub fn stamp_html(&self, html: String) -> Result<String, String> {
20
+ // First, render iterations (expands templates into multiple instances)
21
+ let html = self.render_iterations(html)?;
22
+
23
+ // Then stamp all bindings (including those in rendered iterations)
24
+ Ok(self.binding_regex.replace_all(&html, |caps: &Captures| {
25
+ let path = &caps[1];
26
+ self.resolve_state_path(path)
27
+ .unwrap_or_else(|| caps[0].to_string()) // Keep marker if can't resolve
28
+ }).to_string())
29
+ }
30
+
31
+ fn render_iterations(&self, html: String) -> Result<String, String> {
32
+ let result = self.iteration_regex.replace_all(&html, |caps: &Captures| {
33
+ let array_path = &caps[1];
34
+ let item_alias = &caps[2];
35
+ let index_alias = caps.get(3).map(|m| m.as_str()).unwrap_or("index");
36
+ let template = &caps[4];
37
+
38
+ // Get array from state
39
+ let array = match self.resolve_array_path(array_path) {
40
+ Some(arr) => arr,
41
+ None => {
42
+ // Array not found, keep iteration block as-is
43
+ return caps[0].to_string();
44
+ }
45
+ };
46
+
47
+ // Render each item
48
+ let mut rendered_items = Vec::new();
49
+ for (idx, item) in array.iter().enumerate() {
50
+ let mut item_html = template.to_string();
51
+
52
+ // Replace @[item] and @[index] in template
53
+ item_html = item_html.replace(
54
+ &format!("@[{}]", item_alias),
55
+ &self.value_to_string(item)
56
+ );
57
+ item_html = item_html.replace(
58
+ &format!("@[{}]", index_alias),
59
+ &idx.to_string()
60
+ );
61
+
62
+ rendered_items.push(item_html);
63
+ }
64
+
65
+ // Reconstruct with comments (preserve index alias if it was in original)
66
+ let index_part = if caps.get(3).is_some() {
67
+ format!(", {}", index_alias)
68
+ } else {
69
+ String::new()
70
+ };
71
+
72
+ format!(
73
+ "<!-- each {} as {}{} -->{}<!-- /each -->",
74
+ array_path,
75
+ item_alias,
76
+ index_part,
77
+ rendered_items.join("")
78
+ )
79
+ });
80
+
81
+ Ok(result.to_string())
82
+ }
83
+
84
+ fn resolve_array_path(&self, path: &str) -> Option<&Vec<Value>> {
85
+ let parts: Vec<&str> = path.split('.').collect();
86
+ let mut current = self.state;
87
+
88
+ for part in parts {
89
+ current = current.get(part)?;
90
+ }
91
+
92
+ current.as_array()
93
+ }
94
+
95
+ fn resolve_state_path(&self, path: &str) -> Option<String> {
96
+ // Navigate state by dot path: "user.name" -> state["user"]["name"]
97
+ let parts: Vec<&str> = path.split('.').collect();
98
+ let mut current = self.state;
99
+
100
+ for part in parts {
101
+ // Handle array access: items[0]
102
+ if let Some(bracket_pos) = part.find('[') {
103
+ let array_name = &part[..bracket_pos];
104
+ let index_str = &part[bracket_pos+1..part.len()-1];
105
+ let index: usize = index_str.parse().ok()?;
106
+
107
+ current = current.get(array_name)?.get(index)?;
108
+ } else {
109
+ current = current.get(part)?;
110
+ }
111
+ }
112
+
113
+ Some(self.value_to_string(current))
114
+ }
115
+
116
+ fn value_to_string(&self, val: &Value) -> String {
117
+ match val {
118
+ Value::String(s) => s.clone(),
119
+ Value::Number(n) => n.to_string(),
120
+ Value::Bool(b) => b.to_string(),
121
+ Value::Null => String::new(),
122
+ Value::Array(_) | Value::Object(_) => val.to_string(),
123
+ }
124
+ }
125
+ }
126
+
127
+ #[cfg(test)]
128
+ mod tests {
129
+ use super::*;
130
+ use serde_json::json;
131
+
132
+ #[test]
133
+ fn stamp_simple_binding() {
134
+ let state = json!({ "name": "World" });
135
+ let stamper = ValueStamper::new(&state);
136
+ let html = String::from("<div>Hello @[name]</div>");
137
+ let result = stamper.stamp_html(html).unwrap();
138
+ assert_eq!(result, "<div>Hello World</div>");
139
+ }
140
+
141
+ #[test]
142
+ fn stamp_multiple_bindings() {
143
+ let state = json!({ "firstName": "John", "lastName": "Doe" });
144
+ let stamper = ValueStamper::new(&state);
145
+ let html = String::from("<div>@[firstName] @[lastName]</div>");
146
+ let result = stamper.stamp_html(html).unwrap();
147
+ assert_eq!(result, "<div>John Doe</div>");
148
+ }
149
+
150
+ #[test]
151
+ fn stamp_nested_binding() {
152
+ let state = json!({ "user": { "name": "John" } });
153
+ let stamper = ValueStamper::new(&state);
154
+ let html = String::from("<div>Hello @[user.name]</div>");
155
+ let result = stamper.stamp_html(html).unwrap();
156
+ assert_eq!(result, "<div>Hello John</div>");
157
+ }
158
+
159
+ #[test]
160
+ fn stamp_array_binding() {
161
+ let state = json!({ "items": ["first", "second", "third"] });
162
+ let stamper = ValueStamper::new(&state);
163
+ let html = String::from("<div>@[items[0]]</div>");
164
+ let result = stamper.stamp_html(html).unwrap();
165
+ assert_eq!(result, "<div>first</div>");
166
+ }
167
+
168
+ #[test]
169
+ fn stamp_number_binding() {
170
+ let state = json!({ "count": 42 });
171
+ let stamper = ValueStamper::new(&state);
172
+ let html = String::from("<div>Count: @[count]</div>");
173
+ let result = stamper.stamp_html(html).unwrap();
174
+ assert_eq!(result, "<div>Count: 42</div>");
175
+ }
176
+
177
+ #[test]
178
+ fn stamp_boolean_binding() {
179
+ let state = json!({ "isActive": true });
180
+ let stamper = ValueStamper::new(&state);
181
+ let html = String::from("<div>Active: @[isActive]</div>");
182
+ let result = stamper.stamp_html(html).unwrap();
183
+ assert_eq!(result, "<div>Active: true</div>");
184
+ }
185
+
186
+ #[test]
187
+ fn stamp_missing_binding() {
188
+ let state = json!({ "name": "World" });
189
+ let stamper = ValueStamper::new(&state);
190
+ let html = String::from("<div>Hello @[missing]</div>");
191
+ let result = stamper.stamp_html(html).unwrap();
192
+ // Should keep the binding marker if value doesn't exist
193
+ assert_eq!(result, "<div>Hello @[missing]</div>");
194
+ }
195
+
196
+ #[test]
197
+ fn stamp_attribute_binding() {
198
+ let state = json!({ "firstName": "John" });
199
+ let stamper = ValueStamper::new(&state);
200
+ let html = String::from(r#"<input value="@[firstName]">"#);
201
+ let result = stamper.stamp_html(html).unwrap();
202
+ assert_eq!(result, r#"<input value="John">"#);
203
+ }
204
+
205
+ #[test]
206
+ fn render_simple_iteration() {
207
+ let state = json!({ "items": [1, 2, 3] });
208
+ let stamper = ValueStamper::new(&state);
209
+ let html = String::from(r#"<!-- each items as item --><div>@[item]</div><!-- /each -->"#);
210
+ let result = stamper.stamp_html(html).unwrap();
211
+ assert_eq!(result, r#"<!-- each items as item --><div>1</div><div>2</div><div>3</div><!-- /each -->"#);
212
+ }
213
+
214
+ #[test]
215
+ fn render_iteration_with_index() {
216
+ let state = json!({ "items": ["a", "b", "c"] });
217
+ let stamper = ValueStamper::new(&state);
218
+ let html = String::from(r#"<!-- each items as item, idx --><span>[@[idx]] @[item]</span><!-- /each -->"#);
219
+ let result = stamper.stamp_html(html).unwrap();
220
+ assert_eq!(result, r#"<!-- each items as item, idx --><span>[0] a</span><span>[1] b</span><span>[2] c</span><!-- /each -->"#);
221
+ }
222
+ }
@@ -31,8 +31,6 @@ pub struct VibeCompilerConfig {
31
31
  #[serde(default)]
32
32
  pub accessibility: bool,
33
33
  #[serde(default)]
34
- pub manifest: bool,
35
- #[serde(default)]
36
34
  pub validate: bool,
37
35
  #[serde(default)]
38
36
  pub source_maps: bool,
@@ -61,7 +59,6 @@ impl Default for VibeCompilerConfig {
61
59
  root: None,
62
60
  minify: false,
63
61
  accessibility: false,
64
- manifest: false,
65
62
  validate: false,
66
63
  source_maps: false,
67
64
  exclude_tags: vec![],
@@ -83,7 +80,6 @@ pub struct Config {
83
80
  pub _root: Option<String>,
84
81
  pub minify: bool,
85
82
  pub accessibility: bool,
86
- pub manifest: bool,
87
83
  pub validate: bool,
88
84
  pub source_maps: bool,
89
85
  pub exclude_tags: Vec<String>,
@@ -124,7 +120,6 @@ impl Config {
124
120
  _root: config.root,
125
121
  minify: config.minify,
126
122
  accessibility: config.accessibility,
127
- manifest: config.manifest,
128
123
  validate: config.validate,
129
124
  source_maps: config.source_maps,
130
125
  exclude_tags: config.exclude_tags,
@@ -5,7 +5,6 @@ mod compiler;
5
5
  use clap::Parser as ClapParser;
6
6
  use colored::Colorize;
7
7
  use std::path::PathBuf;
8
- use std::time::Instant;
9
8
 
10
9
  use config::Config;
11
10
  use compiler::compile::CompileError;
@@ -16,11 +15,11 @@ use compiler::Compiler;
16
15
  struct ConfigOverrides {
17
16
  minify: bool,
18
17
  accessibility: bool,
19
- manifest: bool,
20
18
  validate: bool,
21
19
  source_maps: bool,
22
20
  node_modules_as_is: bool,
23
21
  components_as_is: bool,
22
+ runtime_as_is: bool,
24
23
  }
25
24
 
26
25
  /// Vibe Compiler - Compiles Vibe source files into optimized output
@@ -50,10 +49,6 @@ struct Args {
50
49
  #[arg(long)]
51
50
  accessibility: bool,
52
51
 
53
- /// Generate hydration manifest
54
- #[arg(long, name = "create-manifest")]
55
- create_manifest: bool,
56
-
57
52
  /// Generate source maps
58
53
  #[arg(long, name = "source-maps")]
59
54
  source_maps: bool,
@@ -81,11 +76,14 @@ struct Args {
81
76
  /// Keep components directory and <component> tags as-is (don't inline components)
82
77
  #[arg(long, name = "components-as-is")]
83
78
  components_as_is: bool,
79
+
80
+ /// Skip manifest generation - output will use runtime-only mode (no FOUC prevention)
81
+ #[arg(long, name = "runtime-as-is")]
82
+ runtime_as_is: bool,
84
83
  }
85
84
 
86
85
  fn main() {
87
86
  let args = Args::parse();
88
- let start = Instant::now();
89
87
 
90
88
  // Determine working directory
91
89
  let working_dir = args.cwd
@@ -117,7 +115,6 @@ fn main() {
117
115
  // Store original config values before applying flag overrides
118
116
  let original_minify = config.minify;
119
117
  let original_accessibility = config.accessibility;
120
- let original_manifest = config.manifest;
121
118
  let original_validate = config.validate;
122
119
  let original_source_maps = config.source_maps;
123
120
  let original_node_modules_as_is = config.node_modules_as_is;
@@ -134,10 +131,6 @@ fn main() {
134
131
  overrides.accessibility = !original_accessibility;
135
132
  config.accessibility = true;
136
133
  }
137
- if args.create_manifest {
138
- overrides.manifest = !original_manifest;
139
- config.manifest = true;
140
- }
141
134
  if args.validate {
142
135
  overrides.validate = !original_validate;
143
136
  config.validate = true;
@@ -190,12 +183,12 @@ fn main() {
190
183
  println!(" {}: {}", format!("{:<18}", "components").cyan(), format_value_no_flag(&config.components));
191
184
  println!(" {}: {}", format!("{:<18}", "components-as-is").cyan(), format_bool_with_flag(config.components_as_is, overrides.components_as_is, original_components_as_is));
192
185
  println!(" {}: {}", format!("{:<18}", "exclude-tags").cyan(), format_value_no_flag(format!("{:?}", config.exclude_tags)));
193
- println!(" {}: {}", format!("{:<18}", "manifest").cyan(), format_bool_with_flag(config.manifest, overrides.manifest, original_manifest));
194
186
  println!(" {}: {}", format!("{:<18}", "minify").cyan(), format_bool_with_flag(config.minify, overrides.minify, original_minify));
195
187
  println!(" {}: {}", format!("{:<18}", "node-modules-as-is").cyan(), format_bool_with_flag(config.node_modules_as_is, overrides.node_modules_as_is, original_node_modules_as_is));
196
188
  println!(" {}: {}", format!("{:<18}", "output").cyan(), format_value_no_flag(&config._output_str));
197
189
  println!(" {}: {}", format!("{:<18}", "pages").cyan(), format_value_no_flag(&config.pages));
198
190
  println!(" {}: {}", format!("{:<18}", "root").cyan(), format_value_no_flag(config._root.as_ref().map(|s| s.as_str()).unwrap_or("null")));
191
+ println!(" {}: {}", format!("{:<18}", "runtime-as-is").cyan(), if overrides.runtime_as_is { "true (flag)".green().to_string() } else { "false".to_string() });
199
192
  println!(" {}: {}", format!("{:<18}", "source").cyan(), format_value_no_flag(&config._source_str));
200
193
  println!(" {}: {}", format!("{:<18}", "source-maps").cyan(), format_bool_with_flag(config.source_maps, overrides.source_maps, original_source_maps));
201
194
  println!(" {}: {}", format!("{:<18}", "validate").cyan(), format_bool_with_flag(config.validate, overrides.validate, original_validate));
@@ -206,13 +199,28 @@ fn main() {
206
199
 
207
200
  match compiler.compile() {
208
201
  Ok(stats) => {
209
- let duration = start.elapsed();
202
+ // Generate manifests BEFORE showing success (unless --runtime-as-is flag is passed)
203
+ let mut manifest_time_ms = 0.0;
204
+ let mut manifest_stats_result = None;
205
+ if !args.runtime_as_is {
206
+ match compiler.generate_manifests() {
207
+ Ok(manifest_stats) => {
208
+ manifest_time_ms = manifest_stats.total_time_ms;
209
+ manifest_stats_result = Some(manifest_stats);
210
+ }
211
+ Err(e) => {
212
+ eprintln!("\n{}: Manifest generation failed: {}", "Warning".yellow(), e);
213
+ eprintln!("Compilation succeeded but manifests were not generated.");
214
+ eprintln!("Use --runtime-as-is to skip manifest generation.");
215
+ }
216
+ }
217
+ }
210
218
 
211
219
  // Show success headline
212
220
  println!("\n{}", "Compilation successful! ✅".green().bold());
213
221
  println!();
214
222
 
215
- // Show individual phase timings (validation first, then components, HTML, copied)
223
+ // Show individual phase timings (validation first, then components, HTML, manifests, copied)
216
224
 
217
225
  // Show validation time if it happened
218
226
  if let Some(validation_time) = stats.validation_time_ms {
@@ -240,6 +248,16 @@ fn main() {
240
248
  );
241
249
  }
242
250
 
251
+ // Show manifest stats between HTML and Copied files
252
+ if let Some(manifest_stats) = manifest_stats_result {
253
+ println!("* Generated manifests ({} file{}, {} skipped) in {:.0}ms",
254
+ manifest_stats.pages_processed,
255
+ if manifest_stats.pages_processed == 1 { "" } else { "s" },
256
+ manifest_stats.pages_skipped,
257
+ manifest_stats.total_time_ms
258
+ );
259
+ }
260
+
243
261
  if stats.files_copied > 0 {
244
262
  println!("* Copied files ({} file{}) in {:.0}ms",
245
263
  stats.files_copied,
@@ -257,7 +275,18 @@ fn main() {
257
275
  }
258
276
  }
259
277
 
260
- println!("\n{} in {:.0}ms", "Compiled".green(), duration.as_secs_f64() * 1000.0);
278
+ // Calculate total duration as sum of all individual operations
279
+ let total_duration_ms = stats.validation_time_ms.unwrap_or(0.0)
280
+ + stats.components_time_ms
281
+ + stats.compile_time_ms
282
+ + stats.copy_time_ms
283
+ + stats.node_modules_time_ms.unwrap_or(0.0)
284
+ + manifest_time_ms;
285
+
286
+ println!("\n{} in {:.0}ms", "Compiled".green(), total_duration_ms);
287
+
288
+ // Exit explicitly to kill background server thread
289
+ std::process::exit(0);
261
290
  }
262
291
  Err(e) => {
263
292
  match e {
@@ -277,8 +306,4 @@ fn main() {
277
306
  std::process::exit(1);
278
307
  }
279
308
  }
280
-
281
- if args.watch {
282
- println!("{}", "Watch mode not yet implemented".yellow());
283
- }
284
309
  }
@@ -103,6 +103,10 @@ impl HtmlParser {
103
103
  components_dir: &str,
104
104
  external_cache: &HashMap<String, String>,
105
105
  ) -> String {
106
+ // Extract and preserve DOCTYPE declaration if present
107
+ let doctype_re = regex::Regex::new(r"(?i)^\s*<!DOCTYPE[^>]*>\s*").unwrap();
108
+ let doctype = doctype_re.find(content).map(|m| m.as_str().to_string());
109
+
106
110
  let mut result = content.to_string();
107
111
 
108
112
  // Step 1: Transform custom tags matching element files to <component> tags
@@ -118,6 +122,14 @@ impl HtmlParser {
118
122
  result = transform_custom_tags_to_divs(&result, exclude_tags);
119
123
  }
120
124
 
125
+ // Step 4: Restore DOCTYPE if it was present
126
+ if let Some(dt) = doctype {
127
+ // Remove any existing DOCTYPE that might have been left behind
128
+ result = doctype_re.replace(&result, "").to_string();
129
+ // Prepend the original DOCTYPE
130
+ result = format!("{}{}", dt, result);
131
+ }
132
+
121
133
  result
122
134
  }
123
135
 
@@ -235,14 +247,16 @@ impl HtmlParser {
235
247
  replacement = replacement.replace(&prop_binding, prop_value);
236
248
  }
237
249
 
238
- // Replace <slot></slot> with slot content if present
239
- if let Some(ref slot) = slot_content {
240
- if !slot.trim().is_empty() {
241
- replacement = replacement.replace("<slot></slot>", slot);
242
- replacement = replacement.replace("<slot/>", slot);
243
- replacement = replacement.replace("<slot />", slot);
244
- }
245
- }
250
+ // Replace <slot> tags with content, or remove if empty/missing
251
+ let slot_replacement = slot_content
252
+ .as_ref()
253
+ .filter(|s| !s.trim().is_empty())
254
+ .map(|s| s.as_str())
255
+ .unwrap_or("");
256
+
257
+ replacement = replacement.replace("<slot></slot>", slot_replacement);
258
+ replacement = replacement.replace("<slot/>", slot_replacement);
259
+ replacement = replacement.replace("<slot />", slot_replacement);
246
260
 
247
261
  result.replace_range(*start..*end, &replacement);
248
262
  }
package/index.js CHANGED
@@ -1,96 +1,40 @@
1
1
  // Universal entry point for Vibe
2
- // Usage: <script src="vibe/index.js">$.count = 0;</script> (global)
3
- // <script src="vibe/index.js">let count = 0;</script> (component)
2
+ // Usage: import vibe from 'vibe/index.js'; vibe({ initialState }, { debug: true }, 'body');
4
3
 
5
- (async () => {
6
- const script = document.currentScript;
7
- const scriptContent = script?.textContent?.trim() || '';
4
+ import { ensureBoot, boot, isBooted } from './boot.js';
8
5
 
9
- // Determine if this is component state (has let/const/var) or global state (uses $)
10
- const hasDeclarations = /\b(let|const|var)\s+\w+/.test(scriptContent);
11
- const isComponent = hasDeclarations;
12
-
13
- // Boot Vibe if not already initialized or booting
14
- if (!window.__vibeInitialized && !window.__vibeBooting) {
15
- window.__vibeBooting = true;
16
-
17
- // Process ALL scripts on the page BEFORE booting
18
- const { generateComponentId, executeComponentScript } = await import('./runtime/component-state.js');
19
- const allComponentScripts = document.querySelectorAll('script[src*="index.js"]');
20
- const initialState = {};
21
- const globalStateScripts = [];
22
-
23
- allComponentScripts.forEach((s) => {
24
- const content = s.textContent?.trim() || '';
25
- if (!content) return;
26
-
27
- const hasDecl = /\b(let|const|var)\s+\w+/.test(content);
28
- if (hasDecl) {
29
- // This is a component script
30
- const componentId = generateComponentId();
31
-
32
- // Tag script and siblings
33
- s.setAttribute('data-vibe-component-id', componentId);
34
- s.setAttribute('type', 'component');
6
+ const vibe = (state = {}, config, targetSelector) => {
7
+ if (isBooted()) {
8
+ // Already booted - mutate live state
9
+ Object.assign(window.$, state);
10
+ return window.$;
11
+ }
35
12
 
36
- let sibling = s.nextElementSibling;
37
- while (sibling) {
38
- if (sibling.tagName === 'SCRIPT' && /\b(let|const|var)\s+\w+/.test(sibling.textContent || '')) {
39
- break;
40
- }
41
- sibling.setAttribute('data-vibe-component-id', componentId);
42
- sibling = sibling.nextElementSibling;
43
- }
13
+ // Not booted yet - accumulate in global state registry
14
+ if (!window.__vibeGlobalState) {
15
+ window.__vibeGlobalState = {};
16
+ }
17
+ Object.assign(window.__vibeGlobalState, state);
44
18
 
45
- // Execute component script to get state
46
- const componentState = executeComponentScript(content);
47
- initialState[componentId] = componentState;
48
- } else {
49
- // This is a global state script - save for later
50
- globalStateScripts.push(content);
51
- }
52
- });
19
+ // Store config (first caller wins)
20
+ if (config && !window.__vibeConfig) {
21
+ window.__vibeConfig = config;
22
+ }
53
23
 
54
- // Execute global state scripts into initialState BEFORE booting
55
- // This ensures global state is available during initial parse/hydrate
56
- if (globalStateScripts.length > 0) {
57
- const captured = {};
58
- const fakeState = new Proxy(captured, {
59
- set(target, key, value) {
60
- target[key] = value;
61
- return true;
62
- }
63
- });
24
+ // Store targetSelector (first caller wins)
25
+ if (targetSelector && !window.__vibeTargetSelector) {
26
+ window.__vibeTargetSelector = targetSelector;
27
+ }
64
28
 
65
- globalStateScripts.forEach(code => {
66
- const $ = fakeState;
67
- eval(code);
68
- });
29
+ // Explicit boot call (no state passed means "boot now")
30
+ if (Object.keys(state).length === 0 && Object.keys(window.__vibeGlobalState).length > 0) {
31
+ return boot();
32
+ }
69
33
 
70
- // Merge global state into initialState
71
- Object.assign(initialState, captured);
72
- }
34
+ // Ensure boot happens in microtask
35
+ ensureBoot();
73
36
 
74
- // Boot Vibe with both component and global states
75
- const { default: main } = await import('./runtime/index.js');
76
- window.$ = main(initialState, 'vibe', {});
77
- window.__vibeInitialized = true;
78
- window.__vibeBooting = false;
79
- } else {
80
- // Vibe already booted - just execute global code if any
81
- if (window.__vibeBooting) {
82
- await new Promise(resolve => {
83
- const check = setInterval(() => {
84
- if (window.__vibeInitialized) {
85
- clearInterval(check);
86
- resolve();
87
- }
88
- }, 10);
89
- });
90
- }
37
+ return null;
38
+ };
91
39
 
92
- if (!isComponent && scriptContent) {
93
- eval(scriptContent);
94
- }
95
- }
96
- })();
40
+ export default vibe;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ape-egg/vibe",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "Runtime-first reactivity with optional compiler",
6
6
  "main": "index.js",
@@ -7,9 +7,12 @@ import { evalInScope, resolveThisPath } from './utils.js';
7
7
  const evaluateCondition = (expression, state, element = null) => !!evalInScope(expression, state, element);
8
8
 
9
9
  // Helper function to check if a match references a specific key
10
- const matchesKey = (matchStr, key) => matchStr === key || matchStr.startsWith(key + '.');
10
+ const matchesKey = (matchStr, key) =>
11
+ matchStr === key ||
12
+ matchStr.startsWith(key + '.') ||
13
+ matchStr.startsWith(key + '[');
11
14
 
12
- const recursive = (tree, state, newState, affected, scopedStateForHydration = null) => {
15
+ const recursive = (tree, state, newState, affected, scopedStateForHydration = null, depth = 0) => {
13
16
  // Handle iteration nodes specially
14
17
  if (tree.type === 'iteration') {
15
18
  // Handle this.property for component-scoped arrays
@@ -38,7 +41,7 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
38
41
  // Merge newState into scopedState to get updated global values
39
42
  const mergedNewState = { ...instance.scopedState, ...newState };
40
43
  // Pass the Proxy as scopedState so evalInScope can access iteration variables
41
- recursive(instance.tree, instance.scopedState, mergedNewState, affected, instance.scopedState);
44
+ recursive(instance.tree, instance.scopedState, mergedNewState, affected, instance.scopedState, depth + 1);
42
45
  }
43
46
  }
44
47
  }
@@ -63,7 +66,7 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
63
66
 
64
67
  // Condition didn't change, check for affected elements inside active branch
65
68
  if (tree.runtime.activeInstance && tree.runtime.activeInstance.parsedTree) {
66
- return recursive(tree.runtime.activeInstance.parsedTree, state, newState, affected, scopedStateForHydration);
69
+ return recursive(tree.runtime.activeInstance.parsedTree, state, newState, affected, scopedStateForHydration, depth + 1);
67
70
  }
68
71
 
69
72
  return affected;
@@ -234,7 +237,7 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
234
237
  for (const key in children) {
235
238
  const child = children[key];
236
239
  if (child && typeof child === 'object') {
237
- recursive(child, state, newState, affected, scopedStateForHydration);
240
+ recursive(child, state, newState, affected, scopedStateForHydration, depth + 1);
238
241
  }
239
242
  }
240
243
  }
@@ -242,4 +245,7 @@ const recursive = (tree, state, newState, affected, scopedStateForHydration = nu
242
245
  return affected;
243
246
  };
244
247
 
245
- export default (tree, state, newState) => recursive(tree, state, newState, [], null);
248
+ export default (tree, state, newState) => {
249
+ const affected = recursive(tree, state, newState, [], null, 0);
250
+ return affected;
251
+ };