//! The conventional filesystem [`ImportResolver`]: a search path of //! directories probed in order for `Name.gdc`, the matching probe for //! `(asset …)` references, and the eager load of the `.gdw` widget manifests //! that make up a crate's extended widget vocabulary. //! //! The compiler itself never touches the filesystem — [`compile_with`] //! (crate root) takes any resolver — but every host (the proc-macro, the //! dev-mode hot-reload runtime, tests) resolves the same way, so the one //! implementation lives here. Only `std::fs` is involved; the crate stays //! dependency-free. //! //! [`compile_with`]: crate::compile_with use std::path::{Path, PathBuf}; use crate::diagnostics; use crate::manifest::compile_manifest; use crate::registry::Registry; use crate::{ImportResolver, WIDGET_MANIFEST_EXT, component_file_name}; /// Locate an `(asset "…")` reference: probe each asset-search directory in /// order for the relative path, first hit wins. /// /// Both consumers of an asset path go through this — the proc-macro turning /// it into an `include_bytes!` target, the dev runtime reading it from disk — /// so a dev mount finds exactly the file the typed build embedded. The error /// lists everywhere that was looked, since "file not found" without the /// search path is the least useful message a build can give. pub fn find_asset(dirs: &[PathBuf], relative: &str) -> Result { for dir in dirs { let candidate = dir.join(relative); if candidate.is_file() { return Ok(candidate); } } Err(format!( "no `{relative}` in the asset search path ({})", describe_dirs(dirs) )) } /// Load every `.gdw` on the widget search path into one [`Registry`]. /// /// Unlike a component, a widget manifest is never resolved by reference: /// the vocabulary decides what counts as a widget name at all, so it has to be /// complete before a single `.gdc` is read. Every manifest on the path is /// therefore loaded up front — which is also what lets "unknown widget" name /// the application's own widgets, and what makes a name claimed by two /// manifests an error instead of a silent first-one-wins. /// /// Files load in path order, and in sorted order within a directory, so a /// duplicate is always reported against the same one of the pair. Directories /// that do not exist are simply empty: configuring a widget path and not using /// it yet is not an error. `visited` collects every manifest read, in load /// order, for the rebuild tracking a host needs. pub fn load_widgets(dirs: &[PathBuf], visited: &mut Vec) -> Result { let mut registry = Registry::default(); for dir in dirs { let Ok(entries) = std::fs::read_dir(dir) else { continue; }; let mut manifests: Vec = entries .filter_map(|entry| entry.ok().map(|e| e.path())) .filter(|path| { path.extension() .is_some_and(|ext| ext == WIDGET_MANIFEST_EXT) }) .collect(); manifests.sort(); for path in manifests { let display = path.display().to_string(); let source = std::fs::read_to_string(&path) .map_err(|e| format!("cannot read widget manifest {display}: {e}"))?; let broken = |diags: &[diagnostics::Diagnostic]| { format!( "invalid widget manifest\n{}", diagnostics::render(diags, &source, &display) ) }; let widgets = compile_manifest(&source).map_err(|diags| broken(&diags))?; for widget in widgets { registry.insert(widget).map_err(|diag| broken(&[diag]))?; } visited.push(path); } } Ok(registry) } fn describe_dirs(dirs: &[impl AsRef]) -> String { if dirs.is_empty() { return "empty".to_owned(); } dirs.iter() .map(|d| d.as_ref().display().to_string()) .collect::>() .join(", ") } /// Resolves component names by probing `/Name.gdc` for each directory /// in order; the first hit wins. Every file read is recorded in `visited`, /// for rebuild tracking (proc-macro) and change watching (dev runtime). pub struct SearchPathResolver { dirs: Vec, /// Every file successfully read, in resolution order. pub visited: Vec, } impl SearchPathResolver { pub fn new(dirs: impl IntoIterator) -> Self { Self { dirs: dirs.into_iter().collect(), visited: Vec::new(), } } /// The directories probed, in order. pub fn dirs(&self) -> &[PathBuf] { &self.dirs } } impl ImportResolver for SearchPathResolver { fn resolve(&mut self, name: &str) -> Result<(String, String), String> { let file = component_file_name(name); for dir in &self.dirs { let candidate = dir.join(&file); if let Ok(source) = std::fs::read_to_string(&candidate) { self.visited.push(candidate.clone()); return Ok((candidate.display().to_string(), source)); } } Err(format!( "no `{file}` in the component search path ({})", describe_dirs(&self.dirs) )) } } #[cfg(test)] mod tests { use super::*; #[test] fn probes_directories_in_order_and_records_visits() { let base = std::env::temp_dir().join(format!("guiduck-resolve-{}", std::process::id())); let (first, second) = (base.join("first"), base.join("second")); std::fs::create_dir_all(&first).unwrap(); std::fs::create_dir_all(&second).unwrap(); std::fs::write(first.join("Both.gdc"), "from first").unwrap(); std::fs::write(second.join("Both.gdc"), "from second").unwrap(); std::fs::write(second.join("OnlySecond.gdc"), "second only").unwrap(); let mut resolver = SearchPathResolver::new([first.clone(), second.clone()]); let (_, source) = resolver.resolve("Both").expect("resolves"); assert_eq!(source, "from first", "first directory wins"); let (_, source) = resolver.resolve("OnlySecond").expect("resolves"); assert_eq!(source, "second only"); let missing = resolver.resolve("Nowhere").expect_err("missing"); assert!(missing.contains("Nowhere.gdc"), "{missing}"); assert!( missing.contains(&first.display().to_string()), "error lists the probed directories: {missing}" ); assert_eq!( resolver.visited, vec![first.join("Both.gdc"), second.join("OnlySecond.gdc")] ); std::fs::remove_dir_all(&base).ok(); } }