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}