@ape-egg/vibe 1.6.0 → 1.7.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/CHANGELOG.md +62 -1
- package/README.md +136 -24
- package/boot.js +11 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/Cargo.lock +1 -1
- package/compiler/src/Cargo.toml +1 -1
- package/compiler/src/compiler/compile.rs +53 -22
- package/compiler/src/compiler/component_tagger.rs +22 -3
- package/compiler/src/compiler/value_stamper.rs +75 -5
- package/compiler/src/compiler/watcher.rs +69 -2
- package/compiler/src/config.rs +10 -0
- package/compiler/src/main.rs +45 -18
- package/compiler/src/parser/html.rs +61 -9
- package/index.js +34 -5
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/runtime/cleanup.js +0 -9
- package/runtime/component.js +14 -18
- package/runtime/index.js +13 -2
- package/runtime/iterate.js +63 -15
- package/runtime/parse.js +3 -2
- package/runtime/pre-compiled-manifest.js +120 -53
|
@@ -20,10 +20,23 @@ pub struct TaggedResult {
|
|
|
20
20
|
impl ComponentTagger {
|
|
21
21
|
/// Find component wrappers, add deterministic IDs, and structure state
|
|
22
22
|
pub fn tag_components(html: &str, base_path: &PathBuf) -> Result<TaggedResult, String> {
|
|
23
|
-
//
|
|
23
|
+
// Extract ALL <template> content to protect it from HTML parser
|
|
24
|
+
// HTML parsers can strip content from <template> tags during serialization
|
|
25
|
+
let template_regex = regex::Regex::new(r"(?s)<template[^>]*>.*?</template>").unwrap();
|
|
26
|
+
let mut template_placeholders: Vec<String> = Vec::new();
|
|
27
|
+
let mut html_with_placeholders = html.to_string();
|
|
28
|
+
|
|
29
|
+
for (i, mat) in template_regex.find_iter(html).enumerate() {
|
|
30
|
+
let content = mat.as_str();
|
|
31
|
+
let placeholder = format!("<!--VIBE_TEMPLATE_PLACEHOLDER_{}-->", i);
|
|
32
|
+
template_placeholders.push(content.to_string());
|
|
33
|
+
html_with_placeholders = html_with_placeholders.replace(content, &placeholder);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Parse HTML (with placeholders instead of actual vibe-dehydrate content)
|
|
24
37
|
let dom = parse_document(RcDom::default(), Default::default())
|
|
25
38
|
.from_utf8()
|
|
26
|
-
.read_from(&mut
|
|
39
|
+
.read_from(&mut html_with_placeholders.as_bytes())
|
|
27
40
|
.map_err(|e| format!("Failed to parse HTML: {:?}", e))?;
|
|
28
41
|
|
|
29
42
|
// Find all component wrappers and extract their state
|
|
@@ -39,9 +52,15 @@ impl ComponentTagger {
|
|
|
39
52
|
SerializeOpts::default()
|
|
40
53
|
).map_err(|e| format!("Failed to serialize HTML: {:?}", e))?;
|
|
41
54
|
|
|
42
|
-
let modified_html = String::from_utf8(modified_html_bytes)
|
|
55
|
+
let mut modified_html = String::from_utf8(modified_html_bytes)
|
|
43
56
|
.map_err(|e| format!("Failed to convert HTML to UTF-8: {}", e))?;
|
|
44
57
|
|
|
58
|
+
// Restore template content from placeholders
|
|
59
|
+
for (i, content) in template_placeholders.iter().enumerate() {
|
|
60
|
+
let placeholder = format!("<!--VIBE_TEMPLATE_PLACEHOLDER_{}-->", i);
|
|
61
|
+
modified_html = modified_html.replace(&placeholder, content);
|
|
62
|
+
}
|
|
63
|
+
|
|
45
64
|
// Extract all state from HTML
|
|
46
65
|
let all_state = StateExtractor::extract_from_html(html, base_path)?;
|
|
47
66
|
|
|
@@ -79,8 +79,21 @@ impl<'a> ValueStamper<'a> {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
pub fn stamp_html(&self, html: String) -> Result<String, String> {
|
|
82
|
+
// Extract vibe-dehydrate content to protect it from processing
|
|
83
|
+
// This prevents @[...] markers and iterations/conditionals from being processed
|
|
84
|
+
let dehydrate_regex = Regex::new(r"(?s)<template[^>]*\svibe-dehydrate[^>]*>.*?</template>").unwrap();
|
|
85
|
+
let mut dehydrate_placeholders: Vec<String> = Vec::new();
|
|
86
|
+
let mut html_with_placeholders = html.clone();
|
|
87
|
+
|
|
88
|
+
for (i, mat) in dehydrate_regex.find_iter(&html).enumerate() {
|
|
89
|
+
let content = mat.as_str();
|
|
90
|
+
let placeholder = format!("<!--VIBE_DEHYDRATE_PLACEHOLDER_{}-->", i);
|
|
91
|
+
dehydrate_placeholders.push(content.to_string());
|
|
92
|
+
html_with_placeholders = html_with_placeholders.replace(content, &placeholder);
|
|
93
|
+
}
|
|
94
|
+
|
|
82
95
|
// First, render iterations (expands templates into multiple instances)
|
|
83
|
-
let html = self.render_iterations(
|
|
96
|
+
let html = self.render_iterations(html_with_placeholders)?;
|
|
84
97
|
|
|
85
98
|
// Then, render conditionals (resolve if/else branches)
|
|
86
99
|
let html = self.render_conditionals(html)?;
|
|
@@ -95,6 +108,13 @@ impl<'a> ValueStamper<'a> {
|
|
|
95
108
|
}
|
|
96
109
|
}
|
|
97
110
|
|
|
111
|
+
// Skip elements with vibe-dehydrate attribute (content should not be processed)
|
|
112
|
+
// This is a secondary protection in case placeholders aren't used
|
|
113
|
+
let dehydrate_regex = Regex::new(r"(?s)<template[^>]*\svibe-dehydrate[^>]*>.*?</template>").unwrap();
|
|
114
|
+
for mat in dehydrate_regex.find_iter(&html) {
|
|
115
|
+
skip_regions.push((mat.start(), mat.end()));
|
|
116
|
+
}
|
|
117
|
+
|
|
98
118
|
// When components_as_is is true, skip component elements (runtime will handle them)
|
|
99
119
|
// Match both <component> and <div class="component"> elements
|
|
100
120
|
if self.components_as_is {
|
|
@@ -148,6 +168,15 @@ impl<'a> ValueStamper<'a> {
|
|
|
148
168
|
// Add remaining text
|
|
149
169
|
result.push_str(&html[last_pos..]);
|
|
150
170
|
|
|
171
|
+
// Clean up attributes that still contain unresolved markers
|
|
172
|
+
result = self.cleanup_unresolved_attributes(result);
|
|
173
|
+
|
|
174
|
+
// Restore vibe-dehydrate content from placeholders
|
|
175
|
+
for (i, content) in dehydrate_placeholders.iter().enumerate() {
|
|
176
|
+
let placeholder = format!("<!--VIBE_DEHYDRATE_PLACEHOLDER_{}-->", i);
|
|
177
|
+
result = result.replace(&placeholder, content);
|
|
178
|
+
}
|
|
179
|
+
|
|
151
180
|
// Finally, clean up boolean-like attributes with falsy values
|
|
152
181
|
// When components_as_is is true, skip cleanup inside component elements
|
|
153
182
|
Ok(self.cleanup_boolean_attributes(result, &skip_regions))
|
|
@@ -238,13 +267,19 @@ impl<'a> ValueStamper<'a> {
|
|
|
238
267
|
// Render nested iterations first
|
|
239
268
|
let mut item_html = temp_stamper.render_iterations_recursive(template.clone())?;
|
|
240
269
|
|
|
241
|
-
// Then
|
|
270
|
+
// Then render conditionals with this item's context
|
|
271
|
+
item_html = temp_stamper.render_conditionals(item_html)?;
|
|
272
|
+
|
|
273
|
+
// Finally stamp all bindings in this item's context
|
|
242
274
|
item_html = temp_stamper.binding_regex.replace_all(&item_html, |caps: &Captures| {
|
|
243
275
|
let expr = &caps[1];
|
|
244
276
|
temp_stamper.eval_expression(expr)
|
|
245
277
|
.unwrap_or_else(|| caps[0].to_string())
|
|
246
278
|
}).to_string();
|
|
247
279
|
|
|
280
|
+
// Clean up attributes with unresolved markers (e.g., data-tutorial="@[item.id]" when item.id is undefined)
|
|
281
|
+
item_html = temp_stamper.cleanup_unresolved_attributes(item_html);
|
|
282
|
+
|
|
248
283
|
rendered_items.push(item_html);
|
|
249
284
|
}
|
|
250
285
|
|
|
@@ -500,9 +535,31 @@ impl<'a> ValueStamper<'a> {
|
|
|
500
535
|
fn try_eval_condition(&self, condition: &str) -> Option<bool> {
|
|
501
536
|
self.context.with(|ctx| -> Option<bool> {
|
|
502
537
|
// Try to evaluate condition
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
538
|
+
let result: rquickjs::Value = match ctx.eval(condition) {
|
|
539
|
+
Ok(val) => val,
|
|
540
|
+
Err(_) => {
|
|
541
|
+
// Evaluation failed (undefined variable, syntax error, etc.)
|
|
542
|
+
// Default to false as per requirements
|
|
543
|
+
return Some(false);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// Use JavaScript truthiness rules instead of requiring boolean type
|
|
548
|
+
// This allows conditions like "item.id" to work correctly
|
|
549
|
+
if result.is_bool() {
|
|
550
|
+
result.as_bool()
|
|
551
|
+
} else if result.is_null() || result.is_undefined() {
|
|
552
|
+
Some(false)
|
|
553
|
+
} else if result.is_number() {
|
|
554
|
+
// 0, NaN are falsy
|
|
555
|
+
result.as_number().map(|n| n != 0.0 && !n.is_nan())
|
|
556
|
+
} else if result.is_string() {
|
|
557
|
+
// Empty string is falsy
|
|
558
|
+
result.as_string().and_then(|s| s.to_string().ok()).map(|s| !s.is_empty())
|
|
559
|
+
} else {
|
|
560
|
+
// Objects, arrays are truthy
|
|
561
|
+
Some(true)
|
|
562
|
+
}
|
|
506
563
|
})
|
|
507
564
|
}
|
|
508
565
|
|
|
@@ -510,6 +567,19 @@ impl<'a> ValueStamper<'a> {
|
|
|
510
567
|
/// Boolean-like attributes (not in VALUE_ATTRS) should be:
|
|
511
568
|
/// - Removed entirely when falsy
|
|
512
569
|
/// - Present with empty value when truthy (HTML5 boolean attribute syntax)
|
|
570
|
+
/// Remove attributes that still contain unresolved markers (e.g., data-tutorial="@[item.id]")
|
|
571
|
+
/// These occur when iterating over items where some don't have the referenced property
|
|
572
|
+
fn cleanup_unresolved_attributes(&self, html: String) -> String {
|
|
573
|
+
// Match attributes with marker values: attr="@[...]"
|
|
574
|
+
// Use non-greedy match and look for closing "]" at end of attribute value
|
|
575
|
+
let attr_with_marker = Regex::new(r#"\s+([\w-]+)="@\[.+?\]""#).unwrap();
|
|
576
|
+
|
|
577
|
+
attr_with_marker.replace_all(&html, |_caps: &Captures| {
|
|
578
|
+
// Remove the entire attribute when it has an unresolved marker
|
|
579
|
+
String::new()
|
|
580
|
+
}).to_string()
|
|
581
|
+
}
|
|
582
|
+
|
|
513
583
|
fn cleanup_boolean_attributes(&self, html: String, skip_regions: &[(usize, usize)]) -> String {
|
|
514
584
|
// Create a set for faster lookup
|
|
515
585
|
let value_attrs: HashSet<&str> = VALUE_ATTRS.iter().copied().collect();
|
|
@@ -328,7 +328,7 @@ pub fn watch(config: Config, verbose: bool) -> std::result::Result<(), Box<dyn s
|
|
|
328
328
|
|
|
329
329
|
// Keep compiler and parser alive to reuse component cache across incremental compilations
|
|
330
330
|
let mut watch_compiler = Compiler::new(config.clone(), false);
|
|
331
|
-
let parser = {
|
|
331
|
+
let mut parser = {
|
|
332
332
|
use crate::parser::HtmlParser;
|
|
333
333
|
let mut p = HtmlParser::new(config.components_path());
|
|
334
334
|
if let Err(e) = p.load_elements() {
|
|
@@ -396,10 +396,52 @@ pub fn watch(config: Config, verbose: bool) -> std::result::Result<(), Box<dyn s
|
|
|
396
396
|
let dependent_pages = graph.get_all_dependent_pages(&path_canonical);
|
|
397
397
|
if !dependent_pages.is_empty() {
|
|
398
398
|
println!("{} {} changed", "[watch]".cyan(), relative_path.display());
|
|
399
|
+
|
|
400
|
+
// Invalidate component cache so stale content isn't used
|
|
401
|
+
watch_compiler.clear_component_cache();
|
|
402
|
+
|
|
403
|
+
// Reload the changed component in the parser's element cache
|
|
404
|
+
// (used for custom element syntax like <Layout>)
|
|
405
|
+
if path.exists() {
|
|
406
|
+
let _ = parser.reload_element(path);
|
|
407
|
+
}
|
|
408
|
+
|
|
399
409
|
pages_to_recompile.extend(dependent_pages);
|
|
400
410
|
}
|
|
401
411
|
} else if let Some(ext) = path.extension() {
|
|
402
412
|
if ext == "html" {
|
|
413
|
+
// Check if file still exists (handle deletions)
|
|
414
|
+
if !path.exists() {
|
|
415
|
+
// File was deleted - clean up dependency graph
|
|
416
|
+
println!("{} {} deleted", "[watch]".yellow(), relative_path.display());
|
|
417
|
+
|
|
418
|
+
if let Some(old_deps) = graph.page_to_components.remove(path) {
|
|
419
|
+
for dep in old_deps {
|
|
420
|
+
if let Some(pages) = graph.component_to_pages.get_mut(&dep) {
|
|
421
|
+
pages.remove(path);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Delete corresponding compiled output
|
|
427
|
+
let output_path = canonical_output.join(relative_path);
|
|
428
|
+
if output_path.exists() {
|
|
429
|
+
if let Err(e) = std::fs::remove_file(&output_path) {
|
|
430
|
+
eprintln!("{}: Failed to delete {}: {}", "Warning".yellow(), output_path.display(), e);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Delete corresponding manifest
|
|
435
|
+
let manifest_path = canonical_output.join("vibe-hyperspeed").join(format!("{}.manifest.js", relative_path.display()));
|
|
436
|
+
if manifest_path.exists() {
|
|
437
|
+
if let Err(e) = std::fs::remove_file(&manifest_path) {
|
|
438
|
+
eprintln!("{}: Failed to delete manifest {}: {}", "Warning".yellow(), manifest_path.display(), e);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
403
445
|
// Page changed - recompile just this page
|
|
404
446
|
println!("{} {} changed", "[watch]".cyan(), relative_path.display());
|
|
405
447
|
pages_to_recompile.insert(path.clone());
|
|
@@ -431,7 +473,10 @@ pub fn watch(config: Config, verbose: bool) -> std::result::Result<(), Box<dyn s
|
|
|
431
473
|
}
|
|
432
474
|
|
|
433
475
|
// Separate HTML files (from dependency graph) and CSS/JS assets (from changed_paths)
|
|
434
|
-
|
|
476
|
+
// Filter out deleted files
|
|
477
|
+
let html_files: Vec<PathBuf> = pages_to_recompile.into_iter()
|
|
478
|
+
.filter(|p| p.exists())
|
|
479
|
+
.collect();
|
|
435
480
|
|
|
436
481
|
let mut asset_files: Vec<PathBuf> = Vec::new();
|
|
437
482
|
for path in &changed_paths {
|
|
@@ -446,6 +491,24 @@ pub fn watch(config: Config, verbose: bool) -> std::result::Result<(), Box<dyn s
|
|
|
446
491
|
continue;
|
|
447
492
|
}
|
|
448
493
|
|
|
494
|
+
// Handle deleted asset files
|
|
495
|
+
if !path.exists() {
|
|
496
|
+
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
|
497
|
+
if ext == "css" || ext == "js" {
|
|
498
|
+
let relative_path = path.strip_prefix(&config.source).unwrap_or(path);
|
|
499
|
+
let output_path = canonical_output.join(relative_path);
|
|
500
|
+
if output_path.exists() {
|
|
501
|
+
if let Err(e) = std::fs::remove_file(&output_path) {
|
|
502
|
+
eprintln!("{}: Failed to delete {}: {}", "Warning".yellow(), output_path.display(), e);
|
|
503
|
+
} else {
|
|
504
|
+
println!("{} {} deleted", "[watch]".yellow(), relative_path.display());
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
449
512
|
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
|
450
513
|
if ext == "css" || ext == "js" {
|
|
451
514
|
asset_files.push(path.clone());
|
|
@@ -482,6 +545,10 @@ pub fn watch(config: Config, verbose: bool) -> std::result::Result<(), Box<dyn s
|
|
|
482
545
|
|
|
483
546
|
// Compile HTML files
|
|
484
547
|
if !html_files.is_empty() {
|
|
548
|
+
// Always clear component cache before recompiling so components
|
|
549
|
+
// are read fresh from disk (prevents stale cache from previous triggers)
|
|
550
|
+
watch_compiler.clear_component_cache();
|
|
551
|
+
|
|
485
552
|
match watch_compiler.compile_specific_html_files(&html_files, &parser) {
|
|
486
553
|
Ok(stats) => {
|
|
487
554
|
total_stats.files_compiled = stats.files_compiled;
|
package/compiler/src/config.rs
CHANGED
|
@@ -44,6 +44,10 @@ pub struct VibeCompilerConfig {
|
|
|
44
44
|
pub runtime_as_is: bool,
|
|
45
45
|
#[serde(default)]
|
|
46
46
|
pub iterations_as_is: bool,
|
|
47
|
+
#[serde(default)]
|
|
48
|
+
pub no_clean: bool,
|
|
49
|
+
#[serde(default)]
|
|
50
|
+
pub fouc_as_is: bool,
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
fn default_source() -> String { "./".to_string() }
|
|
@@ -99,6 +103,8 @@ impl Default for VibeCompilerConfig {
|
|
|
99
103
|
components_as_is: false,
|
|
100
104
|
runtime_as_is: false,
|
|
101
105
|
iterations_as_is: false,
|
|
106
|
+
no_clean: false,
|
|
107
|
+
fouc_as_is: false,
|
|
102
108
|
}
|
|
103
109
|
}
|
|
104
110
|
}
|
|
@@ -122,6 +128,8 @@ pub struct Config {
|
|
|
122
128
|
pub components_as_is: bool,
|
|
123
129
|
pub runtime_as_is: bool,
|
|
124
130
|
pub iterations_as_is: bool,
|
|
131
|
+
pub no_clean: bool,
|
|
132
|
+
pub fouc_as_is: bool,
|
|
125
133
|
pub working_dir: PathBuf,
|
|
126
134
|
}
|
|
127
135
|
|
|
@@ -168,6 +176,8 @@ impl Config {
|
|
|
168
176
|
components_as_is: config.components_as_is,
|
|
169
177
|
runtime_as_is: config.runtime_as_is,
|
|
170
178
|
iterations_as_is: config.iterations_as_is,
|
|
179
|
+
no_clean: config.no_clean,
|
|
180
|
+
fouc_as_is: config.fouc_as_is,
|
|
171
181
|
working_dir,
|
|
172
182
|
}
|
|
173
183
|
}
|
package/compiler/src/main.rs
CHANGED
|
@@ -26,7 +26,7 @@ struct ConfigOverrides {
|
|
|
26
26
|
#[derive(ClapParser, Debug)]
|
|
27
27
|
#[command(name = "vibe-compile")]
|
|
28
28
|
#[command(author = "Kim Korte")]
|
|
29
|
-
#[command(version = "
|
|
29
|
+
#[command(version = "1.6.1")]
|
|
30
30
|
#[command(about = "Compiles Vibe source files into optimized output")]
|
|
31
31
|
struct Args {
|
|
32
32
|
/// Working directory (defaults to current directory)
|
|
@@ -84,6 +84,14 @@ struct Args {
|
|
|
84
84
|
/// Skip iteration optimization - use runtime DOM cloning instead of compiled batch functions
|
|
85
85
|
#[arg(long, name = "iterations-as-is")]
|
|
86
86
|
iterations_as_is: bool,
|
|
87
|
+
|
|
88
|
+
/// Skip cleaning output directory before compilation
|
|
89
|
+
#[arg(long, name = "no-clean", hide = true)]
|
|
90
|
+
no_clean: bool,
|
|
91
|
+
|
|
92
|
+
/// Keep FOUC prevention class/attribute in compiled output
|
|
93
|
+
#[arg(long, name = "fouc-as-is")]
|
|
94
|
+
fouc_as_is: bool,
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
fn main() {
|
|
@@ -161,9 +169,15 @@ fn main() {
|
|
|
161
169
|
overrides.iterations_as_is = !original_iterations_as_is;
|
|
162
170
|
config.iterations_as_is = true;
|
|
163
171
|
}
|
|
172
|
+
if args.no_clean {
|
|
173
|
+
config.no_clean = true;
|
|
174
|
+
}
|
|
175
|
+
if args.fouc_as_is {
|
|
176
|
+
config.fouc_as_is = true;
|
|
177
|
+
}
|
|
164
178
|
|
|
165
179
|
if args.verbose {
|
|
166
|
-
println!("{}", "Vibe Compiler".cyan().bold());
|
|
180
|
+
println!("{}", format!("Vibe Compiler v{}", env!("CARGO_PKG_VERSION")).cyan().bold());
|
|
167
181
|
println!();
|
|
168
182
|
println!(" {}: {}", "Working dir".cyan(), config.working_dir.display());
|
|
169
183
|
println!(" {}: {}", "Source".cyan(), config.source.display());
|
|
@@ -191,22 +205,35 @@ fn main() {
|
|
|
191
205
|
format!("{} (config: {})", value.to_string().green(), value.to_string().yellow())
|
|
192
206
|
}
|
|
193
207
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
println!(" {}: {}", format!("{:<
|
|
205
|
-
println!(" {}: {}", format!("{:<
|
|
206
|
-
println!(" {}: {}", format!("{:<
|
|
207
|
-
println!(" {}: {}", format!("{:<
|
|
208
|
-
println!(" {}: {}", format!("{:<
|
|
208
|
+
fn format_reserved_elements(elements: &[String]) -> String {
|
|
209
|
+
if elements.len() <= 2 {
|
|
210
|
+
format!("{:?}", elements)
|
|
211
|
+
} else {
|
|
212
|
+
let remaining = elements.len() - 2;
|
|
213
|
+
format!("[\"component\", \"div\", ... + {} more]", remaining)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Alphabetically ordered with padding (longest key is "reservedElements" = 16 chars)
|
|
218
|
+
println!(" {}: {}", format!("{:<16}", "assets").cyan(), format_value_no_flag(&config._assets));
|
|
219
|
+
println!(" {}: {}", format!("{:<16}", "components").cyan(), format_value_no_flag(&config.components));
|
|
220
|
+
println!(" {}: {}", format!("{:<16}", "componentsAsIs").cyan(), format_bool_with_flag(config.components_as_is, overrides.components_as_is, original_components_as_is));
|
|
221
|
+
println!(" {}: {}", format!("{:<16}", "elementsAsIs").cyan(), format_bool_with_flag(config.elements_as_is, overrides.elements_as_is, original_elements_as_is));
|
|
222
|
+
println!(" {}: {}", format!("{:<16}", "iterationsAsIs").cyan(), format_bool_with_flag(config.iterations_as_is, overrides.iterations_as_is, original_iterations_as_is));
|
|
223
|
+
println!(" {}: {}", format!("{:<16}", "minify").cyan(), format_bool_with_flag(config.minify, overrides.minify, original_minify));
|
|
224
|
+
println!(" {}: {}", format!("{:<16}", "nodeModulesAsIs").cyan(), format_bool_with_flag(config.node_modules_as_is, overrides.node_modules_as_is, original_node_modules_as_is));
|
|
225
|
+
println!(" {}: {}", format!("{:<16}", "output").cyan(), format_value_no_flag(&config._output_str));
|
|
226
|
+
println!(" {}: {}", format!("{:<16}", "pages").cyan(), format_value_no_flag(&config.pages));
|
|
227
|
+
println!(" {}: {}", format!("{:<16}", "reservedElements").cyan(), format_value_no_flag(format_reserved_elements(&config.reserved_elements)));
|
|
228
|
+
println!(" {}: {}", format!("{:<16}", "root").cyan(), format_value_no_flag(config._root.as_ref().map(|s| s.as_str()).unwrap_or("null")));
|
|
229
|
+
println!(" {}: {}", format!("{:<16}", "runtimeAsIs").cyan(), format_bool_with_flag(config.runtime_as_is, overrides.runtime_as_is, original_runtime_as_is));
|
|
230
|
+
println!(" {}: {}", format!("{:<16}", "source").cyan(), format_value_no_flag(&config._source_str));
|
|
231
|
+
println!(" {}: {}", format!("{:<16}", "sourceMaps").cyan(), format_bool_with_flag(config.source_maps, overrides.source_maps, original_source_maps));
|
|
232
|
+
println!(" {}: {}", format!("{:<16}", "validate").cyan(), format_bool_with_flag(config.validate, overrides.validate, original_validate));
|
|
209
233
|
println!();
|
|
234
|
+
} else {
|
|
235
|
+
// Show version in non-verbose mode
|
|
236
|
+
println!("{}", format!("Vibe Compiler v{}", env!("CARGO_PKG_VERSION")).cyan());
|
|
210
237
|
}
|
|
211
238
|
|
|
212
239
|
// Handle watch mode
|
|
@@ -243,7 +270,7 @@ fn main() {
|
|
|
243
270
|
}
|
|
244
271
|
|
|
245
272
|
// Show success headline
|
|
246
|
-
println!("\n{}", "Compilation successful! ✅".green().bold());
|
|
273
|
+
println!("\n{}", format!("Compilation successful! (v{}) ✅", env!("CARGO_PKG_VERSION")).green().bold());
|
|
247
274
|
println!();
|
|
248
275
|
|
|
249
276
|
// Show individual phase timings (validation first, then components, HTML, manifests, copied)
|
|
@@ -71,6 +71,21 @@ impl HtmlParser {
|
|
|
71
71
|
Ok(())
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/// Reload a single element from disk (when the file changes in watch mode)
|
|
75
|
+
pub fn reload_element(&mut self, path: &Path) -> Result<(), ParseError> {
|
|
76
|
+
if path.extension().map_or(false, |ext| ext == "html") {
|
|
77
|
+
if let Some(tag_name) = path.file_stem().and_then(|s| s.to_str()) {
|
|
78
|
+
let content = fs::read_to_string(path).map_err(|e| ParseError::ReadError {
|
|
79
|
+
path: path.display().to_string(),
|
|
80
|
+
source: e,
|
|
81
|
+
})?;
|
|
82
|
+
let element = Element::new(tag_name.to_string(), path.to_path_buf(), content);
|
|
83
|
+
self.cache.insert(tag_name.to_string(), element);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
Ok(())
|
|
87
|
+
}
|
|
88
|
+
|
|
74
89
|
/// Get an element by tag name
|
|
75
90
|
pub fn _get_element(&self, tag_name: &str) -> Option<&Element> {
|
|
76
91
|
self.cache.get(tag_name)
|
|
@@ -115,8 +130,20 @@ impl HtmlParser {
|
|
|
115
130
|
|
|
116
131
|
// Step 2: Handle explicit <component src="..."> elements
|
|
117
132
|
// If components_as_is is false, recursively inline all <component> elements
|
|
133
|
+
// Keep running until no more components are found (handles nested components in slots)
|
|
118
134
|
if !components_as_is {
|
|
119
|
-
|
|
135
|
+
let mut iterations = 0;
|
|
136
|
+
let max_iterations = 50; // Prevent infinite loops
|
|
137
|
+
loop {
|
|
138
|
+
let before = result.clone();
|
|
139
|
+
result = self.inline_component_elements(&result, external_cache);
|
|
140
|
+
iterations += 1;
|
|
141
|
+
|
|
142
|
+
// Stop if no changes or max iterations reached
|
|
143
|
+
if result == before || iterations >= max_iterations {
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
120
147
|
|
|
121
148
|
// NOTE: Don't run inline_custom_elements again here - it causes infinite recursion
|
|
122
149
|
// Custom elements inside components are already processed when the component was cached
|
|
@@ -366,7 +393,7 @@ impl HtmlParser {
|
|
|
366
393
|
replacement = replacement.replace("<slot/>", slot_replacement);
|
|
367
394
|
replacement = replacement.replace("<slot />", slot_replacement);
|
|
368
395
|
|
|
369
|
-
// Keep the <component> wrapper
|
|
396
|
+
// Keep the <component> wrapper (without src attribute)
|
|
370
397
|
let wrapper = format!("<component>{}</component>", replacement);
|
|
371
398
|
result.replace_range(*start..*end, &wrapper);
|
|
372
399
|
}
|
|
@@ -440,16 +467,41 @@ fn transform_custom_tags_to_divs(content: &str, reserved_elements: &[String]) ->
|
|
|
440
467
|
}
|
|
441
468
|
}
|
|
442
469
|
|
|
470
|
+
// Sort by descending length so more specific tags (e.g. "accordion-content")
|
|
471
|
+
// are processed before shorter prefixes (e.g. "accordion"), preventing
|
|
472
|
+
// partial tag-name matches like <accordion([^>]*)> matching <accordion-content>
|
|
473
|
+
custom_tags.sort_by(|a, b| b.len().cmp(&a.len()));
|
|
474
|
+
|
|
475
|
+
// Pre-compile class attribute regex for merging existing class values
|
|
476
|
+
let class_attr_re = regex::Regex::new(r#"\bclass="([^"]*)""#).unwrap();
|
|
477
|
+
|
|
443
478
|
// Transform each custom tag
|
|
444
|
-
for tag in custom_tags {
|
|
445
|
-
// Opening tag:
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
479
|
+
for tag in &custom_tags {
|
|
480
|
+
// Opening tag: require whitespace or end-of-tag after the tag name so that
|
|
481
|
+
// <accordion> does not accidentally match <accordion-content>
|
|
482
|
+
let open_re = regex::Regex::new(
|
|
483
|
+
&format!(r"<{}([\s][^>]*|)>", regex::escape(tag))
|
|
484
|
+
).unwrap();
|
|
485
|
+
|
|
486
|
+
result = open_re.replace_all(&result, |caps: ®ex::Captures| -> String {
|
|
487
|
+
let attrs = caps.get(1).map(|m| m.as_str()).unwrap_or("");
|
|
488
|
+
|
|
489
|
+
// If the element already has class="...", merge tag name with existing value
|
|
490
|
+
if let Some(class_cap) = class_attr_re.captures(attrs) {
|
|
491
|
+
let existing = class_cap.get(1).unwrap().as_str();
|
|
492
|
+
let merged = format!("{} {}", tag, existing);
|
|
493
|
+
let new_attrs = class_attr_re.replace(
|
|
494
|
+
attrs,
|
|
495
|
+
format!(r#"class="{}""#, merged.trim()).as_str(),
|
|
496
|
+
);
|
|
497
|
+
format!("<div{}>", new_attrs)
|
|
498
|
+
} else {
|
|
499
|
+
format!("<div class=\"{}\"{}>", tag, attrs)
|
|
500
|
+
}
|
|
501
|
+
}).to_string();
|
|
450
502
|
|
|
451
503
|
// Closing tag: </custom-tag> -> </div>
|
|
452
|
-
let close_re = regex::Regex::new(&format!(r"</{}>", regex::escape(
|
|
504
|
+
let close_re = regex::Regex::new(&format!(r"</{}>", regex::escape(tag))).unwrap();
|
|
453
505
|
result = close_re.replace_all(&result, "</div>").to_string();
|
|
454
506
|
}
|
|
455
507
|
|
package/index.js
CHANGED
|
@@ -1,15 +1,41 @@
|
|
|
1
1
|
// Universal entry point for Vibe
|
|
2
2
|
// Usage: import vibe from 'vibe/index.js'; vibe({ initialState }, { debug: true }, 'body');
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { boot, isBooted, ensureBoot } from './boot.js';
|
|
5
|
+
|
|
6
|
+
// Shared instance for queueing listeners before boot
|
|
7
|
+
let vibeInstance = null;
|
|
8
|
+
|
|
9
|
+
const createVibeInstance = () => ({
|
|
10
|
+
_pendingListeners: { afterUpdate: [], afterDomMutation: [], ready: [] },
|
|
11
|
+
on(event, callback) {
|
|
12
|
+
// If booted, delegate to window.$
|
|
13
|
+
if (isBooted() && window.$) {
|
|
14
|
+
return window.$.on(event, callback);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Otherwise queue for later
|
|
18
|
+
if (this._pendingListeners[event]) {
|
|
19
|
+
this._pendingListeners[event].push(callback);
|
|
20
|
+
}
|
|
21
|
+
return () => {
|
|
22
|
+
this._pendingListeners[event] = this._pendingListeners[event].filter(cb => cb !== callback);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
});
|
|
5
26
|
|
|
6
27
|
const vibe = (state = {}, config, targetSelector) => {
|
|
7
28
|
if (isBooted()) {
|
|
8
|
-
// Already booted -
|
|
29
|
+
// Already booted - merge state into live proxy
|
|
9
30
|
Object.assign(window.$, state);
|
|
10
31
|
return window.$;
|
|
11
32
|
}
|
|
12
33
|
|
|
34
|
+
// Create shared instance on first call
|
|
35
|
+
if (!vibeInstance) {
|
|
36
|
+
vibeInstance = createVibeInstance();
|
|
37
|
+
}
|
|
38
|
+
|
|
13
39
|
// Not booted yet - accumulate in global state registry
|
|
14
40
|
if (!window.__vibeGlobalState) {
|
|
15
41
|
window.__vibeGlobalState = {};
|
|
@@ -26,15 +52,18 @@ const vibe = (state = {}, config, targetSelector) => {
|
|
|
26
52
|
window.__vibeTargetSelector = targetSelector;
|
|
27
53
|
}
|
|
28
54
|
|
|
29
|
-
// Explicit boot call (no state passed means "boot now")
|
|
55
|
+
// Explicit boot call (no state passed means "boot now with accumulated state")
|
|
30
56
|
if (Object.keys(state).length === 0 && Object.keys(window.__vibeGlobalState).length > 0) {
|
|
31
57
|
return boot();
|
|
32
58
|
}
|
|
33
59
|
|
|
34
|
-
//
|
|
60
|
+
// Queue boot in microtask to allow all component scripts to register
|
|
35
61
|
ensureBoot();
|
|
36
62
|
|
|
37
|
-
return
|
|
63
|
+
return vibeInstance;
|
|
38
64
|
};
|
|
39
65
|
|
|
66
|
+
// Export function to get pending listeners (used by boot.js)
|
|
67
|
+
export const getPendingListeners = () => vibeInstance?._pendingListeners || null;
|
|
68
|
+
|
|
40
69
|
export default vibe;
|
package/llms.txt
CHANGED
|
@@ -129,7 +129,7 @@ Conditionals can be nested inside iterations and vice versa.
|
|
|
129
129
|
Skip reactive processing for an element and its children:
|
|
130
130
|
|
|
131
131
|
```html
|
|
132
|
-
<code dehydrate>@[this] displays literally, not parsed</code>
|
|
132
|
+
<code vibe-dehydrate>@[this] displays literally, not parsed</code>
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
Use cases:
|
package/package.json
CHANGED
package/runtime/cleanup.js
CHANGED
|
@@ -68,13 +68,4 @@ export const cleanup = (rootElement, debug = false) => {
|
|
|
68
68
|
debug,
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
// Dispatch ready event to signal that Vibe has completed all initial processing
|
|
73
|
-
if (typeof document !== 'undefined') {
|
|
74
|
-
document.dispatchEvent(
|
|
75
|
-
new CustomEvent('vibe:ready', {
|
|
76
|
-
detail: { rootElement, cleanName, isClass },
|
|
77
|
-
}),
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
71
|
};
|