Skip to main content

velox_sfc/
component_resolver.rs

1//! Component Resolver for .vx imports
2//!
3//! This module handles importing and resolving .vx components
4//! from other files, enabling component composition.
5
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9use crate::sfc::Sfc;
10use crate::template_ast::Node;
11
12/// Represents an imported component
13#[derive(Debug, Clone)]
14pub struct ComponentImport {
15    /// The local name used in the template (e.g., "MyButton")
16    pub local_name: String,
17    /// The source file path (e.g., "./components/Button.vx")
18    pub source: String,
19    /// Whether this is a default import
20    pub default: bool,
21    /// The parsed SFC (loaded lazily)
22    pub sfc: Option<Sfc>,
23}
24
25/// Resolver for tracking and loading component imports
26#[derive(Debug, Default)]
27pub struct ComponentResolver {
28    /// Map of local names to imports
29    imports: HashMap<String, ComponentImport>,
30    /// Base path for resolving relative imports
31    base_path: PathBuf,
32}
33
34impl ComponentResolver {
35    /// Create a new resolver with a base path for resolving imports
36    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    /// Parse import statements from script content
44    pub fn parse_imports(&mut self, script_content: &str) {
45        for line in script_content.lines() {
46            let line = line.trim();
47
48            // Parse: import MyButton from './components/Button.vx'
49            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                // Handle single default import
60                let name = import_part.trim().to_string();
61                self.add_import(name, source, true);
62            }
63
64            // Parse: import { Button, Card } from './components.vx'
65            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    /// Add an import to the resolver
85    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    /// Check if a tag name is a component import
96    pub fn is_component(&self, tag: &str) -> bool {
97        self.imports.contains_key(tag)
98    }
99
100    /// Get the import for a component name
101    pub fn get_import(&self, name: &str) -> Option<&ComponentImport> {
102        self.imports.get(name)
103    }
104
105    /// Load a component's SFC from disk
106    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    /// Get all imported component names
136    pub fn component_names(&self) -> Vec<String> {
137        self.imports.keys().cloned().collect()
138    }
139
140    /// Resolve the full path for a component source
141    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
151/// Transform a template AST to resolve component tags
152/// This converts custom component tags to a standard representation
153pub 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            // Check if this is a component
163            if resolver.is_component(tag) {
164                // Add a special marker attribute
165                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                // Rename tag to div with component marker
172                // The actual component will be rendered separately
173                *tag = String::from("div");
174            }
175
176            // Recursively transform children
177            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}