1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
use std::{fmt, path::PathBuf};

/// Module Resolution Options
///
/// Options are directly ported from [enhanced-resolve](https://github.com/webpack/enhanced-resolve#resolver-options).
///
/// See [webpack resolve](https://webpack.js.org/configuration/resolve/) for information and examples
#[derive(Debug, Clone)]
pub struct ResolveOptions {
    /// Path to TypeScript configuration file.
    ///
    /// Default `None`
    pub tsconfig: Option<TsconfigOptions>,

    /// Create aliases to import or require certain modules more easily.
    /// A trailing $ can also be added to the given object's keys to signify an exact match.
    pub alias: Alias,

    /// A list of alias fields in description files.
    /// Specify a field, such as `browser`, to be parsed according to [this specification](https://github.com/defunctzombie/package-browser-field-spec).
    /// Can be a path to json object such as `["path", "to", "exports"]`.
    ///
    /// Default `[]`
    pub alias_fields: Vec<Vec<String>>,

    /// Condition names for exports field which defines entry points of a package.
    /// The key order in the exports field is significant. During condition matching, earlier entries have higher priority and take precedence over later entries.
    ///
    /// Default `[]`
    pub condition_names: Vec<String>,

    /// The JSON files to use for descriptions. (There was once a `bower.json`.)
    ///
    /// Default `["package.json"]`
    pub description_files: Vec<String>,

    /// If true, it will not allow extension-less files.
    /// So by default `require('./foo')` works if `./foo` has a `.js` extension,
    /// but with this enabled only `require('./foo.js')` will work.
    ///
    /// Default to `true` when [ResolveOptions::extensions] contains an empty string.
    /// Use `Some(false)` to disable the behavior.
    /// See <https://github.com/webpack/enhanced-resolve/pull/285>
    ///
    /// Default None, which is the same as `Some(false)` when the above empty rule is not applied.
    pub enforce_extension: EnforceExtension,

    /// A list of exports fields in description files.
    /// Can be a path to json object such as `["path", "to", "exports"]`.
    ///
    /// Default `[["exports"]]`.
    pub exports_fields: Vec<Vec<String>>,

    /// An object which maps extension to extension aliases.
    ///
    /// Default `{}`
    pub extension_alias: Vec<(String, Vec<String>)>,

    /// Attempt to resolve these extensions in order.
    /// If multiple files share the same name but have different extensions,
    /// will resolve the one with the extension listed first in the array and skip the rest.
    ///
    /// Default `[".js", ".json", ".node"]`
    pub extensions: Vec<String>,

    /// Redirect module requests when normal resolving fails.
    ///
    /// Default `[]`
    pub fallback: Alias,

    /// 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).
    ///
    /// See also webpack configuration [resolve.fullySpecified](https://webpack.js.org/configuration/module/#resolvefullyspecified)
    ///
    /// Default `false`
    pub fully_specified: bool,

    /// A list of main fields in description files
    ///
    /// Default `["main"]`.
    pub main_fields: Vec<String>,

    /// The filename to be used while resolving directories.
    ///
    /// Default `["index"]`
    pub main_files: Vec<String>,

    /// A list of directories to resolve modules from, can be absolute path or folder name.
    ///
    /// Default `["node_modules"]`
    pub modules: Vec<String>,

    /// Resolve to a context instead of a file.
    ///
    /// Default `false`
    pub resolve_to_context: bool,

    /// Prefer to resolve module requests as relative requests instead of using modules from node_modules directories.
    ///
    /// Default `false`
    pub prefer_relative: bool,

    /// Prefer to resolve server-relative urls as absolute paths before falling back to resolve in ResolveOptions::roots.
    ///
    /// Default `false`
    pub prefer_absolute: bool,

    /// A list of resolve restrictions to restrict the paths that a request can be resolved on.
    ///
    /// Default `[]`
    pub restrictions: Vec<Restriction>,

    /// A list of directories where requests of server-relative URLs (starting with '/') are resolved.
    /// On non-Windows systems these requests are resolved as an absolute path first.
    ///
    /// Default `[]`
    pub roots: Vec<PathBuf>,

