nodejs_resolver/
lib.rs

1//! # `nodejs_resolver`
2//!
3//! ## How to use?
4//!
5//! ```rust
6//! // |-- node_modules
7//! // |---- foo
8//! // |------ index.js
9//! // | src
10//! // |-- foo.ts
11//! // |-- foo.js
12//! // | tests
13//!
14//! use nodejs_resolver::Resolver;
15//!
16//! let cwd = std::env::current_dir().unwrap();
17//! let resolver = Resolver::new(Default::default());
18//!
19//! resolver.resolve(&cwd.join("./src"), "foo");
20//! // -> ResolveResult::Info(ResolveInfo {
21//! //    path: PathBuf::from("<cwd>/node_modules/foo/index.js")
22//! //    request: Request {
23//! //       target: "",
24//! //       fragment: "",
25//! //       query: ""
26//! //    }
27//! //  })
28//! //
29//!
30//! resolver.resolve(&cwd.join("./src"), "./foo");
31//! // -> ResolveResult::Info(ResolveInfo {
32//! //    path: PathBuf::from("<cwd>/src/foo.js")
33//! //    request: Request {
34//! //       target: "",
35//! //       fragment: "",
36//! //       query: ""
37//! //    }
38//! //  })
39//! //
40//! ```
41//!
42
43mod cache;
44mod context;
45mod description;
46mod entry;
47mod error;
48mod fs;
49mod info;
50mod kind;
51mod log;
52mod map;
53mod options;
54mod parse;
55mod plugin;
56mod resolve;
57mod resource;
58mod state;
59mod tsconfig;
60mod tsconfig_path;
61
62pub use cache::Cache;
63use context::Context;
64pub use description::DescriptionData;
65pub use error::Error;
66use info::Info;
67use kind::PathKind;
68use log::{color, depth};
69use options::EnforceExtension::{Auto, Disabled, Enabled};
70pub use options::{AliasMap, EnforceExtension, Options};
71use plugin::{
72    AliasPlugin, BrowserFieldPlugin, ImportsFieldPlugin, ParsePlugin, Plugin, PreferRelativePlugin,
73    SymlinkPlugin,
74};
75pub use resource::Resource;
76use state::State;
77
78#[derive(Debug)]
79pub struct Resolver {
80    pub options: Options,
81    pub(crate) cache: std::sync::Arc<Cache>,
82}
83
84#[derive(Debug, Clone)]
85pub enum ResolveResult<T: Clone> {
86    Resource(T),
87    Ignored,
88}
89
90pub type RResult<T> = Result<T, Error>;
91
92impl Resolver {
93    #[must_use]
94    pub fn new(options: Options) -> Self {
95        log::enable_by_env();
96
97        let cache = if let Some(external_cache) = options.external_cache.as_ref() {
98            external_cache.clone()
99        } else {
100            std::sync::Arc::new(Cache::default())
101        };
102
103        let enforce_extension = match options.enforce_extension {
104            Auto => {
105                if options.extensions.iter().any(|ext| ext.is_empty()) {
106                    Enabled
107                } else {
108                    Disabled
109                }
110            }
111            _ => options.enforce_extension,
112        };
113
114        let options = Options {
115            enforce_extension,
116            ..options
117        };
118        Self { options, cache }
119    }
120
121    pub fn resolve(
122        &self,
123        path: &std::path::Path,
124        request: &str,
125    ) -> RResult<ResolveResult<Resource>> {
126        tracing::debug!(
127            "{:-^30}\nTry to resolve '{}' in '{}'",
128            color::green(&"[RESOLVER]"),
129            color::cyan(&request),
130            color::cyan(&path.display().to_string())
131        );
132        // let start = std::time::Instant::now();
133        let parsed = Self::parse(request);
134        let info = Info::new(path, parsed);
135        let mut context = Context::new(
136            self.options.fully_specified,
137            self.options.resolve_to_context,
138        );
139        let result = if let Some(tsconfig_location) = self.options.tsconfig.as_ref() {
140            self._resolve_with_tsconfig(info, tsconfig_location, &mut context)
141        } else {
142            self._resolve(info, &mut context)
143        };
144
145        let result = result.map_failed(|info| {
146            type FallbackPlugin<'a> = AliasPlugin<'a>;
147            FallbackPlugin::new(&self.options.fallback).apply(self, info, &mut context)
148        });
149        let result = result.map_success(|info| SymlinkPlugin::apply(self, info, &mut context));
150
151        // let duration = start.elapsed().as_millis();
152        // println!("time cost: {:?} us", duration); // us
153        // if duration > 10 {
154        //     println!(
155        //         "{:?}ms, path: {:?}, request: {:?}",
156        //         duration,
157        //         path.display(),
158        //         request,
159        //     );
160        // }
161
162        match result {
163            State::Success(ResolveResult::Ignored) => Ok(ResolveResult::Ignored),
164            State::Success(ResolveResult::Resource(info)) => {
165                let resource = Resource::new(info, self);
166                Ok(ResolveResult::Resource(resource))
167            }
168            State::Error(err) => Err(err),
169            State::Resolving(_) | State::Failed(_) => Err(Error::ResolveFailedTag),
170        }
171    }
172
173    fn _resolve(&self, info: Info, context: &mut Context) -> State {
174        tracing::debug!(
175            "Resolving '{request}' in '{path}'",
176            request = color::cyan(&info.request().target()),
177            path = color::cyan(&info.normalized_path().as_ref().display())
178        );
179
180        context.depth.increase();
181        if context.depth.cmp(127).is_ge() {
182            return State::Error(Error::Overflow);
183        }
184
185        let state = ParsePlugin::apply(self, info, context)
186            .then(|info| AliasPlugin::new(&self.options.alias).apply(self, info, context))
187            .then(|info| PreferRelativePlugin::apply(self, info, context))
188            .then(|info| {
189                let request = info.to_resolved_path();
190                let entry = self.load_entry(&request);
191                let pkg_info = match entry.pkg_info(self) {
192                    Ok(pkg_info) => pkg_info,
193                    Err(error) => return State::Error(error),
194                };
195                if let Some(pkg_info) = pkg_info {
196                    ImportsFieldPlugin::new(pkg_info)
197                        .apply(self, info, context)
198                        .then(|info| {
199                            BrowserFieldPlugin::new(pkg_info, false).apply(self, info, context)
200                        })
201                } else {
202                    State::Resolving(info)
203                }
204            })
205            .then(|info| {
206                if matches!(
207                    info.request().kind(),
208                    PathKind::AbsolutePosix | PathKind::AbsoluteWin | PathKind::Relative
209                ) {
210                    self.resolve_as_context(info, context)
211                        .then(|info| self.resolve_as_fully_specified(info, context))
212                        .then(|info| self.resolve_as_file(info, context))
213                        .then(|info| self.resolve_as_dir(info, context))
214                } else {
215                    self.resolve_as_modules(info, context)
216                }
217            });
218
219        context.depth.decrease();
220        state
221    }
222}
223
224#[cfg(debug_assertions)]
225pub mod test_helper {
226    #[must_use]
227    pub fn p(paths: Vec<&str>) -> std::path::PathBuf {
228        paths.iter().fold(
229            std::env::current_dir()
230                .unwrap()
231                .join("tests")
232                .join("fixtures"),
233            |acc, path| acc.join(path),
234        )
235    }
236
237    #[must_use]
238    pub fn vec_to_set(vec: Vec<&str>) -> std::collections::HashSet<String> {
239        std::collections::HashSet::from_iter(vec.into_iter().map(|s| s.to_string()))
240    }
241}