velox_sfc/
component_resolver.rs1use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9use crate::sfc::Sfc;
10use crate::template_ast::Node;
11
12#[derive(Debug, Clone)]
14pub struct ComponentImport {
15 pub local_name: String,
17 pub source: String,
19 pub default: bool,
21 pub sfc: Option<Sfc>,
23}
24
25#[derive(Debug, Default)]
27pub struct ComponentResolver {
28 imports: HashMap<String, ComponentImport>,
30 base_path: PathBuf,
32}
33
34impl ComponentResolver {
35 pub fn new(base_path: impl Into<PathBuf>) -> Self {
37 Self {
38 imports: HashMap::new(),
39 base_path: base_path.into(),
40 }
41 }
42
43 pub fn parse_imports(&mut self, script_content: &str) {
45 for line in script_content.lines() {
46 let line = line.trim();
47
48 if let Some(rest) = line.strip_prefix("import ")
50 && let Some((import_part, source)) = rest.split_once(" from ")
51 {
52 let source = source
53 .trim()
54 .trim_end_matches(';')
55 .trim()
56 .trim_matches('"')
57 .trim_matches('\'');
58
59 let name = import_part.trim().to_string();
61 self.add_import(name, source, true);
62 }
63
64 if let Some(rest) = line.strip_prefix("import {")
66 && let Some((names, source)) = rest.split_once("} from ")
67 {
68 let source = source
69 .trim()
70 .trim_end_matches(';')
71 .trim()
72 .trim_matches('"')
73 .trim_matches('\'');
74 for name in names.split(',') {
75 let name = name.trim();
76 if !name.is_empty() {
77 self.add_import(name.to_string(), source, false);
78 }
79 }
80 }
81 }
82 }
83
84 fn add_import(&mut self, local_name: String, source: &str, default: bool) {
86 let import = ComponentImport {
87 local_name: local_name.clone(),
88 source: source.to_string(),
89 default,
90 sfc: None,
91 };
92 self.imports.insert(local_name, import);
93 }
94
95 pub fn is_component(&self, tag: &str) -> bool {
97 self.imports.contains_key(tag)
98 }
99
100 pub fn get_import(&self, name: &str) -> Option<&ComponentImport> {
102 self.imports.get(name)
103 }
104
105 pub fn load_component(&mut self, name: &str) -> Result<&Sfc, String> {
107 if let Some(import) = self.imports.get(name) {
108 if import.sfc.is_none() {
109 let source_path = if Path::new(&import.source).is_absolute() {
110 PathBuf::from(&import.source)
111 } else {
112 self.base_path.join(&import.source)
113 };
114
115 let content = std::fs::read_to_string(&source_path)
116 .map_err(|e| format!("Failed to read {}: {}", source_path.display(), e))?;
117
118 let sfc = crate::parse_sfc(&content)?;
119
120 if let Some(import) = self.imports.get_mut(name) {
121 import.sfc = Some(sfc);
122 }
123 }
124
125 return self
126 .imports
127 .get(name)
128 .and_then(|i| i.sfc.as_ref())
129 .ok_or_else(|| format!("Component {} not loaded", name));
130 }
131
132 Err(format!("Component {} not found in imports", name))
133 }
134
135 pub fn component_names(&self) -> Vec<String> {
137 self.imports.keys().cloned().collect()
138 }
139
140 pub fn resolve_path(&self, source: &str) -> PathBuf {
142 let path = Path::new(source);
143 if path.is_absolute() {
144 path.to_path_buf()
145 } else {
146 self.base_path.join(path)
147 }
148 }
149}
150
151pub fn transform_components(nodes: &mut [Node], resolver: &ComponentResolver) {
154 for node in nodes.iter_mut() {
155 if let Node::Element {
156 tag,
157 attrs,
158 children,
159 ..
160 } = node
161 {
162 if resolver.is_component(tag) {
164 attrs.push(crate::template_ast::TemplateAttr {
166 name: String::from("data-velox-component"),
167 value: Some(tag.clone()),
168 kind: crate::template_ast::AttrKind::Static,
169 });
170
171 *tag = String::from("div");
174 }
175
176 transform_components(children, resolver);
178 }
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_parse_default_import() {
188 let mut resolver = ComponentResolver::new("/tmp");
189 resolver.parse_imports(r#"import MyButton from './components/Button.vx'"#);
190
191 assert!(resolver.is_component("MyButton"));
192 let import = resolver.get_import("MyButton").unwrap();
193 assert_eq!(import.source, "./components/Button.vx");
194 assert!(import.default);
195 }
196
197 #[test]
198 fn test_parse_named_imports() {
199 let mut resolver = ComponentResolver::new("/tmp");
200 resolver.parse_imports(r#"import { Button, Card } from './components.vx'"#);
201
202 assert!(resolver.is_component("Button"));
203 assert!(resolver.is_component("Card"));
204 }
205
206 #[test]
207 fn test_resolve_path() {
208 let resolver = ComponentResolver::new("/home/project/src");
209
210 let relative = resolver.resolve_path("./components/Button.vx");
211 assert_eq!(
212 relative,
213 PathBuf::from("/home/project/src/components/Button.vx")
214 );
215
216 let absolute = resolver.resolve_path("/usr/lib/components.vx");
217 assert_eq!(absolute, PathBuf::from("/usr/lib/components.vx"));
218 }
219}