spinne_core/traverse/
resolver.rs

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    /// resolve a relative file path to an absolute file path
15    ///
16    /// dir: is the directory of the file that has the import statement
17    /// specifier: is the relative file path that the import statement is importing
18    /// tsconfig: is the path to the tsconfig.json file that contains the tsconfig options and tsconfigPaths
19    ///
20    /// # Example
21    ///
22    /// ```
23    /// use std::path::PathBuf;
24    /// use spinne_core::ProjectResolver;
25    ///
26    /// let dir = PathBuf::from("/Users/tim/projects/spinne/src/index.ts");
27    /// let resolver = ProjectResolver::new(None);
28    /// let resolution = resolver.resolve(&dir, "./components/Button");
29    /// ```
30    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}