harn_modules/
package_imports.rs1use std::collections::{HashMap, HashSet};
2use std::path::{Component, Path, PathBuf};
3
4use serde::Deserialize;
5
6use crate::package_snapshot::PackageSnapshot;
7
8#[derive(Debug, Default, Deserialize)]
9struct PackageManifest {
10 #[serde(default)]
11 exports: HashMap<String, String>,
12}
13
14pub fn resolve_import_path(current_file: &Path, import_path: &str) -> Option<PathBuf> {
19 let snapshots = PackageSnapshot::acquire_nearest(current_file)
20 .ok()
21 .flatten()
22 .into_iter()
23 .collect::<Vec<_>>();
24 resolve_import_path_with_snapshots(current_file, import_path, &snapshots)
25}
26
27pub(crate) fn resolve_import_path_with_snapshots(
28 current_file: &Path,
29 import_path: &str,
30 package_snapshots: &[PackageSnapshot],
31) -> Option<PathBuf> {
32 if let Some(module) = import_path
33 .strip_prefix("std/")
34 .or_else(|| (import_path == "observability").then_some("observability"))
35 {
36 return super::stdlib::get_stdlib_source(module)
37 .map(|_| super::stdlib::stdlib_virtual_path(module));
38 }
39
40 let base = current_file.parent().unwrap_or(Path::new("."));
41 let mut file_path = base.join(import_path);
42 if !file_path.exists() && file_path.extension().is_none() {
43 file_path.set_extension("harn");
44 }
45 if file_path.exists() {
46 return Some(file_path);
47 }
48
49 resolve_package_import(current_file, import_path, package_snapshots)
50}
51
52pub(crate) fn acquire_package_snapshots(files: &[PathBuf]) -> Vec<PackageSnapshot> {
53 let mut roots = HashSet::new();
54 let mut snapshots = Vec::new();
55 for file in files {
56 let Ok(Some(snapshot)) = PackageSnapshot::acquire_nearest(file) else {
57 continue;
58 };
59 if roots.insert(snapshot.project_root().to_path_buf()) {
60 snapshots.push(snapshot);
61 }
62 }
63 snapshots
64}
65
66fn resolve_package_import(
67 current_file: &Path,
68 import_path: &str,
69 package_snapshots: &[PackageSnapshot],
70) -> Option<PathBuf> {
71 let current_file = canonicalize_with_existing_parent(current_file);
72 package_snapshots
73 .iter()
74 .filter(|snapshot| current_file.starts_with(snapshot.project_root()))
75 .max_by_key(|snapshot| snapshot.project_root().components().count())
76 .and_then(|snapshot| resolve_from_packages_root(snapshot.packages_root(), import_path))
77}
78
79fn canonicalize_with_existing_parent(path: &Path) -> PathBuf {
80 path.canonicalize().unwrap_or_else(|_| {
81 path.parent()
82 .and_then(|parent| parent.canonicalize().ok())
83 .and_then(|parent| path.file_name().map(|name| parent.join(name)))
84 .unwrap_or_else(|| path.to_path_buf())
85 })
86}
87
88fn resolve_from_packages_root(packages_root: &Path, import_path: &str) -> Option<PathBuf> {
89 let safe_import_path = safe_package_relative_path(import_path)?;
90 let package_name = package_name_from_relative_path(&safe_import_path)?;
91 let package_root = packages_root.join(package_name);
92
93 let direct_path = packages_root.join(&safe_import_path);
94 if let Some(path) = finalize_package_target(&package_root, &direct_path) {
95 return Some(path);
96 }
97
98 let export_name = export_name_from_relative_path(&safe_import_path)?;
99 let manifest = read_package_manifest(&package_root.join("harn.toml"))?;
100 let safe_export_path = safe_package_relative_path(manifest.exports.get(export_name)?)?;
101 finalize_package_target(&package_root, &package_root.join(safe_export_path))
102}
103
104fn read_package_manifest(path: &Path) -> Option<PackageManifest> {
105 let content = std::fs::read_to_string(path).ok()?;
106 toml::from_str(&content).ok()
107}
108
109fn safe_package_relative_path(raw: &str) -> Option<PathBuf> {
110 if raw.is_empty() || raw.contains('\\') {
111 return None;
112 }
113 let mut out = PathBuf::new();
114 let mut saw_component = false;
115 for component in Path::new(raw).components() {
116 match component {
117 Component::Normal(part) => {
118 saw_component = true;
119 out.push(part);
120 }
121 Component::CurDir => {}
122 Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
123 }
124 }
125 saw_component.then_some(out)
126}
127
128fn package_name_from_relative_path(path: &Path) -> Option<&str> {
129 match path.components().next()? {
130 Component::Normal(name) => name.to_str(),
131 _ => None,
132 }
133}
134
135fn export_name_from_relative_path(path: &Path) -> Option<&str> {
136 let mut components = path.components();
137 components.next()?;
138 let rest = components.as_path();
139 if rest.as_os_str().is_empty() {
140 None
141 } else {
142 rest.to_str()
143 }
144}
145
146fn target_within_package_root(package_root: &Path, path: PathBuf) -> Option<PathBuf> {
147 let root = package_root.canonicalize().ok()?;
148 let canonical = path.canonicalize().ok()?;
149 (canonical == root || canonical.starts_with(&root)).then_some(path)
150}
151
152fn finalize_package_target(package_root: &Path, path: &Path) -> Option<PathBuf> {
153 if path.is_dir() {
154 let lib = path.join("lib.harn");
155 return if lib.exists() {
156 target_within_package_root(package_root, lib)
157 } else {
158 target_within_package_root(package_root, path.to_path_buf())
159 };
160 }
161 if path.exists() {
162 return target_within_package_root(package_root, path.to_path_buf());
163 }
164 if path.extension().is_none() {
165 let mut with_extension = path.to_path_buf();
166 with_extension.set_extension("harn");
167 if with_extension.exists() {
168 return target_within_package_root(package_root, with_extension);
169 }
170 }
171 None
172}