1use std::path::PathBuf;
2
3use oxc_resolver::{Resolution, ResolveOptions, Resolver, TsconfigOptions, TsconfigReferences};
4
5pub struct ProjectResolver {
6 tsconfig_path: Option<PathBuf>,
7}
8
9impl ProjectResolver {
10 pub fn new(tsconfig_path: Option<PathBuf>) -> Self {
11 Self { tsconfig_path }
12 }
13
14 pub fn resolve(&self, dir: &PathBuf, specifier: &str) -> Result<Resolution, String> {
31 let options = ResolveOptions {
32 tsconfig: self.tsconfig_path.as_ref().map(|tsconfig| TsconfigOptions {
33 config_file: tsconfig.to_path_buf(),
34 references: TsconfigReferences::Auto,
35 }),
36 condition_names: vec![
37 "default".to_string(),
38 "types".to_string(),
39 "import".to_string(),
40 "require".to_string(),
41 "node".to_string(),
42 "node-addons".to_string(),
43 "browser".to_string(),
44 "esm2020".to_string(),
45 "es2020".to_string(),
46 "es2015".to_string(),
47 ],
48 extensions: vec![
49 ".ts".to_string(),
50 ".tsx".to_string(),
51 ".d.ts".to_string(),
52 ".js".to_string(),
53 ".jsx".to_string(),
54 ".mjs".to_string(),
55 ".cjs".to_string(),
56 ".json".to_string(),
57 ".node".to_string(),
58 ],
59 extension_alias: vec![
60 (
61 ".js".to_string(),
62 vec![
63 ".ts".to_string(),
64 ".tsx".to_string(),
65 ".d.ts".to_string(),
66 ".js".to_string(),
67 ],
68 ),
69 (
70 ".jsx".to_string(),
71 vec![".tsx".to_string(), ".d.ts".to_string(), ".jsx".to_string()],
72 ),
73 (
74 ".mjs".to_string(),
75 vec![".mts".to_string(), ".mjs".to_string()],
76 ),
77 (
78 ".cjs".to_string(),
79 vec![".cts".to_string(), ".cjs".to_string()],
80 ),
81 ],
82 main_fields: vec![
83 "types".to_string(),
84 "typings".to_string(),
85 "module".to_string(),
86 "main".to_string(),
87 "browser".to_string(),
88 "jsnext:main".to_string(),
89 ],
90 alias_fields: vec![vec!["browser".to_string()]],
91 ..ResolveOptions::default()
92 };
93
94 match Resolver::new(options).resolve(dir, &specifier) {
95 Ok(resolved_path) => Ok(resolved_path),
96 Err(e) => Err(e.to_string()),
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use crate::util::test_utils;
104
105 use super::*;
106
107 #[test]
108 fn test_resolve_file_path() {
109 let temp_dir = test_utils::create_mock_project(&vec![
110 (
111 "src/components/Button.tsx",
112 "export function Button() { return <div>Button</div>; }",
113 ),
114 (
115 "src/components/index.ts",
116 "import { Button } from './Button';",
117 ),
118 ]);
119
120 let specifier = "./components/Button";
121 let resolver = ProjectResolver::new(None);
122 let resolution = resolver.resolve(&temp_dir.path().join("src"), &specifier);
123
124 assert!(resolution.is_ok());
125 assert_eq!(
126 resolution.unwrap().path(),
127 temp_dir.path().join("src/components/Button.tsx")
128 );
129 }
130
131 #[test]
132 fn test_resolve_file_path_with_tsconfig_paths() {
133 let temp_dir = test_utils::create_mock_project(&vec![
134 (
135 "tsconfig.json",
136 r#"{
137 "compilerOptions": {
138 "baseUrl": ".",
139 "paths": {
140 "@components/*": ["./src/components/*"]
141 }
142 }
143 }"#,
144 ),
145 (
146 "src/components/Button.tsx",
147 "export function Button() { return <div>Button</div>; }",
148 ),
149 (
150 "src/components/index.ts",
151 "import { Button } from './Button';",
152 ),
153 ]);
154
155 let specifier = "@components/Button";
156 let resolver = ProjectResolver::new(Some(temp_dir.path().join("tsconfig.json")));
157 let resolution = resolver.resolve(&temp_dir.path().join("src"), &specifier);
158
159 assert!(resolution.is_ok());
160 assert_eq!(
161 resolution.unwrap().path(),
162 temp_dir.path().join("src/components/Button.tsx")
163 );
164 }
165
166 #[test]
167 fn test_resolve_file_path_with_node_modules() {
168 let temp_dir = test_utils::create_mock_project(&vec![
169 (
170 "node_modules/framer-motion/index.js",
171 "module.exports = { motion: () => <div>Framer Motion</div> };",
172 ),
173 (
174 "src/components/Button.tsx",
175 "export function Button() { return <div>Button</div>; }",
176 ),
177 (
178 "src/components/index.ts",
179 "import { Button } from './Button';",
180 ),
181 ]);
182
183 let specifier = "framer-motion";
184 let resolver = ProjectResolver::new(None);
185 let resolution = resolver.resolve(&temp_dir.path().join("src"), &specifier);
186
187 assert!(resolution.is_ok());
188 assert_eq!(
189 resolution.unwrap().path(),
190 temp_dir.path().join("node_modules/framer-motion/index.js")
191 );
192 }
193
194 #[test]
195 fn test_resolve_node16_imports() {
196 let temp_dir = test_utils::create_mock_project(&vec![
197 (
198 "package.json",
199 r#"{
200 "type": "module"
201 }"#,
202 ),
203 (
204 "tsconfig.json",
205 r#"{
206 "compilerOptions": {
207 "baseUrl": ".",
208 "module": "NodeNext",
209 "moduleResolution": "NodeNext",
210 "paths": {
211 "@components/*": ["./src/components/*"]
212 }
213 }
214 }"#,
215 ),
216 (
217 "src/components/index.ts",
218 "export { Button } from './Button';",
219 ),
220 (
221 "src/components/Button/index.ts",
222 "export function Button() { return <div>Button</div>; }",
223 ),
224 (
225 "src/components/Button.tsx",
226 "export function Button() { return <div>Button</div>; }",
227 ),
228 ]);
229
230 let specifier = "./components/Button.js";
231 let resolver = ProjectResolver::new(Some(temp_dir.path().join("tsconfig.json")));
232 let resolution = resolver.resolve(&temp_dir.path().join("src"), &specifier);
233
234 assert!(resolution.is_ok());
235 assert_eq!(
236 resolution.unwrap().path(),
237 temp_dir.path().join("src/components/Button.tsx")
238 );
239
240 let specifier = "./components/index.js";
241 let resolver = ProjectResolver::new(Some(temp_dir.path().join("tsconfig.json")));
242 let resolution = resolver.resolve(&temp_dir.path().join("src"), &specifier);
243
244 assert!(resolution.is_ok());
245 assert_eq!(
246 resolution.unwrap().path(),
247 temp_dir.path().join("src/components/index.ts")
248 );
249
250 let specifier = "./components/Button/index.js";
251 let resolver = ProjectResolver::new(Some(temp_dir.path().join("tsconfig.json")));
252 let resolution = resolver.resolve(&temp_dir.path().join("src"), &specifier);
253
254 assert!(resolution.is_ok());
255 assert_eq!(
256 resolution.unwrap().path(),
257 temp_dir.path().join("src/components/Button/index.ts")
258 );
259
260 let specifier = "@components/Button/index.js";
261 let resolver = ProjectResolver::new(Some(temp_dir.path().join("tsconfig.json")));
262 let resolution = resolver.resolve(&temp_dir.path().join("src"), &specifier);
263
264 assert!(resolution.is_ok());
265 assert_eq!(
266 resolution.unwrap().path(),
267 temp_dir.path().join("src/components/Button/index.ts")
268 );
269 }
270}