    /// Whether to resolve symlinks to their symlinked location.
    /// When enabled, symlinked resources are resolved to their real path, not their symlinked location.
    /// Note that this may cause module resolution to fail when using tools that symlink packages (like npm link).
    ///
    /// Default `true`
    pub symlinks: bool,

    /// Whether to parse [module.builtinModules](https://nodejs.org/api/module.html#modulebuiltinmodules) or not.
    /// For example, "zlib" will throw [crate::ResolveError::Builtin] when set to true.
    ///
    /// Default `false`
    pub builtin_modules: bool,
}

/// Value for [ResolveOptions::enforce_extension]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnforceExtension {
    Auto,
    Enabled,
    Disabled,
}

impl EnforceExtension {
    pub fn is_auto(&self) -> bool {
        *self == Self::Auto
    }

    pub fn is_enabled(&self) -> bool {
        *self == Self::Enabled
    }

    pub fn is_disabled(&self) -> bool {
        *self == Self::Disabled
    }
}

/// Alias for [ResolveOptions::alias] and [ResolveOptions::fallback]
pub type Alias = Vec<(String, Vec<AliasValue>)>;

/// Alias Value for [ResolveOptions::alias] and [ResolveOptions::fallback]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum AliasValue {
    /// The path value
    Path(String),

    /// The `false` value
    Ignore,
}

/// Value for [ResolveOptions::restrictions]
#[derive(Debug, Clone)]
pub enum Restriction {
    Path(PathBuf),
    RegExp(String),
}

/// Tsconfig Options for [ResolveOptions::tsconfig]
///
/// Derived from [tsconfig-paths-webpack-plugin](https://github.com/dividab/tsconfig-paths-webpack-plugin#options)
#[derive(Debug, Clone)]
pub struct TsconfigOptions {
    /// Allows you to specify where to find the TypeScript configuration file.
    /// You may provide
    /// * a relative path to the configuration file. It will be resolved relative to cwd.
    /// * an absolute path to the configuration file.
    pub config_file: PathBuf,

    /// Support for Typescript Project References.
    pub references: TsconfigReferences,
}

/// Configuration for [TsconfigOptions::references]
#[derive(Debug, Clone)]
pub enum TsconfigReferences {
    Disabled,
    /// Use the `references` field from tsconfig of `config_file`.
    Auto,
    /// Manually provided relative or absolute path.
    Paths(Vec<PathBuf>),
}

impl Default for ResolveOptions {
    fn default() -> Self {
        Self {
            tsconfig: None,
            alias: vec![],
            alias_fields: vec![],
            condition_names: vec![],
            description_files: vec!["package.json".into()],
            enforce_extension: EnforceExtension::Auto,
            extension_alias: vec![],
            exports_fields: vec![vec!["exports".into()]],
            extensions: vec![".js".into(), ".json".into(), ".node".into()],
            fallback: vec![],
            fully_specified: false,
            main_fields: vec!["main".into()],
            main_files: vec!["index".into()],
            modules: vec!["node_modules".into()],
            resolve_to_context: false,
            prefer_relative: false,
            prefer_absolute: false,
            restrictions: vec![],
            roots: vec![],
            symlinks: true,
            builtin_modules: false,
        }
    }
}

impl ResolveOptions {
    pub(crate) fn sanitize(mut self) -> Self {
        // Set `enforceExtension` to `true` when [ResolveOptions::extensions] contains an empty string.
        // See <https://github.com/webpack/enhanced-resolve/pull/285>
        if self.enforce_extension == EnforceExtension::Auto {
            if !self.extensions.is_empty() && self.extensions.iter().any(String::is_empty) {
                self.enforce_extension = EnforceExtension::Enabled;
            } else {
                self.enforce_extension = EnforceExtension::Disabled;
            }
        }
        self
    }
}

