use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::sfc::Sfc;
use crate::template_ast::Node;
#[derive(Debug, Clone)]
pub struct ComponentImport {
pub local_name: String,
pub source: String,
pub default: bool,
pub sfc: Option<Sfc>,
}
#[derive(Debug, Default)]
pub struct ComponentResolver {
imports: HashMap<String, ComponentImport>,
base_path: PathBuf,
}
impl ComponentResolver {
pub fn new(base_path: impl Into<PathBuf>) -> Self {
Self {
imports: HashMap::new(),
base_path: base_path.into(),
}
}
pub fn parse_imports(&mut self, script_content: &str) {
for line in script_content.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("import ")
&& let Some((import_part, source)) = rest.split_once(" from ")
{
let source = source
.trim()
.trim_end_matches(';')
.trim()
.trim_matches('"')
.trim_matches('\'');
let name = import_part.trim().to_string();
self.add_import(name, source, true);
}
if let Some(rest) = line.strip_prefix("import {")
&& let Some((names, source)) = rest.split_once("} from ")
{
let source = source
.trim()
.trim_end_matches(';')
.trim()
.trim_matches('"')
.trim_matches('\'');
for name in names.split(',') {
let name = name.trim();
if !name.is_empty() {
self.add_import(name.to_string(), source, false);
}
}
}
}
}
fn add_import(&mut self, local_name: String, source: &str, default: bool) {
let import = ComponentImport {
local_name: local_name.clone(),
source: source.to_string(),
default,
sfc: None,
};
self.imports.insert(local_name, import);
}
pub fn is_component(&self, tag: &str) -> bool {
self.imports.contains_key(tag)
}
pub fn get_import(&self, name: &str) -> Option<&ComponentImport> {
self.imports.get(name)
}
pub fn load_component(&mut self, name: &str) -> Result<&Sfc, String> {
if let Some(import) = self.imports.get(name) {
if import.sfc.is_none() {
let source_path = if Path::new(&import.source).is_absolute() {
PathBuf::from(&import.source)
} else {
self.base_path.join(&import.source)
};
let content = std::fs::read_to_string(&source_path)
.map_err(|e| format!("Failed to read {}: {}", source_path.display(), e))?;
let sfc = crate::parse_sfc(&content)?;
if let Some(import) = self.imports.get_mut(name) {
import.sfc = Some(sfc);
}
}
return self
.imports
.get(name)
.and_then(|i| i.sfc.as_ref())
.ok_or_else(|| format!("Component {} not loaded", name));
}
Err(format!("Component {} not found in imports", name))
}
pub fn component_names(&self) -> Vec<String> {
self.imports.keys().cloned().collect()
}
pub fn resolve_path(&self, source: &str) -> PathBuf {
let path = Path::new(source);
if path.is_absolute() {
path.to_path_buf()
} else {
self.base_path.join(path)
}
}
}
pub fn transform_components(nodes: &mut [Node], resolver: &ComponentResolver) {
for node in nodes.iter_mut() {
if let Node::Element {
tag,
attrs,
children,
..
} = node
{
if resolver.is_component(tag) {
attrs.push(crate::template_ast::TemplateAttr {
name: String::from("data-velox-component"),
value: Some(tag.clone()),
kind: crate::template_ast::AttrKind::Static,
});
*tag = String::from("div");
}
transform_components(children, resolver);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_default_import() {
let mut resolver = ComponentResolver::new("/tmp");
resolver.parse_imports(r#"import MyButton from './components/Button.vx'"#);
assert!(resolver.is_component("MyButton"));
let import = resolver.get_import("MyButton").unwrap();
assert_eq!(import.source, "./components/Button.vx");
assert!(import.default);
}
#[test]
fn test_parse_named_imports() {
let mut resolver = ComponentResolver::new("/tmp");
resolver.parse_imports(r#"import { Button, Card } from './components.vx'"#);
assert!(resolver.is_component("Button"));
assert!(resolver.is_component("Card"));
}
#[test]
fn test_resolve_path() {
let resolver = ComponentResolver::new("/home/project/src");
let relative = resolver.resolve_path("./components/Button.vx");
assert_eq!(
relative,
PathBuf::from("/home/project/src/components/Button.vx")
);
let absolute = resolver.resolve_path("/usr/lib/components.vx");
assert_eq!(absolute, PathBuf::from("/usr/lib/components.vx"));
}
}