@ape-egg/vibe 1.2.0 → 1.3.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/CHANGELOG.md +72 -0
- package/README.md +1 -1
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/compiler/compile.rs +204 -51
- package/compiler/src/compiler/manifest_builder.rs +432 -0
- package/compiler/src/compiler/mod.rs +3 -0
- package/compiler/src/compiler/state_extractor.rs +148 -0
- package/compiler/src/compiler/value_stamper.rs +222 -0
- package/compiler/src/config.rs +0 -5
- package/compiler/src/main.rs +45 -20
- package/compiler/src/parser/html.rs +22 -8
- package/component.js +59 -0
- package/index.js +30 -86
- package/package.json +3 -1
- package/runtime/affected.js +12 -6
- package/runtime/cleanup.js +45 -24
- package/runtime/component-state.js +1 -1
- package/runtime/component.js +20 -13
- package/runtime/constants.js +41 -10
- package/runtime/debug.js +1 -0
- package/runtime/hydrate.js +6 -1
- package/runtime/hyperspeed.js +425 -0
- package/runtime/index.js +236 -49
- package/runtime/iterate.js +3 -0
- package/runtime/parse.js +21 -29
- package/runtime/scope.js +5 -25
- package/runtime/utils.js +4 -13
- package/vibe.css +4 -2
|
@@ -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
|
+
}
|
package/compiler/src/config.rs
CHANGED
|
@@ -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,
|
package/compiler/src/main.rs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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/component.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Component state entry point
|
|
2
|
+
// Usage:
|
|
3
|
+
// <component>
|
|
4
|
+
// <script type="module">
|
|
5
|
+
// import component from 'vibe/component.js';
|
|
6
|
+
// component({ count: 0 }, { debug: false }, 'body');
|
|
7
|
+
// </script>
|
|
8
|
+
// <div>@[this.count]</div>
|
|
9
|
+
// </component>
|
|
10
|
+
//
|
|
11
|
+
// Or with class:
|
|
12
|
+
// <div class="component">...</div>
|
|
13
|
+
|
|
14
|
+
import { generateComponentId } from './runtime/component-state.js';
|
|
15
|
+
import { ensureBoot } from './boot.js';
|
|
16
|
+
|
|
17
|
+
const component = (state = {}, config, targetSelector) => {
|
|
18
|
+
// Find the first unprocessed INLINE component wrapper
|
|
19
|
+
// Skip <component src=""> (fetched components) - they don't need state tagging
|
|
20
|
+
// Scripts execute in DOM order, so we claim wrappers in DOM order too
|
|
21
|
+
// Supports: <component> or <div class="component">
|
|
22
|
+
const allWrappers = Array.from(document.querySelectorAll('component:not([src]), div.component:not([src])'));
|
|
23
|
+
const wrapper = allWrappers.find(el => !el.hasAttribute('data-vibe-component-id'));
|
|
24
|
+
|
|
25
|
+
if (!wrapper) {
|
|
26
|
+
console.warn('[vibe] component() must be called inside <component> or <div class="component">');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Generate unique component ID
|
|
31
|
+
const componentId = generateComponentId();
|
|
32
|
+
|
|
33
|
+
// Tag only the wrapper element
|
|
34
|
+
// All descendants will find it via element.closest('[data-vibe-component-id]')
|
|
35
|
+
wrapper.setAttribute('data-vibe-component-id', componentId);
|
|
36
|
+
|
|
37
|
+
// Register component state in shared registry
|
|
38
|
+
if (!window.__vibeComponents) {
|
|
39
|
+
window.__vibeComponents = {};
|
|
40
|
+
}
|
|
41
|
+
window.__vibeComponents[componentId] = state;
|
|
42
|
+
|
|
43
|
+
// Store config (first caller wins)
|
|
44
|
+
if (config && !window.__vibeConfig) {
|
|
45
|
+
window.__vibeConfig = config;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Store targetSelector (first caller wins)
|
|
49
|
+
if (targetSelector && !window.__vibeTargetSelector) {
|
|
50
|
+
window.__vibeTargetSelector = targetSelector;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Ensure boot happens
|
|
54
|
+
ensureBoot();
|
|
55
|
+
|
|
56
|
+
return componentId;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default component;
|
package/index.js
CHANGED
|
@@ -1,96 +1,40 @@
|
|
|
1
1
|
// Universal entry point for Vibe
|
|
2
|
-
// Usage:
|
|
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
|
-
|
|
6
|
-
const script = document.currentScript;
|
|
7
|
-
const scriptContent = script?.textContent?.trim() || '';
|
|
4
|
+
import { ensureBoot, boot, isBooted } from './boot.js';
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
34
|
+
// Ensure boot happens in microtask
|
|
35
|
+
ensureBoot();
|
|
73
36
|
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
93
|
-
eval(scriptContent);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
})();
|
|
40
|
+
export default vibe;
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ape-egg/vibe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime-first reactivity with optional compiler",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"homepage": "https://vibe.korte.kim",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./index.js",
|
|
10
|
+
"./component": "./component.js",
|
|
10
11
|
"./runtime": "./runtime/index.js",
|
|
11
12
|
"./compiler": "./compiler/bin/vibe-compile.js"
|
|
12
13
|
},
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
17
18
|
"index.js",
|
|
19
|
+
"component.js",
|
|
18
20
|
"vibe.css",
|
|
19
21
|
"runtime/",
|
|
20
22
|
"compiler/bin/",
|