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