// For tracing
impl fmt::Display for ResolveOptions {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if let Some(tsconfig) = &self.tsconfig {
            write!(f, "tsconfig:{tsconfig:?},")?;
        }
        if !self.alias.is_empty() {
            write!(f, "alias:{:?},", self.alias)?;
        }
        if !self.alias_fields.is_empty() {
            write!(f, "alias_fields:{:?},", self.alias_fields)?;
        }
        if !self.condition_names.is_empty() {
            write!(f, "condition_names:{:?},", self.condition_names)?;
        }
        if self.enforce_extension.is_enabled() {
            write!(f, "enforce_extension:{:?},", self.enforce_extension)?;
        }
        if !self.exports_fields.is_empty() {
            write!(f, "exports_fields:{:?},", self.exports_fields)?;
        }
        if !self.extension_alias.is_empty() {
            write!(f, "extension_alias:{:?},", self.extension_alias)?;
        }
        if !self.extensions.is_empty() {
            write!(f, "extensions:{:?},", self.extensions)?;
        }
        if !self.fallback.is_empty() {
            write!(f, "fallback:{:?},", self.fallback)?;
        }
        if self.fully_specified {
            write!(f, "fully_specified:{:?},", self.fully_specified)?;
        }
        if !self.main_fields.is_empty() {
            write!(f, "main_fields:{:?},", self.main_fields)?;
        }
        if !self.main_files.is_empty() {
            write!(f, "main_files:{:?},", self.main_files)?;
        }
        if !self.modules.is_empty() {
            write!(f, "modules:{:?},", self.modules)?;
        }
        if self.resolve_to_context {
            write!(f, "resolve_to_context:{:?},", self.resolve_to_context)?;
        }
        if self.prefer_relative {
            write!(f, "prefer_relative:{:?},", self.prefer_relative)?;
        }
        if self.prefer_absolute {
            write!(f, "prefer_absolute:{:?},", self.prefer_absolute)?;
        }
        if !self.restrictions.is_empty() {
            write!(f, "restrictions:{:?},", self.restrictions)?;
        }
        if !self.roots.is_empty() {
            write!(f, "roots:{:?},", self.roots)?;
        }
        if self.symlinks {
            write!(f, "symlinks:{:?},", self.symlinks)?;
        }
        if self.builtin_modules {
            write!(f, "builtin_modules:{:?},", self.builtin_modules)?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use super::{
        AliasValue, EnforceExtension, ResolveOptions, Restriction, TsconfigOptions,
        TsconfigReferences,
    };
    use std::path::PathBuf;

    #[test]
    fn enforce_extension() {
        assert!(EnforceExtension::Auto.is_auto());
        assert!(!EnforceExtension::Enabled.is_auto());
        assert!(!EnforceExtension::Disabled.is_auto());

        assert!(!EnforceExtension::Auto.is_enabled());
        assert!(EnforceExtension::Enabled.is_enabled());
        assert!(!EnforceExtension::Disabled.is_enabled());

        assert!(!EnforceExtension::Auto.is_disabled());
        assert!(!EnforceExtension::Enabled.is_disabled());
        assert!(EnforceExtension::Disabled.is_disabled());
    }

    #[test]
    fn display() {
        let options = ResolveOptions {
            tsconfig: Some(TsconfigOptions {
                config_file: PathBuf::from("tsconfig.json"),
                references: TsconfigReferences::Auto,
            }),
            alias: vec![("a".into(), vec![AliasValue::Ignore])],
            alias_fields: vec![vec!["browser".into()]],
            condition_names: vec!["require".into()],
            enforce_extension: EnforceExtension::Enabled,
            extension_alias: vec![(".js".into(), vec![".ts".into()])],
            exports_fields: vec![vec!["exports".into()]],
            fallback: vec![("fallback".into(), vec![AliasValue::Ignore])],
            fully_specified: true,
            resolve_to_context: true,
            prefer_relative: true,
            prefer_absolute: true,
            restrictions: vec![Restriction::Path(PathBuf::from("restrictions"))],
            roots: vec![PathBuf::from("roots")],
            builtin_modules: true,
            ..ResolveOptions::default()
        };

        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"]],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,"#;
        assert_eq!(format!("{options}"), expected);
    }
}