unrspack_resolver/options.rs
1use std::{
2 fmt,
3 path::{Path, PathBuf},
4};
5
6/// Module Resolution Options
7///
8/// Options are directly ported from [enhanced-resolve](https://github.com/webpack/enhanced-resolve#resolver-options).
9///
10/// See [webpack resolve](https://webpack.js.org/configuration/resolve/) for information and examples
11#[expect(clippy::struct_excessive_bools)]
12#[derive(Debug, Clone)]
13pub struct ResolveOptions {
14 /// Path to TypeScript configuration file.
15 ///
16 /// Default `None`
17 pub tsconfig: Option<TsconfigOptions>,
18
19 /// Create aliases to import or require certain modules more easily.
20 ///
21 /// An alias is used to replace a whole path or part of a path.
22 /// For example, to alias a commonly used `src/` folders: `vec![("@/src"), vec![AliasValue::Path("/path/to/src")]]`
23 ///
24 /// A trailing $ can also be added to the given object's keys to signify an exact match.
25 ///
26 /// See [webpack's `resolve.alias` documentation](https://webpack.js.org/configuration/resolve/#resolvealias) for a list of use cases.
27 pub alias: Alias,
28
29 /// A list of alias fields in description files.
30 ///
31 /// Specify a field, such as `browser`, to be parsed according to [this specification](https://github.com/defunctzombie/package-browser-field-spec).
32 /// Can be a path to json object such as `["path", "to", "exports"]`.
33 ///
34 /// Default `[]`
35 pub alias_fields: Vec<Vec<String>>,
36
37 /// Condition names for exports field which defines entry points of a package.
38 ///
39 /// The key order in the exports field is significant. During condition matching, earlier entries have higher priority and take precedence over later entries.
40 ///
41 /// Default `[]`
42 pub condition_names: Vec<String>,
43
44 /// The JSON files to use for descriptions. (There was once a `bower.json`.)
45 ///
46 /// Default `["package.json"]`
47 pub description_files: Vec<String>,
48
49 /// Whether the resolver should check for the presence of a .pnp.cjs file up the dependency tree.
50 ///
51 /// Default `true`
52 #[cfg(feature = "yarn_pnp")]
53 pub enable_pnp: bool,
54
55 /// Set to [EnforceExtension::Enabled] for [ESM Mandatory file extensions](https://nodejs.org/api/esm.html#mandatory-file-extensions).
56 ///
57 /// If `enforce_extension` is set to [EnforceExtension::Enabled], resolution will not allow extension-less files.
58 /// This means `require('./foo.js')` will resolve, while `require('./foo')` will not.
59 ///
60 /// The default value for `enforce_extension` is [EnforceExtension::Auto], which is changed upon initialization.
61 ///
62 /// It changes to [EnforceExtension::Enabled] if [ResolveOptions::extensions] contains an empty string;
63 /// otherwise, this value changes to [EnforceExtension::Disabled].
64 ///
65 /// Explicitly set the value to [EnforceExtension::Disabled] to disable this automatic behavior.
66 ///
67 /// For reference, this behavior is aligned with `enhanced-resolve`. See <https://github.com/webpack/enhanced-resolve/pull/285>.
68 pub enforce_extension: EnforceExtension,
69
70 /// A list of exports fields in description files.
71 ///
72 /// Can be a path to a JSON object such as `["path", "to", "exports"]`.
73 ///
74 /// Default `[["exports"]]`.
75 pub exports_fields: Vec<Vec<String>>,
76
77 /// Fields from `package.json` which are used to provide the internal requests of a package
78 /// (requests starting with # are considered internal).
79 ///
80 /// Can be a path to a JSON object such as `["path", "to", "imports"]`.
81 ///
82 /// Default `[["imports"]]`.
83 pub imports_fields: Vec<Vec<String>>,
84
85 /// An object which maps extension to extension aliases.
86 ///
87 /// Default `{}`
88 pub extension_alias: Vec<(String, Vec<String>)>,
89
90 /// Attempt to resolve these extensions in order.
91 ///
92 /// If multiple files share the same name but have different extensions,
93 /// will resolve the one with the extension listed first in the array and skip the rest.
94 ///
95 /// All extensions must have a leading dot.
96 ///
97 /// Default `[".js", ".json", ".node"]`
98 pub extensions: Vec<String>,
99
100 /// Redirect module requests when normal resolving fails.
101 ///
102 /// Default `[]`
103 pub fallback: Alias,
104
105 /// Request passed to resolve is already fully specified and extensions or main files are not resolved for it (they are still resolved for internal requests).
106 ///
107 /// See also webpack configuration [resolve.fullySpecified](https://webpack.js.org/configuration/module/#resolvefullyspecified)
108 ///
109 /// Default `false`
110 pub fully_specified: bool,
111
112 /// A list of main fields in description files
113 ///
114 /// Default `["main"]`.
115 pub main_fields: Vec<String>,
116
117 /// The filename to be used while resolving directories.
118 ///
119 /// Default `["index"]`
120 pub main_files: Vec<String>,
121
122 /// A list of directories to resolve modules from, can be absolute path or folder name.
123 ///
124 /// Default `["node_modules"]`
125 pub modules: Vec<String>,
126
127 /// Resolve to a context instead of a file.
128 ///
129 /// Default `false`
130 pub resolve_to_context: bool,
131
132 /// Prefer to resolve module requests as relative requests instead of using modules from node_modules directories.
133 ///
134 /// Default `false`
135 pub prefer_relative: bool,
136
137 /// Prefer to resolve server-relative urls as absolute paths before falling back to resolve in ResolveOptions::roots.
138 ///
139 /// Default `false`
140 pub prefer_absolute: bool,
141
142 /// A list of resolve restrictions to restrict the paths that a request can be resolved on.
143 ///
144 /// Default `[]`
145 pub restrictions: Vec<Restriction>,
146
147 /// A list of directories where requests of server-relative URLs (starting with '/') are resolved.
148 /// On non-Windows systems these requests are resolved as an absolute path first.
149 ///
150 /// Default `[]`
151 pub roots: Vec<PathBuf>,
152
153 /// Whether to resolve symlinks to their symlinked location.
154 /// When enabled, symlinked resources are resolved to their real path, not their symlinked location.
155 /// Note that this may cause module resolution to fail when using tools that symlink packages (like npm link).
156 ///
157 /// Default `true`
158 pub symlinks: bool,
159
160 /// Whether to parse [module.builtinModules](https://nodejs.org/api/module.html#modulebuiltinmodules) or not.
161 /// For example, "zlib" will throw [crate::ResolveError::Builtin] when set to true.
162 ///
163 /// Default `false`
164 pub builtin_modules: bool,
165}
166
167impl ResolveOptions {
168 /// ## Examples
169 ///
170 /// ```
171 /// use unrspack_resolver::ResolveOptions;
172 ///
173 /// let options = ResolveOptions::default().with_condition_names(&["bar"]);
174 /// assert_eq!(options.condition_names, vec!["bar".to_string()])
175 /// ```
176 #[must_use]
177 pub fn with_condition_names(mut self, names: &[&str]) -> Self {
178 self.condition_names = names.iter().map(ToString::to_string).collect::<Vec<String>>();
179 self
180 }
181
182 /// ## Examples
183 ///
184 /// ```
185 /// use unrspack_resolver::ResolveOptions;
186 ///
187 /// let options = ResolveOptions::default().with_builtin_modules(false);
188 /// assert_eq!(options.builtin_modules, false)
189 /// ```
190 #[must_use]
191 pub const fn with_builtin_modules(mut self, flag: bool) -> Self {
192 self.builtin_modules = flag;
193 self
194 }
195
196 /// Adds a single root to the options
197 ///
198 /// ## Examples
199 ///
200 /// ```
201 /// use unrspack_resolver::ResolveOptions;
202 /// use std::path::{Path, PathBuf};
203 ///
204 /// let options = ResolveOptions::default().with_root("foo");
205 /// assert_eq!(options.roots, vec![PathBuf::from("foo")])
206 /// ```
207 #[must_use]
208 pub fn with_root<P: AsRef<Path>>(mut self, root: P) -> Self {
209 self.roots.push(root.as_ref().to_path_buf());
210 self
211 }
212
213 /// Adds a single extension to the list of extensions. Extension must start with a `.`
214 ///
215 /// ## Examples
216 ///
217 /// ```
218 /// use unrspack_resolver::ResolveOptions;
219 /// use std::path::{Path, PathBuf};
220 ///
221 /// let options = ResolveOptions::default().with_extension(".jsonc");
222 /// assert!(options.extensions.contains(&".jsonc".to_string()));
223 /// ```
224 #[must_use]
225 pub fn with_extension<S: Into<String>>(mut self, extension: S) -> Self {
226 self.extensions.push(extension.into());
227 self
228 }
229
230 /// Adds a single main field to the list of fields
231 ///
232 /// ## Examples
233 ///
234 /// ```
235 /// use unrspack_resolver::ResolveOptions;
236 /// use std::path::{Path, PathBuf};
237 ///
238 /// let options = ResolveOptions::default().with_main_field("something");
239 /// assert!(options.main_fields.contains(&"something".to_string()));
240 /// ```
241 #[must_use]
242 pub fn with_main_field<S: Into<String>>(mut self, field: S) -> Self {
243 self.main_fields.push(field.into());
244 self
245 }
246
247 /// Changes how the extension should be treated
248 ///
249 /// ## Examples
250 ///
251 /// ```
252 /// use unrspack_resolver::{ResolveOptions, EnforceExtension};
253 /// use std::path::{Path, PathBuf};
254 ///
255 /// let options = ResolveOptions::default().with_force_extension(EnforceExtension::Enabled);
256 /// assert_eq!(options.enforce_extension, EnforceExtension::Enabled);
257 /// ```
258 #[must_use]
259 pub const fn with_force_extension(mut self, enforce_extension: EnforceExtension) -> Self {
260 self.enforce_extension = enforce_extension;
261 self
262 }
263
264 /// Sets the value for [ResolveOptions::fully_specified]
265 ///
266 /// ## Examples
267 ///
268 /// ```
269 /// use unrspack_resolver::{ResolveOptions};
270 /// use std::path::{Path, PathBuf};
271 ///
272 /// let options = ResolveOptions::default().with_fully_specified(true);
273 /// assert_eq!(options.fully_specified, true);
274 /// ```
275 #[must_use]
276 pub const fn with_fully_specified(mut self, fully_specified: bool) -> Self {
277 self.fully_specified = fully_specified;
278 self
279 }
280
281 /// Sets the value for [ResolveOptions::prefer_relative]
282 ///
283 /// ## Examples
284 ///
285 /// ```
286 /// use unrspack_resolver::{ResolveOptions};
287 /// use std::path::{Path, PathBuf};
288 ///
289 /// let options = ResolveOptions::default().with_prefer_relative(true);
290 /// assert_eq!(options.prefer_relative, true);
291 /// ```
292 #[must_use]
293 pub const fn with_prefer_relative(mut self, flag: bool) -> Self {
294 self.prefer_relative = flag;
295 self
296 }
297
298 /// Sets the value for [ResolveOptions::prefer_absolute]
299 ///
300 /// ## Examples
301 ///
302 /// ```
303 /// use unrspack_resolver::{ResolveOptions};
304 /// use std::path::{Path, PathBuf};
305 ///
306 /// let options = ResolveOptions::default().with_prefer_absolute(true);
307 /// assert_eq!(options.prefer_absolute, true);
308 /// ```
309 #[must_use]
310 pub const fn with_prefer_absolute(mut self, flag: bool) -> Self {
311 self.prefer_absolute = flag;
312 self
313 }
314
315 /// Changes the value of [ResolveOptions::symlinks]
316 ///
317 /// ## Examples
318 ///
319 /// ```
320 /// use unrspack_resolver::{ResolveOptions};
321 ///
322 /// let options = ResolveOptions::default().with_symbolic_link(false);
323 /// assert_eq!(options.symlinks, false);
324 /// ```
325 #[must_use]
326 pub const fn with_symbolic_link(mut self, flag: bool) -> Self {
327 self.symlinks = flag;
328 self
329 }
330
331 /// Adds a module to [ResolveOptions::modules]
332 ///
333 /// ## Examples
334 ///
335 /// ```
336 /// use unrspack_resolver::{ResolveOptions};
337 ///
338 /// let options = ResolveOptions::default().with_module("module");
339 /// assert!(options.modules.contains(&"module".to_string()));
340 /// ```
341 #[must_use]
342 pub fn with_module<M: Into<String>>(mut self, module: M) -> Self {
343 self.modules.push(module.into());
344 self
345 }
346
347 /// Adds a main file to [ResolveOptions::main_files]
348 ///
349 /// ## Examples
350 ///
351 /// ```
352 /// use unrspack_resolver::{ResolveOptions};
353 ///
354 /// let options = ResolveOptions::default().with_main_file("foo");
355 /// assert!(options.main_files.contains(&"foo".to_string()));
356 /// ```
357 #[must_use]
358 pub fn with_main_file<M: Into<String>>(mut self, module: M) -> Self {
359 self.main_files.push(module.into());
360 self
361 }
362
363 pub(crate) fn sanitize(mut self) -> Self {
364 debug_assert!(
365 self.extensions.iter().filter(|e| !e.is_empty()).all(|e| e.starts_with('.')),
366 "All extensions must start with a leading dot"
367 );
368 // Set `enforceExtension` to `true` when [ResolveOptions::extensions] contains an empty string.
369 // See <https://github.com/webpack/enhanced-resolve/pull/285>
370 if self.enforce_extension == EnforceExtension::Auto {
371 if !self.extensions.is_empty() && self.extensions.iter().any(String::is_empty) {
372 self.enforce_extension = EnforceExtension::Enabled;
373 } else {
374 self.enforce_extension = EnforceExtension::Disabled;
375 }
376 }
377 self
378 }
379}
380
381/// Value for [ResolveOptions::enforce_extension]
382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
383pub enum EnforceExtension {
384 Auto,
385 Enabled,
386 Disabled,
387}
388
389impl EnforceExtension {
390 #[must_use]
391 pub const fn is_auto(&self) -> bool {
392 matches!(self, Self::Auto)
393 }
394
395 #[must_use]
396 pub const fn is_enabled(&self) -> bool {
397 matches!(self, Self::Enabled)
398 }
399
400 #[must_use]
401 pub const fn is_disabled(&self) -> bool {
402 matches!(self, Self::Disabled)
403 }
404}
405
406/// Alias for [ResolveOptions::alias] and [ResolveOptions::fallback]
407pub type Alias = Vec<(String, Vec<AliasValue>)>;
408
409/// Alias Value for [ResolveOptions::alias] and [ResolveOptions::fallback]
410#[derive(Debug, Clone, Hash, PartialEq, Eq)]
411pub enum AliasValue {
412 /// The path value
413 Path(String),
414
415 /// The `false` value
416 Ignore,
417}
418
419impl<S> From<S> for AliasValue
420where
421 S: Into<String>,
422{
423 fn from(value: S) -> Self {
424 Self::Path(value.into())
425 }
426}
427
428/// Value for [ResolveOptions::restrictions]
429#[derive(Debug, Clone)]
430pub enum Restriction {
431 Path(PathBuf),
432 RegExp(String),
433}
434
435/// Tsconfig Options for [ResolveOptions::tsconfig]
436///
437/// Derived from [tsconfig-paths-webpack-plugin](https://github.com/dividab/tsconfig-paths-webpack-plugin#options)
438#[derive(Debug, Clone)]
439pub struct TsconfigOptions {
440 /// Allows you to specify where to find the TypeScript configuration file.
441 /// You may provide
442 /// * a relative path to the configuration file. It will be resolved relative to cwd.
443 /// * an absolute path to the configuration file.
444 pub config_file: PathBuf,
445
446 /// Support for Typescript Project References.
447 pub references: TsconfigReferences,
448}
449
450/// Configuration for [TsconfigOptions::references]
451#[derive(Debug, Clone)]
452pub enum TsconfigReferences {
453 Disabled,
454 /// Use the `references` field from tsconfig of `config_file`.
455 Auto,
456 /// Manually provided relative or absolute path.
457 Paths(Vec<PathBuf>),
458}
459
460impl Default for ResolveOptions {
461 fn default() -> Self {
462 Self {
463 tsconfig: None,
464 alias: vec![],
465 alias_fields: vec![],
466 condition_names: vec![],
467 description_files: vec!["package.json".into()],
468 enforce_extension: EnforceExtension::Auto,
469 extension_alias: vec![],
470 exports_fields: vec![vec!["exports".into()]],
471 imports_fields: vec![vec!["imports".into()]],
472 extensions: vec![".js".into(), ".json".into(), ".node".into()],
473 fallback: vec![],
474 fully_specified: false,
475 main_fields: vec!["main".into()],
476 main_files: vec!["index".into()],
477 modules: vec!["node_modules".into()],
478 enable_pnp: true,
479 resolve_to_context: false,
480 prefer_relative: false,
481 prefer_absolute: false,
482 restrictions: vec![],
483 roots: vec![],
484 symlinks: true,
485 builtin_modules: false,
486 }
487 }
488}
489
490// For tracing
491impl fmt::Display for ResolveOptions {
492 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
493 if let Some(tsconfig) = &self.tsconfig {
494 write!(f, "tsconfig:{tsconfig:?},")?;
495 }
496 if !self.alias.is_empty() {
497 write!(f, "alias:{:?},", self.alias)?;
498 }
499 if !self.alias_fields.is_empty() {
500 write!(f, "alias_fields:{:?},", self.alias_fields)?;
501 }
502 if !self.condition_names.is_empty() {
503 write!(f, "condition_names:{:?},", self.condition_names)?;
504 }
505 if self.enforce_extension.is_enabled() {
506 write!(f, "enforce_extension:{:?},", self.enforce_extension)?;
507 }
508 if !self.exports_fields.is_empty() {
509 write!(f, "exports_fields:{:?},", self.exports_fields)?;
510 }
511 if !self.imports_fields.is_empty() {
512 write!(f, "imports_fields:{:?},", self.imports_fields)?;
513 }
514 if !self.extension_alias.is_empty() {
515 write!(f, "extension_alias:{:?},", self.extension_alias)?;
516 }
517 if !self.extensions.is_empty() {
518 write!(f, "extensions:{:?},", self.extensions)?;
519 }
520 if !self.fallback.is_empty() {
521 write!(f, "fallback:{:?},", self.fallback)?;
522 }
523 if self.fully_specified {
524 write!(f, "fully_specified:{:?},", self.fully_specified)?;
525 }
526 if !self.main_fields.is_empty() {
527 write!(f, "main_fields:{:?},", self.main_fields)?;
528 }
529 if !self.main_files.is_empty() {
530 write!(f, "main_files:{:?},", self.main_files)?;
531 }
532 if !self.modules.is_empty() {
533 write!(f, "modules:{:?},", self.modules)?;
534 }
535 if self.resolve_to_context {
536 write!(f, "resolve_to_context:{:?},", self.resolve_to_context)?;
537 }
538 if self.prefer_relative {
539 write!(f, "prefer_relative:{:?},", self.prefer_relative)?;
540 }
541 if self.prefer_absolute {
542 write!(f, "prefer_absolute:{:?},", self.prefer_absolute)?;
543 }
544 if !self.restrictions.is_empty() {
545 write!(f, "restrictions:{:?},", self.restrictions)?;
546 }
547 if !self.roots.is_empty() {
548 write!(f, "roots:{:?},", self.roots)?;
549 }
550 if self.symlinks {
551 write!(f, "symlinks:{:?},", self.symlinks)?;
552 }
553 if self.builtin_modules {
554 write!(f, "builtin_modules:{:?},", self.builtin_modules)?;
555 }
556 Ok(())
557 }
558}
559
560#[cfg(test)]
561mod test {
562 use std::path::PathBuf;
563
564 use super::{
565 AliasValue, EnforceExtension, ResolveOptions, Restriction, TsconfigOptions,
566 TsconfigReferences,
567 };
568
569 #[test]
570 fn enforce_extension() {
571 assert!(EnforceExtension::Auto.is_auto());
572 assert!(!EnforceExtension::Enabled.is_auto());
573 assert!(!EnforceExtension::Disabled.is_auto());
574
575 assert!(!EnforceExtension::Auto.is_enabled());
576 assert!(EnforceExtension::Enabled.is_enabled());
577 assert!(!EnforceExtension::Disabled.is_enabled());
578
579 assert!(!EnforceExtension::Auto.is_disabled());
580 assert!(!EnforceExtension::Enabled.is_disabled());
581 assert!(EnforceExtension::Disabled.is_disabled());
582 }
583
584 #[test]
585 fn display() {
586 let options = ResolveOptions {
587 tsconfig: Some(TsconfigOptions {
588 config_file: PathBuf::from("tsconfig.json"),
589 references: TsconfigReferences::Auto,
590 }),
591 alias: vec![("a".into(), vec![AliasValue::Ignore])],
592 alias_fields: vec![vec!["browser".into()]],
593 condition_names: vec!["require".into()],
594 enforce_extension: EnforceExtension::Enabled,
595 extension_alias: vec![(".js".into(), vec![".ts".into()])],
596 exports_fields: vec![vec!["exports".into()]],
597 imports_fields: vec![vec!["imports".into()]],
598 fallback: vec![("fallback".into(), vec![AliasValue::Ignore])],
599 fully_specified: true,
600 resolve_to_context: true,
601 prefer_relative: true,
602 prefer_absolute: true,
603 restrictions: vec![Restriction::Path(PathBuf::from("restrictions"))],
604 roots: vec![PathBuf::from("roots")],
605 builtin_modules: true,
606 ..ResolveOptions::default()
607 };
608
609 let expected = r#"tsconfig:TsconfigOptions { config_file: "tsconfig.json", references: Auto },alias:[("a", [Ignore])],alias_fields:[["browser"]],condition_names:["require"],enforce_extension:Enabled,exports_fields:[["exports"]],imports_fields:[["imports"]],extension_alias:[(".js", [".ts"])],extensions:[".js", ".json", ".node"],fallback:[("fallback", [Ignore])],fully_specified:true,main_fields:["main"],main_files:["index"],modules:["node_modules"],resolve_to_context:true,prefer_relative:true,prefer_absolute:true,restrictions:[Path("restrictions")],roots:["roots"],symlinks:true,builtin_modules:true,"#;
610 assert_eq!(format!("{options}"), expected);
611
612 let options = ResolveOptions {
613 alias: vec![],
614 alias_fields: vec![],
615 builtin_modules: false,
616 condition_names: vec![],
617 description_files: vec![],
618 #[cfg(feature = "yarn_pnp")]
619 enable_pnp: true,
620 enforce_extension: EnforceExtension::Disabled,
621 exports_fields: vec![],
622 extension_alias: vec![],
623 extensions: vec![],
624 fallback: vec![],
625 fully_specified: false,
626 imports_fields: vec![],
627 main_fields: vec![],
628 main_files: vec![],
629 modules: vec![],
630 prefer_absolute: false,
631 prefer_relative: false,
632 resolve_to_context: false,
633 restrictions: vec![],
634 roots: vec![],
635 symlinks: false,
636 tsconfig: None,
637 };
638
639 assert_eq!(format!("{options}"), "");
640 }
641}