turbopack_resolve/
resolve.rs

1use anyhow::Result;
2use turbo_tasks::Vc;
3use turbo_tasks_fs::{FileSystem, FileSystemPath};
4use turbopack_core::resolve::{
5    AliasMap, AliasPattern, ExternalTraced, ExternalType, FindContextFileResult, find_context_file,
6    options::{
7        ConditionValue, ImportMap, ImportMapping, ResolutionConditions, ResolveInPackage,
8        ResolveIntoPackage, ResolveModules, ResolveOptions,
9    },
10};
11
12use crate::{
13    resolve_options_context::ResolveOptionsContext,
14    typescript::{apply_tsconfig_resolve_options, tsconfig, tsconfig_resolve_options},
15};
16
17const NODE_EXTERNALS: [&str; 63] = [
18    "assert",
19    "async_hooks",
20    "buffer",
21    "child_process",
22    "cluster",
23    "console",
24    "constants",
25    "crypto",
26    "dgram",
27    "diagnostics_channel",
28    "dns",
29    "dns/promises",
30    "domain",
31    "events",
32    "fs",
33    "fs/promises",
34    "http",
35    "http2",
36    "https",
37    "inspector",
38    "module",
39    "net",
40    "os",
41    "path",
42    "path/posix",
43    "path/win32",
44    "perf_hooks",
45    "process",
46    "punycode",
47    "querystring",
48    "readline",
49    "repl",
50    "stream",
51    "stream/promises",
52    "stream/web",
53    "string_decoder",
54    "sys",
55    "timers",
56    "timers/promises",
57    "tls",
58    "trace_events",
59    "tty",
60    "url",
61    "util",
62    "util/types",
63    "v8",
64    "vm",
65    "wasi",
66    "worker_threads",
67    "zlib",
68    "pnpapi",
69    "_http_agent",
70    "_http_client",
71    "_http_common",
72    "_http_incoming",
73    "_http_outgoing",
74    "_http_server",
75    "_stream_duplex",
76    "_stream_passthrough",
77    "_stream_readable",
78    "_stream_transform",
79    "_stream_wrap",
80    "_stream_writable",
81];
82
83const EDGE_NODE_EXTERNALS: [&str; 5] = ["buffer", "events", "assert", "util", "async_hooks"];
84
85#[turbo_tasks::function]
86async fn base_resolve_options(
87    resolve_path: Vc<FileSystemPath>,
88    options_context: Vc<ResolveOptionsContext>,
89) -> Result<Vc<ResolveOptions>> {
90    let parent = resolve_path.parent().resolve().await?;
91    if parent != resolve_path {
92        return Ok(base_resolve_options(parent, options_context));
93    }
94    let resolve_path_value = resolve_path.await?;
95    let opt = options_context.await?;
96    let emulating = opt.emulate_environment;
97    let root = resolve_path_value.fs.root();
98    let mut direct_mappings = AliasMap::new();
99    let node_externals = if let Some(environment) = emulating {
100        environment.node_externals().owned().await?
101    } else {
102        opt.enable_node_externals
103    };
104    if node_externals {
105        for req in NODE_EXTERNALS {
106            direct_mappings.insert(
107                AliasPattern::exact(req),
108                ImportMapping::External(None, ExternalType::CommonJs, ExternalTraced::Untraced)
109                    .resolved_cell(),
110            );
111            direct_mappings.insert(
112                AliasPattern::exact(format!("node:{req}")),
113                ImportMapping::External(None, ExternalType::CommonJs, ExternalTraced::Untraced)
114                    .resolved_cell(),
115            );
116        }
117    }
118    if opt.enable_edge_node_externals {
119        for req in EDGE_NODE_EXTERNALS {
120            direct_mappings.insert(
121                AliasPattern::exact(req),
122                ImportMapping::External(
123                    Some(format!("node:{req}").into()),
124                    ExternalType::CommonJs,
125                    ExternalTraced::Untraced,
126                )
127                .resolved_cell(),
128            );
129            direct_mappings.insert(
130                AliasPattern::exact(format!("node:{req}")),
131                ImportMapping::External(None, ExternalType::CommonJs, ExternalTraced::Untraced)
132                    .resolved_cell(),
133            );
134        }
135    }
136
137    let mut import_map = ImportMap::new(direct_mappings);
138    if let Some(additional_import_map) = opt.import_map {
139        let additional_import_map = additional_import_map.await?;
140        import_map.extend_ref(&additional_import_map);
141    }
142    let import_map = import_map.resolved_cell();
143
144    let conditions = {
145        let mut conditions: ResolutionConditions = [
146            ("import".into(), ConditionValue::Unknown),
147            ("require".into(), ConditionValue::Unknown),
148        ]
149        .into_iter()
150        .collect();
151        if opt.browser {
152            conditions.insert("browser".into(), ConditionValue::Set);
153        }
154        if opt.module {
155            conditions.insert("module".into(), ConditionValue::Set);
156        }
157        if let Some(environment) = emulating {
158            for condition in environment.resolve_conditions().await?.iter() {
159                conditions.insert(condition.clone(), ConditionValue::Set);
160            }
161        }
162        for condition in opt.custom_conditions.iter() {
163            conditions.insert(condition.clone(), ConditionValue::Set);
164        }
165        // Infer some well-known conditions
166        let dev = conditions.get("development").cloned();
167        let prod = conditions.get("production").cloned();
168        if prod.is_none() {
169            conditions.insert(
170                "production".into(),
171                if matches!(dev, Some(ConditionValue::Set)) {
172                    ConditionValue::Unset
173                } else {
174                    ConditionValue::Unknown
175                },
176            );
177        }
178        if dev.is_none() {
179            conditions.insert(
180                "development".into(),
181                if matches!(prod, Some(ConditionValue::Set)) {
182                    ConditionValue::Unset
183                } else {
184                    ConditionValue::Unknown
185                },
186            );
187        }
188        conditions
189    };
190
191    let extensions = if let Some(custom_extension) = &opt.custom_extensions {
192        custom_extension.clone()
193    } else if let Some(environment) = emulating {
194        environment.resolve_extensions().owned().await?
195    } else {
196        let mut ext = Vec::new();
197        if opt.enable_typescript && opt.enable_react {
198            ext.push(".tsx".into());
199        }
200        if opt.enable_typescript {
201            ext.push(".ts".into());
202        }
203        if opt.enable_react {
204            ext.push(".jsx".into());
205        }
206        ext.push(".js".into());
207        if opt.enable_mjs_extension {
208            ext.push(".mjs".into());
209        }
210        if opt.enable_node_native_modules {
211            ext.push(".node".into());
212        }
213        ext.push(".json".into());
214        ext
215    };
216    Ok(ResolveOptions {
217        extensions,
218        modules: if let Some(environment) = emulating {
219            if *environment.resolve_node_modules().await? {
220                vec![ResolveModules::Nested(
221                    root.to_resolved().await?,
222                    vec!["node_modules".into()],
223                )]
224            } else {
225                Vec::new()
226            }
227        } else {
228            let mut mods = Vec::new();
229            if let Some(dir) = opt.enable_node_modules {
230                mods.push(ResolveModules::Nested(dir, vec!["node_modules".into()]));
231            }
232            mods
233        },
234        into_package: {
235            let mut resolve_into = vec![ResolveIntoPackage::ExportsField {
236                conditions: conditions.clone(),
237                unspecified_conditions: ConditionValue::Unset,
238            }];
239            if opt.browser {
240                resolve_into.push(ResolveIntoPackage::MainField {
241                    field: "browser".into(),
242                });
243            }
244            if opt.module {
245                resolve_into.push(ResolveIntoPackage::MainField {
246                    field: "module".into(),
247                });
248            }
249            resolve_into.push(ResolveIntoPackage::MainField {
250                field: "main".into(),
251            });
252            resolve_into
253        },
254        in_package: {
255            let mut resolve_in = vec![ResolveInPackage::ImportsField {
256                conditions,
257                unspecified_conditions: ConditionValue::Unset,
258            }];
259            if opt.browser {
260                resolve_in.push(ResolveInPackage::AliasField("browser".into()));
261            }
262            resolve_in
263        },
264        default_files: vec!["index".into()],
265        import_map: Some(import_map),
266        resolved_map: opt.resolved_map,
267        after_resolve_plugins: opt.after_resolve_plugins.clone(),
268        before_resolve_plugins: opt.before_resolve_plugins.clone(),
269        loose_errors: opt.loose_errors,
270        ..Default::default()
271    }
272    .into())
273}
274
275#[turbo_tasks::function]
276pub async fn resolve_options(
277    resolve_path: Vc<FileSystemPath>,
278    options_context: Vc<ResolveOptionsContext>,
279) -> Result<Vc<ResolveOptions>> {
280    let options_context_value = options_context.await?;
281    if !options_context_value.rules.is_empty() {
282        let context_value = &*resolve_path.await?;
283        for (condition, new_options_context) in options_context_value.rules.iter() {
284            if condition.matches(context_value).await? {
285                return Ok(resolve_options(resolve_path, **new_options_context));
286            }
287        }
288    }
289
290    let resolve_options = base_resolve_options(resolve_path, options_context);
291
292    let resolve_options = if options_context_value.enable_typescript {
293        let find_tsconfig = async || {
294            // Otherwise, attempt to find a tsconfig up the file tree
295            let tsconfig = find_context_file(resolve_path, tsconfig()).await?;
296            anyhow::Ok::<Vc<ResolveOptions>>(match *tsconfig {
297                FindContextFileResult::Found(path, _) => {
298                    apply_tsconfig_resolve_options(resolve_options, tsconfig_resolve_options(*path))
299                }
300                FindContextFileResult::NotFound(_) => resolve_options,
301            })
302        };
303
304        // Use a specified tsconfig path if provided. In Next.js, this is always provided by the
305        // default config, at the very least.
306        if let Some(tsconfig_path) = options_context_value.tsconfig_path {
307            let meta = tsconfig_path.metadata().await;
308            if meta.is_ok() {
309                // If the file exists, use it.
310                apply_tsconfig_resolve_options(
311                    resolve_options,
312                    tsconfig_resolve_options(*tsconfig_path),
313                )
314            } else {
315                // Otherwise, try and find one.
316                // TODO: If the user provides a tsconfig.json explicitly, this should fail
317                // explicitly. Currently implemented this way for parity with webpack.
318                find_tsconfig().await?
319            }
320        } else {
321            find_tsconfig().await?
322        }
323    } else {
324        resolve_options
325    };
326
327    // Make sure to always apply `options_context.import_map` last, so it properly
328    // overwrites any other mappings.
329    let resolve_options = options_context_value
330        .import_map
331        .map(|import_map| resolve_options.with_extended_import_map(*import_map))
332        .unwrap_or(resolve_options);
333    // And the same for the fallback_import_map
334    let resolve_options = options_context_value
335        .fallback_import_map
336        .map(|fallback_import_map| {
337            resolve_options.with_extended_fallback_import_map(*fallback_import_map)
338        })
339        .unwrap_or(resolve_options);
340
341    Ok(resolve_options)
342}