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#[derive(Debug, Clone, Eq, PartialEq)]
56pub enum ResolveResult {
57 Resource(Resource),
58 Ignored,
59}
60
61#[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 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 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 format!("{prefix}{relative_path}")
159 } else if PARENT_PATH_REGEX.is_match(args.specifier) {
160 relative_path.as_str().to_string()
163 } else {
164 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 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 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; 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 !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 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 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
304pub 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}