@ape-egg/vibe-compiler 4.3.23 → 4.3.25

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.
Binary file
Binary file
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@ape-egg/vibe-compiler",
3
- "version": "4.3.23",
3
+ "version": "4.3.25",
4
4
  "type": "module",
5
- "description": "Optional Rust compiler for Vibe \u2014 pre-renders, inlines components, and optimizes iterations. The runtime never needs it.",
5
+ "description": "Optional Rust compiler for Vibe — pre-renders, inlines components, and optimizes iterations. The runtime never needs it.",
6
6
  "homepage": "https://vibe.korte.kim",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/kkortes/ape-egg.git",
10
+ "directory": "packages/vibe-compiler"
11
+ },
7
12
  "bin": {
8
13
  "vibe": "./bin/vibe-compile.js"
9
14
  },
@@ -29,7 +34,8 @@
29
34
  "author": "kkortes",
30
35
  "license": "ISC",
31
36
  "publishConfig": {
32
- "access": "public"
37
+ "access": "public",
38
+ "registry": "https://registry.npmjs.org"
33
39
  },
34
40
  "dependencies": {}
35
41
  }
package/src/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "vibe-compiler"
3
- version = "4.3.23"
3
+ version = "4.3.25"
4
4
  edition = "2021"
5
5
  description = "Vibe framework compiler - compiles Vibe source files into optimized output"
6
6
  authors = ["Kim Korte"]
@@ -11,7 +11,7 @@ use rayon::prelude::*;
11
11
  use serde_json::{Map, Value};
12
12
 
13
13
  use crate::config::Config;
14
- use crate::parser::HtmlParser;
14
+ use crate::parser::{HtmlParser, component_key};
15
15
 
16
16
  /// Map an output-relative HTML path to the URL path the runtime resolves a
17
17
  /// manifest from. Strips the `root` dir (served at /) and collapses dynamic
