resolve.rs
raw
//! 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<PathBuf, String> {
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<PathBuf>) -> Result<Registry, String> {
let mut registry = Registry::default();
for dir in dirs {
let Ok(entries) = std::fs::read_dir(dir) else {
continue;
};
let mut manifests: Vec<PathBuf> = 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<Path>]) -> String {
if dirs.is_empty() {
return "empty".to_owned();
}
dirs.iter()
.map(|d| d.as_ref().display().to_string())
.collect::<Vec<_>>()
.join(", ")
}
/// Resolves component names by probing `<dir>/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<PathBuf>,
/// Every file successfully read, in resolution order.
pub visited: Vec<PathBuf>,
}
impl SearchPathResolver {
pub fn new(dirs: impl IntoIterator<Item = PathBuf>) -> 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();
}
}