Skip to main content

rspack_core/resolver/
mod.rs

1mod boxfs;
2mod factory;
3mod resolver_impl;
4use std::{
5  borrow::Borrow,
6  fmt,
7  path::PathBuf,
8  sync::{Arc, LazyLock},
9};
10
11use regex::Regex;
12use rspack_error::Error;
13use rspack_fs::ReadableFileSystem;
14use rspack_loader_runner::{DescriptionData, ResourceData};
15use rspack_paths::{AssertUtf8, Utf8PathBuf};
16use rspack_util::identifier::insert_zero_width_space_for_fragment;
17use rustc_hash::FxHashSet;
18use sugar_path::SugarPath;
19
20pub use self::{
21  factory::{ResolveOptionsWithDependencyType, ResolverFactory},
22  resolver_impl::{ResolveContext, ResolveInnerError, ResolveInnerOptions, Resolver},
23};
24use crate::{
25  Context, DependencyCategory, DependencyRange, DependencyType, ModuleIdentifier, Resolve,
26  SharedPluginDriver,
27};
28
29static RELATIVE_PATH_REGEX: LazyLock<Regex> =
30  LazyLock::new(|| Regex::new(r"^\.\.?\/").expect("should init regex"));
31
32static PARENT_PATH_REGEX: LazyLock<Regex> =
33  LazyLock::new(|| Regex::new(r"^\.\.[\/]").expect("should init regex"));
34
35static CURRENT_DIR_REGEX: LazyLock<Regex> =
36  LazyLock::new(|| Regex::new(r"^(\.[\/])").expect("should init regex"));
37
38#[derive(Debug)]
39pub struct ResolveArgs<'a> {
40  pub importer: Option<&'a ModuleIdentifier>,
41  pub issuer: Option<&'a str>,
42  pub context: Context,
43  pub specifier: &'a str,
44  pub dependency_type: &'a DependencyType,
45  pub dependency_category: &'a DependencyCategory,
46  pub span: Option<DependencyRange>,
47  pub resolve_options: Option<Arc<Resolve>>,
48  pub resolve_to_context: bool,
49  pub optional: bool,
50  pub file_dependencies: &'a mut FxHashSet<PathBuf>,
51  pub missing_dependencies: &'a mut FxHashSet<PathBuf>,
52}
53
54/// A successful path resolution or an ignored path.
55#[derive(Debug, Clone, Eq, PartialEq)]
56pub enum ResolveResult {
57  Resource(Resource),
58  Ignored,
59}
60
61/// A successful path resolution.
62///
63/// Contains the raw `package.json` value if there is one.
64#[derive(Clone)]
65pub struct Resource {
66  pub path: Utf8PathBuf,
67  pub query: String,
68  pub fragment: String,
69  pub description_data: Option<DescriptionData>,
70}
71
72impl fmt::Debug for Resource {
73  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
74    write!(f, "{:?}", self.full_path())
75  }
76}
77
78impl PartialEq for Resource {
79  fn eq(&self, other: &Self) -> bool {
80    self.path == other.path && self.query == other.query && self.fragment == other.fragment
81  }
82}
83impl Eq for Resource {}
84
85impl Resource {
86  /// Get the full path with query and fragment attached.
87  pub fn full_path(&self) -> String {
88    let mut buf = insert_zero_width_space_for_fragment(self.path.as_str()).into_owned();
89    buf.push_str(&insert_zero_width_space_for_fragment(&self.query));
90    buf.push_str(&self.fragment);
91    buf
92  }
93}
94
95impl From<Resource> for ResourceData {
96  fn from(resource: Resource) -> Self {
97    let mut resource_data = Self::new_with_path(
98      resource.full_path(),
99      resource.path,
100      Some(resource.query),
101      Some(resource.fragment),
102    );
103    resource_data.set_description_optional(resource.description_data);
104    resource_data
105  }
106}
107
108pub async fn resolve_for_error_hints(
109  args: ResolveArgs<'_>,
110  plugin_driver: &SharedPluginDriver,
111  fs: Arc<dyn ReadableFileSystem>,
112) -> Option<String> {
113  let dep = ResolveOptionsWithDependencyType {
114    resolve_options: args
115      .resolve_options
116      .map(|r| Box::new(Arc::unwrap_or_clone(r))),
117    resolve_to_context: args.resolve_to_context,
118    dependency_category: *args.dependency_category,
119  };
120
121  let base_dir = args.context.clone();
122  let base_dir = base_dir.as_ref();
123
124  let fully_specified = dep
125    .resolve_options
126    .as_ref()
127    .and_then(|o| o.fully_specified(Some(args.dependency_category)))
128    .unwrap_or_default();
129
130  let prefer_relative = dep
131    .resolve_options
132    .as_ref()
133    .and_then(|o| o.prefer_relative(Some(args.dependency_category)))
134    .unwrap_or_default();
135
136  // Try to resolve without fully specified
137  if fully_specified {
138    let mut dep = dep.clone();
139    dep.resolve_options = dep.resolve_options.map(|mut options| {
140      options.fully_specified = Some(false);
141      options
142    });
143    let resolver = plugin_driver.resolver_factory.get(dep);
144    if let Ok(ResolveResult::Resource(resource)) = resolver.resolve(base_dir, args.specifier).await
145    {
146      let relative_path = resource
147        .path
148        .as_std_path()
149        .relative(args.context)
150        .assert_utf8();
151      let suggestion = if let Some((_, [prefix])) = CURRENT_DIR_REGEX
152        .captures_iter(args.specifier)
153        .next()
154        .map(|c| c.extract())
155      {
156        // If the specifier is a relative path pointing to the current directory,
157        // we can suggest the path relative to the current directory.
158        format!("{prefix}{relative_path}")
159      } else if PARENT_PATH_REGEX.is_match(args.specifier) {
160        // If the specifier is a relative path to which the parent directory is,
161        // then we return the relative path directly.
162        relative_path.as_str().to_string()
163      } else {
164        // If the specifier is a package name like or some arbitrary alias,
165        // then we return the full path.
166        resource.path.as_str().to_string()
167      };
168      return Some(format!("Did you mean '{}'?
169
170The request '{}' failed to resolve only because it was resolved as fully specified,
171probably because the origin is strict EcmaScript Module,
172e. g. a module with javascript mimetype, a '*.mjs' file, or a '*.js' file where the package.json contains '\"type\": \"module\"'.
173
174The extension in the request is mandatory for it to be fully specified.
175Add the extension to the request.", suggestion, args.specifier));
176    }
177  }
178
179  // Try to resolve with relative path if request is not relative
180  if !RELATIVE_PATH_REGEX.is_match(args.specifier) && !prefer_relative {
181    let dep = dep.clone();
182    let module_directories = dep
183      .resolve_options
184      .as_deref()
185      .or(Some(&plugin_driver.options.resolve))
186      .and_then(|o| o.modules.as_ref().map(|m| m.join(", ")));
187    let module_directories = {
188      if let Some(module_directories) = module_directories {
189        format!(" ({module_directories}).")
190      } else {
191        ".".to_string()
192      }
193    };
194    let resolver = plugin_driver.resolver_factory.get(dep);
195    let request = format!("./{}", args.specifier);
196    if resolver.resolve(base_dir, &request).await.is_ok() {
197      return Some(format!(
198          "Did you mean './{}'?
199
200Requests that should resolve in the current directory need to start with './'.
201Requests that start with a name are treated as module requests and resolve within module directories{module_directories}
202
203If changing the source code is not an option, there is also a resolve options called 'preferRelative'
204which tries to resolve these kind of requests in the current directory too.",
205          args.specifier
206        ));
207    }
208  }
209
210  // try to resolve relative path with extension
211  if RELATIVE_PATH_REGEX.is_match(args.specifier) {
212    let connected_path = base_dir.join(args.specifier);
213    let normalized_path = connected_path.absolutize();
214
215    let mut is_resolving_dir = false; // whether the request is to resolve a directory or not
216
217    let file_name = normalized_path.file_name();
218    let utf8_normalized_path =
219      Utf8PathBuf::from_path_buf(normalized_path.clone()).expect("should be a valid utf8 path");
220
221    let parent_path = match fs.metadata(&utf8_normalized_path).await {
222      Ok(metadata) => {
223        // if the path is not directory, we need to resolve the parent directory
224        if !metadata.is_directory {
225          normalized_path.parent()
226        } else {
227          is_resolving_dir = true;
228          Some(normalized_path.borrow())
229        }
230      }
231      Err(_) => normalized_path.parent(),
232    };
233
234    if let Some(file_name) = file_name
235      && let Some(parent_path) =
236        parent_path.and_then(|path| Utf8PathBuf::from_path_buf(path.to_path_buf()).ok())
237    {
238      // read the files in the parent directory
239      if let Ok(files) = fs.read_dir(&parent_path).await {
240        let mut requested_names = vec![
241          file_name
242            .to_str()
243            .map(|f| f.to_string())
244            .unwrap_or_default(),
245        ];
246        if is_resolving_dir {
247          // The request maybe is like `./` or `./dir` to resolve the main file (e.g.: index) in directory
248          // So we need to check them.
249          let main_files = dep
250            .resolve_options
251            .as_deref()
252            .or(Some(&plugin_driver.options.resolve))
253            .and_then(|o| o.main_files.as_ref().cloned())
254            .unwrap_or_default();
255
256          requested_names.extend(main_files);
257        }
258
259        let suggestions = files
260          .into_iter()
261          .filter_map(|file| {
262            let path = parent_path.join(file);
263            path.file_stem().and_then(|file_stem| {
264              if requested_names.contains(&file_stem.to_string()) {
265                let mut suggestion = path.as_std_path().relative(&args.context).assert_utf8();
266
267                if !suggestion.as_str().starts_with('.') {
268                  suggestion = Utf8PathBuf::from(format!("./{suggestion}"));
269                }
270                Some(suggestion)
271              } else {
272                None
273              }
274            })
275          })
276          .collect::<Vec<_>>();
277
278        if suggestions.is_empty() {
279          return None;
280        }
281
282        let mut hint: Vec<String> = vec![];
283        for suggestion in suggestions {
284          let suggestion_ext = suggestion.extension().unwrap_or_default();
285          let specifier = args.specifier;
286
287          hint.push(format!(
288          "Found module '{suggestion}'. However, it's not possible to request this module without the extension 
289if its extension was not listed in the `resolve.extensions`. Here're some possible solutions:
290
2911. add the extension `\".{suggestion_ext}\"` to `resolve.extensions` in your rspack configuration
2922. use '{suggestion}' instead of '{specifier}'
293"));
294        }
295
296        return Some(hint.join("\n"));
297      }
298    }
299  }
300
301  None
302}
303
304/// Main entry point for module resolution.
305// #[tracing::instrument(err, "resolve", skip_all, fields(
306//     resolve.specifier = args.specifier,
307//     resolve.importer = ?args.importer,
308//     resolve.context = ?args.context,
309//     resolve.dependency_type = ?args.dependency_type,
310//     resolve.dependency_category = ?args.dependency_category
311//   ),
312//   level = "trace"
313// )]
314pub async fn resolve(
315  args: ResolveArgs<'_>,
316  plugin_driver: &SharedPluginDriver,
317) -> Result<ResolveResult, Error> {
318  let dep = ResolveOptionsWithDependencyType {
319    resolve_options: args
320      .resolve_options
321      .clone()
322      .map(|r| Box::new(Arc::unwrap_or_clone(r))),
323    resolve_to_context: args.resolve_to_context,
324    dependency_category: *args.dependency_category,
325  };
326
327  let mut context = Default::default();
328  let resolver = plugin_driver.resolver_factory.get(dep);
329  let mut result = resolver
330    .resolve_with_context(args.context.as_ref(), args.specifier, &mut context)
331    .await
332    .map_err(|error| error.into_resolve_error(&args));
333
334  if let Err(ref err) = result {
335    tracing::error!(
336      specifier = args.specifier,
337      importer = ?args.importer,
338      context = %args.context,
339      dependency_type = %args.dependency_type,
340      dependency_category = %args.dependency_category,
341      "Resolve error: {}",
342      err.to_string()
343    );
344  }
345
346  args.file_dependencies.extend(context.file_dependencies);
347  args
348    .missing_dependencies
349    .extend(context.missing_dependencies);
350
351  if result.is_err()
352    && let Some(hint) = resolve_for_error_hints(args, plugin_driver, resolver.inner_fs()).await
353  {
354    result = result.map_err(|mut err| {
355      err.help = Some(hint);
356      err
357    })
358  };
359
360  result
361}