@@ -239,13 +239,13 @@ fn extract_event_handlers(html: &str) -> Vec<String> {
239
239
  /// the inliner's `src=` regex won't match the renamed attribute, so it stops
240
240
  /// re-expanding. `process_html_with_cache` restores the attribute to plain
241
241
  /// `src=` at the end of compilation so the runtime fetches it normally.
242
- fn escape_recursive_src(content: &str, component_src: &str) -> String {
243
- let pattern = format!(
244
- r#"(<(?:component|div)\s+[^>]*\b)src="{}""#,
245
- regex::escape(component_src),
246
- );
247
- let re = Regex::new(&pattern).unwrap();
248
- re.replace_all(content, format!(r#"$1{}="{}""#, RECURSIVE_SRC_ATTR, component_src))
242
+ fn escape_recursive_src(content: &str, key: &str) -> String {
243
+ Regex::new(r#"(<(?:component|div)\s(?:[^>]*\s)?)src="([^"]+)""#)
244
+ .unwrap()
245
+ .replace_all(content, |cap: &regex::Captures| match component_key(&cap[2]) == key {
246
+ true => format!(r#"{}{}="{}""#, &cap[1], RECURSIVE_SRC_ATTR, &cap[2]),
247
+ false => cap[0].to_string(),
248
+ })
249
249
  .into_owned()
250
250
  }
251
251
 
@@ -1875,19 +1875,19 @@ impl Compiler {
1875
1875
  }
1876
1876
 
1877
1877
  fn collect_component_children(&mut self, component_src: &str, relationships: &mut Vec<(String, Vec<String>)>, visited: &mut HashSet<String>) {
1878
- if visited.contains(component_src) {
1878
+ let key = component_key(component_src);
1879
+ if !visited.insert(key.clone()) {
1879
1880
  return;
1880
1881
  }
1881
- visited.insert(component_src.to_string());
1882
1882
 
1883
1883
  // Read component content
1884
- let content = if component_src.starts_with("http://") || component_src.starts_with("https://") {
1885
- match self.fetch_external_component_raw(component_src) {
1884
+ let content = if key.starts_with("http://") || key.starts_with("https://") {
1885
+ match self.fetch_external_component_raw(&key) {
1886
1886
  Ok(c) => c,
1887
1887
  Err(_) => return,
1888
1888
  }
1889
1889
  } else {
1890
- match self.resolve_component_path(component_src) {
1890
+ match self.resolve_component_path(&key) {
1891
1891
  Ok(path) => match fs::read_to_string(&path) {
1892
1892
  Ok(c) => c,
1893
1893
  Err(_) => return,
@@ -2050,17 +2050,7 @@ impl Compiler {
2050
2050
  parser: &HtmlParser,
2051
2051
  visiting: &mut std::collections::HashSet<String>,
2052
2052
  ) -> Option<String> {
2053
- // Normalize path: ensure it starts with / (unless it's a URL)
2054
- let normalized_src = if component_src.starts_with("http://") || component_src.starts_with("https://") {
2055
- component_src.to_string()
2056
- } else {
2057
- let without_prefix = component_src.trim_start_matches("./");
2058
- if without_prefix.starts_with('/') {
2059
- without_prefix.to_string()
2060
- } else {
2061
- format!("/{}", without_prefix)
2062
- }
2063
- };
2053
+ let normalized_src = component_key(component_src);
2064
2054
 
2065
2055
  // Return cached if already fetched and fully resolved
2066
2056
  if let Some(cached) = self.component_cache.get(&normalized_src) {
@@ -2744,6 +2734,19 @@ fn count_newlines(bytes: &[u8]) -> usize {
2744
2734
  mod tests {
2745
2735
  use super::*;
2746
2736
 
2737
+ #[test]
2738
+ fn escape_recursive_src_matches_a_cycle_written_without_extension() {
2739
+ let out = escape_recursive_src(
2740
+ r#"<component src="/tree"></component><component src="/leaf.html"></component>"#,
2741
+ "/tree.html",
2742
+ );
2743
+ assert_eq!(
2744
+ out,
2745
+ format!(r#"<component {RECURSIVE_SRC_ATTR}="/tree"></component><component src="/leaf.html"></component>"#),
2746
+ );
2747
+ assert_eq!(escape_recursive_src(&out, "/tree.html"), out, "an escaped src was escaped again");
2748
+ }
2749
+
2747
2750
  #[test]
2748
2751
  fn minify_preserves_script_newlines_so_line_comments_dont_swallow_code() {
2749
2752
  // A `//` line comment inside a component script relies on its trailing
@@ -622,10 +622,10 @@ mod tests {
622
622
  #[test]
623
623
  fn harvests_and_derives_shell_imports() {
624
624
  assert_eq!(
625
- harvest_vibe_import("import vibe from '/nodemodules/@ape-egg/vibe/index.js';"),
626
- Some("/nodemodules/@ape-egg/vibe/index.js".to_string())
625
+ harvest_vibe_import("import vibe from '/node_modules/@ape-egg/vibe/index.js';"),
626
+ Some("/node_modules/@ape-egg/vibe/index.js".to_string())
627
627
  );
628
- assert_eq!(spa_import_for("/nodemodules/@ape-egg/vibe/index.js"), "/nodemodules/@ape-egg/vibe/spa.js");
628
+ assert_eq!(spa_import_for("/node_modules/@ape-egg/vibe/index.js"), "/node_modules/@ape-egg/vibe/spa.js");
629
629
  assert_eq!(spa_import_for("@ape-egg/vibe"), "@ape-egg/vibe/spa");
630
630
  }
631
631
 
@@ -8,6 +8,7 @@ use regex::Regex;
8
8
 
9
9
  use super::{Compiler, compile::should_skip_path, shutdown};
10
10
  use crate::config::Config;
11
+ use crate::parser::component_key;
11
12
  use crate::constants::{HYPERSPEED_DIR, MANIFEST_SUFFIX};
12
13
 
13
14
  /// Tracks which pages and components depend on which components
@@ -159,8 +160,7 @@ pub fn extract_component_dependencies(html: &str, components_dir: &Path) -> Hash
159
160
  let component_tag_re = Regex::new(r#"<component\s+[^>]*src=["']([^"']+)["']"#).unwrap();
160
161
  for cap in component_tag_re.captures_iter(html) {
161
162
  if let Some(src) = cap.get(1) {
162
- let src_path = src.as_str().trim_start_matches('/');
163
- deps.insert(PathBuf::from(src_path));
163
+ deps.insert(PathBuf::from(component_key(src.as_str()).trim_start_matches('/')));
164
164
  }
165
165
  }
166
166
 
@@ -7,6 +7,22 @@ use super::element::{Element, ElementCache};
7
7
  use crate::inert::Inert;
8
8
  use crate::constants::{RECURSIVE_SRC_ATTR, MODULE_SCRIPT_TYPE};
9
9
 
10
+ /// The key a `<component src>` is fetched and cached under: rooted at `/` unless
11
+ /// it is a URL, with `.html` appended when its last path segment has no
12
+ /// extension, so `/vibe/counter` and `/vibe/counter.html` are one component.
13
+ /// Mirrors `fetchComponentTemplate` in the vibe runtime.
14
+ pub fn component_key(src: &str) -> String {
15
+ let rooted = if src.starts_with("http://") || src.starts_with("https://") {
16
+ src.to_string()
17
+ } else {
18
+ format!("/{}", src.trim_start_matches("./").trim_start_matches('/'))
19
+ };
20
+ regex::Regex::new(r"^((?:[^?#]*/)?[^/?#.]+)([?#].*)?$")
21
+ .unwrap()
22
+ .replace(&rooted, "$1.html$2")
23
+ .into_owned()
24
+ }
25
+
10
26
  #[derive(Error, Debug)]
11
27
  pub enum ParseError {
12
28
  #[error("Failed to read file {path}: {source}")]
@@ -449,18 +465,7 @@ impl HtmlParser {
449
465
 
450
466
  let mut applied = false;
451
467
  for (start, end, src, props, slot_content) in sorted {
452
- let normalized_src = if src.starts_with("http://") || src.starts_with("https://") {
453
- src.to_string()
454
- } else {
455
- let without_prefix = src.trim_start_matches("./");
456
- if without_prefix.starts_with('/') {
457
- without_prefix.to_string()
458
- } else {
459
- format!("/{}", without_prefix)
460
- }
461
- };
462
-
463
- if let Some(template) = external_cache.get(&normalized_src) {
468
+ if let Some(template) = external_cache.get(&component_key(&src)) {
464
469
  // Iter-prop components stay as runtime `<component src>` tags so the
465
470
  // runtime renders them via its __vibe.iterProps path — inlining
466
471
  // breaks the enclosing loop's scope (see is_iter_prop_root).
@@ -891,6 +896,36 @@ fn transform_custom_tags(content: &str, reserved_elements: &[String]) -> String
891
896
  mod tests {
892
897
  use super::*;
893
898
 
899
+ #[test]
900
+ fn component_key_appends_html_to_an_extensionless_src() {
901
+ let cases = [
902
+ ("/vibe/counter", "/vibe/counter.html"),
903
+ ("vibe/counter", "/vibe/counter.html"),
904
+ ("./vibe/counter", "/vibe/counter.html"),
905
+ ("/vibe/counter.html", "/vibe/counter.html"),
906
+ ("/vibe/counter/v2.html", "/vibe/counter/v2.html"),
907
+ ("/vibe/counter?v=2", "/vibe/counter.html?v=2"),
908
+ ("https://x.com/vibe/counter#a", "https://x.com/vibe/counter.html#a"),
909
+ ("/v1.2/counter", "/v1.2/counter.html"),
910
+ ("/a/b.html?x=/y", "/a/b.html?x=/y"),
911
+ ("/dir/", "/dir/"),
912
+ ];
913
+ for (src, key) in cases {
914
+ assert_eq!(component_key(src), key, "src {src}");
915
+ }
916
+ }
917
+
918
+ #[test]
919
+ fn inline_component_without_extension_uses_the_html_template() {
920
+ let parser = HtmlParser::new(std::path::PathBuf::from("."));
921
+ let mut cache = HashMap::new();
922
+ cache.insert("/components/Potion.html".to_string(), "<icon potion></icon>".to_string());
923
+
924
+ let out = parser.inline_component_elements("<wrap><component src=\"/components/Potion\"></component></wrap>", &cache);
925
+
926
+ assert_eq!(out, "<wrap><component><icon potion></icon></component></wrap>");
927
+ }
928
+
894
929
  #[test]
895
930
  fn find_matching_close_tolerates_whitespace_in_end_tag() {
896
931
  // Whitespace-controlled markup splits an end tag across lines, e.g.
package/src/parser/mod.rs CHANGED
@@ -1,7 +1,7 @@
1
1
  pub mod html;
2
2
  mod element;
3
3
 
4
- pub use html::HtmlParser;
4
+ pub use html::{HtmlParser, component_key};
5
5
  #[allow(unused_imports)]
6
6
  pub use html::ParseError;
7
7
  #[allow(unused_imports)]