weavatrix_rust_refactor/
specifier.rs1#[must_use]
14pub fn is_relative(specifier: &str) -> bool {
15 specifier.starts_with("./") || specifier.starts_with("../")
16}
17
18fn directory_of(path: &str) -> Vec<&str> {
20 let mut segments = path.split('/').collect::<Vec<_>>();
21 segments.pop();
22 segments
23}
24
25#[must_use]
30pub fn resolve(importer: &str, specifier: &str) -> Option<String> {
31 let mut segments = directory_of(importer);
32 for part in specifier.split('/') {
33 match part {
34 "." | "" => {}
35 ".." => {
36 segments.pop()?;
37 }
38 other => segments.push(other),
39 }
40 }
41 Some(segments.join("/"))
42}
43
44#[must_use]
50pub fn between(importer: &str, target: &str) -> String {
51 let from = directory_of(importer);
52 let to = target.split('/').collect::<Vec<_>>();
53 let shared = from
54 .iter()
55 .zip(to.iter())
56 .take_while(|(left, right)| left == right)
57 .count();
58 let mut parts = vec![".."; from.len().saturating_sub(shared)];
59 parts.extend(to[shared..].iter().copied());
60 let joined = parts.join("/");
61 if joined.starts_with("..") {
62 joined
63 } else {
64 format!("./{joined}")
65 }
66}
67
68#[must_use]
73pub fn rewrite(moved_to: &str, original: &str, resolved: &str) -> String {
74 let rewritten = between(moved_to, resolved);
75 let wrote_extension = original
78 .rsplit('/')
79 .next()
80 .is_some_and(|last| last.contains('.') && !last.starts_with('.'));
81 if wrote_extension || !resolved.contains('.') {
82 return rewritten;
83 }
84 rewritten
86 .rsplit_once('.')
87 .map_or(rewritten.clone(), |(stem, _)| stem.to_owned())
88}
89
90#[cfg(test)]
91mod tests {
92 use super::{between, is_relative, resolve, rewrite};
93
94 #[test]
95 fn only_path_specifiers_are_relative() {
96 assert!(is_relative("./core.js"));
97 assert!(is_relative("../lib/y.js"));
98 assert!(!is_relative("crate::core"));
99 assert!(!is_relative("react"));
100 assert!(!is_relative("com.example.Core"));
101 assert!(!is_relative("/absolute/core.js"));
102 }
103
104 #[test]
105 fn a_specifier_resolves_against_the_importing_directory() {
106 assert_eq!(
107 resolve("src/app/main.js", "./core.js"),
108 Some("src/app/core.js".to_owned())
109 );
110 assert_eq!(
111 resolve("src/app/main.js", "../lib/y.js"),
112 Some("src/lib/y.js".to_owned())
113 );
114 assert_eq!(
115 resolve("src/app/main.js", "../../top.js"),
116 Some("top.js".to_owned())
117 );
118 }
119
120 #[test]
121 fn a_specifier_that_escapes_the_root_resolves_to_nothing() {
122 assert_eq!(resolve("main.js", "../outside.js"), None);
123 }
124
125 #[test]
126 fn the_specifier_between_two_files_is_always_explicitly_relative() {
127 assert_eq!(between("src/app/main.js", "src/app/core.js"), "./core.js");
129 assert_eq!(between("src/app/main.js", "src/lib/y.js"), "../lib/y.js");
130 assert_eq!(between("main.js", "src/deep/x.js"), "./src/deep/x.js");
131 }
132
133 #[test]
134 fn a_round_trip_through_resolve_and_between_is_stable() {
135 for (importer, specifier) in [
136 ("src/app/main.js", "./core.js"),
137 ("src/app/main.js", "../lib/y.js"),
138 ("a/b/c/d.js", "../../e.js"),
139 ] {
140 let resolved = resolve(importer, specifier).expect("resolves");
141 assert_eq!(
142 between(importer, &resolved),
143 specifier,
144 "{specifier} from {importer} did not survive the round trip"
145 );
146 }
147 }
148
149 #[test]
150 fn moving_a_file_deeper_adds_the_climb_its_imports_need() {
151 let resolved = resolve("src/main.js", "./core.js").expect("resolves");
153 assert_eq!(
154 rewrite("src/deep/main.js", "./core.js", &resolved),
155 "../core.js"
156 );
157 }
158
159 #[test]
160 fn moving_a_file_up_removes_the_climb() {
161 let resolved = resolve("src/deep/main.js", "../core.js").expect("resolves");
162 assert_eq!(rewrite("src/main.js", "../core.js", &resolved), "./core.js");
163 }
164
165 #[test]
166 fn an_extensionless_specifier_stays_extensionless() {
167 let resolved = "src/core.js";
168 assert_eq!(rewrite("src/deep/main.js", "./core", resolved), "../core");
169 }
170
171 #[test]
172 fn an_explicit_extension_is_kept() {
173 let resolved = "src/core.js";
174 assert_eq!(
175 rewrite("src/deep/main.js", "./core.js", resolved),
176 "../core.js"
177 );
178 }
179}