turbopack_resolve/
resolve.rs

1use anyhow::Result;
2use turbo_rcstr::rcstr;
3use turbo_tasks::{ResolvedVc, Vc};
4use turbo_tasks_fs::{FileSystem, FileSystemPath};
5use turbopack_core::resolve::{
6    AliasMap, AliasPattern, ExternalTraced, ExternalType, FindContextFileResult, find_context_file,
7    options::{
8        ConditionValue, ImportMap, ImportMapping, ResolutionConditions, ResolveInPackage,
9        ResolveIntoPackage, ResolveModules, ResolveOptions,
10    },
11};
12
13use crate::{
14    resolve_options_context::ResolveOptionsContext,
15    typescript::{apply_tsconfig_resolve_options, tsconfig, tsconfig_resolve_options},
16};
17
18const NODE_EXTERNALS: [&str; 64] = [
19    "assert",
20    "assert/strict",
21    "async_hooks",
22    "buffer",
23    "child_process",
24    "cluster",
25    "console",
26    "constants",
27    "crypto",
28    "dgram",
29    "diagnostics_channel",
30    "dns",
31    "dns/promises",
32    "domain",
33    "events",
34    "fs",
35    "fs/promises",
36    "http",
37    "http2",
38    "https",
39    "inspector",
40    "module",
41    "net",
42    "os",
43    "path",
44    "path/posix",
45    "path/win32",
46    "perf_hooks",
47    "process",
48    "punycode",
49    "querystring",
50    "readline",
51    "repl",
52    "stream",
53    "stream/promises",
54    "stream/web",
55    "string_decoder",
56    "sys",
57    "timers",
58    "timers/promises",
59    "tls",
60    "trace_events",
61    "tty",
62    "url",
63    "util",
64    "util/types",
65    "v8",
66    "vm",
67    "wasi",
68    "worker_threads",
69    "zlib",
70    "pnpapi",
71    "_http_agent",
72    "_http_client",
73    "_http_common",
74    "_http_incoming",
75    "_http_outgoing",
76    "_http_server",
77    "_stream_duplex",
78    "_stream_passthrough",
79    "_stream_readable",
80    "_stream_transform",
81    "_stream_wrap",
82    "_stream_writable",
83];
84
85const EDGE_NODE_EXTERNALS: [&str; 5] = ["buffer", "events", "assert", "util", "async_hooks"];
86
87#[turbo_tasks::function]
88async fn base_resolve_options(
89    fs: ResolvedVc<Box<dyn FileSystem>>,
90    options_context: Vc<ResolveOptionsContext>,
91) -> Result<Vc<ResolveOptions>> {
92    let opt = options_context.await?;
93    let emulating = opt.emulate_environment;
94    let root = fs.root().await?.clone_value();
95    let mut direct_mappings = AliasMap::new();
96    let node_externals = if let Some(environment) = emulating {
97        environment.node_externals().owned().await?
98    } else {
99        opt.enable_node_externals
100    };
101    if node_externals || opt.enable_edge_node_externals {
102        let untraced_external_cell =
103            ImportMapping::External(None, ExternalType::CommonJs, ExternalTraced::Untraced)
104                .resolved_cell();
105
106        if node_externals {
107            for req in NODE_EXTERNALS {
108                direct_mappings.insert(AliasPattern::exact(req), untraced_external_cell);
109                direct_mappings.insert(
110                    AliasPattern::exact(format!("node:{req}")),
111                    untraced_external_cell,
112                );
113            }
114        }
115        if opt.enable_edge_node_externals {
116            for req in EDGE_NODE_EXTERNALS {
117                direct_mappings.insert(
118                    AliasPattern::exact(req),
119                    ImportMapping::External(
120                        Some(format!("node:{req}").into()),
121                        ExternalType::CommonJs,
122                        ExternalTraced::Untraced,
123                    )
124                    .resolved_cell(),
125                );
126                direct_mappings.insert(
127                    AliasPattern::exact(format!("node:{req}")),
128                    untraced_external_cell,
129                );
130            }
131        }
132    }
133
134    let mut import_map = ImportMap::new(direct_mappings);
135    if let Some(additional_import_map) = opt.import_map {
136        let additional_import_map = additional_import_map.await?;
137        import_map.extend_ref(&additional_import_map);
138    }
139    let import_map = import_map.resolved_cell();
140
141    let conditions = {
142        let mut conditions: ResolutionConditions = [
143            (rcstr!("import"), ConditionValue::Unknown),
144            (rcstr!("require"), ConditionValue::Unknown),
145        ]
146        .into_iter()
147        .collect();
148        if opt.browser {
149            conditions.insert(rcstr!("browser"), ConditionValue::Set);
150        }
151        if opt.module {
152            conditions.insert(rcstr!("module"), ConditionValue::Set);
153        }
154        if let Some(environment) = emulating {
155            for condition in environment.resolve_conditions().await?.iter() {
156                conditions.insert(condition.clone(), ConditionValue::Set);
157            }
158        }
159        for condition in opt.custom_conditions.iter() {
160            conditions.insert(condition.clone(), ConditionValue::Set);
161        }
162        // Infer some well-known conditions
163        let dev = conditions.get("development").cloned();
164        let prod = conditions.get("production").cloned();
165        if prod.is_none() {
166            conditions.insert(
167                rcstr!("production"),
168                if matches!(dev, Some(ConditionValue::Set)) {
169                    ConditionValue::Unset
170                } else {
171                    ConditionValue::Unknown
172                },
173            );
174        }
175        if dev.is_none() {
176            conditions.insert(
177                rcstr!("development"),
178                if matches!(prod, Some(ConditionValue::Set)) {
179                    ConditionValue::Unset
180                } else {
181                    ConditionValue::Unknown
182                },
183            );
184        }
185        conditions
186    };
187
188    let extensions = if let Some(custom_extension) = &opt.custom_extensions {
189        custom_extension.clone()
190    } else if let Some(environment) = emulating {
191        environment.resolve_extensions().owned().await?
192    } else {
193        let mut ext = Vec::new();
194        if opt.enable_typescript && opt.enable_react {
195            ext.push(rcstr!(".tsx"));
196        }
197        if opt.enable_typescript {
198            ext.push(rcstr!(".ts"));
199        }
200        if opt.enable_react {
201            ext.push(rcstr!(".jsx"));
202        }
203        ext.push(rcstr!(".js"));
204        if opt.enable_mjs_extension {
205            ext.push(rcstr!(".mjs"));
206        }
207        if opt.enable_node_native_modules {
208            ext.push(rcstr!(".node"));
209        }
210        ext.push(rcstr!(".json"));
211        ext
212    };
213    Ok(ResolveOptions {
214        extensions,
215        modules: if let Some(environment) = emulating {
216            if *environment.resolve_node_modules().await? {
217                vec![ResolveModules::Nested(
218                    root.clone(),
219                    vec![rcstr!("node_modules")],
220                )]
221            } else {
222                Vec::new()
223            }
224        } else {
225            let mut mods = Vec::new();
226            if let Some(dir) = &opt.enable_node_modules {
227                mods.push(ResolveModules::Nested(
228                    dir.clone(),
229                    vec![rcstr!("node_modules")],
230                ));
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: rcstr!("browser"),
242                });
243            }
244            if opt.module {
245                resolve_into.push(ResolveIntoPackage::MainField {
246                    field: rcstr!("module"),
247                });
248            }
249            resolve_into.push(ResolveIntoPackage::MainField {
250                field: rcstr!("main"),
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(rcstr!("browser")));
261            }
262            resolve_in
263        },
264        default_files: vec![rcstr!("index")],
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: 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        for (condition, new_options_context) in options_context_value.rules.iter() {
283            if condition.matches(&resolve_path) {
284                return Ok(resolve_options(resolve_path, **new_options_context));
285            }
286        }
287    }
288
289    let resolve_options = base_resolve_options(*resolve_path.fs, options_context);
290
291    let resolve_options = if options_context_value.enable_typescript {
292        let find_tsconfig = async || {
293            // Otherwise, attempt to find a tsconfig up the file tree
294            let tsconfig = find_context_file(resolve_path.clone(), tsconfig()).await?;
295            anyhow::Ok::<Vc<ResolveOptions>>(match &*tsconfig {
296                FindContextFileResult::Found(path, _) => apply_tsconfig_resolve_options(
297                    resolve_options,
298                    tsconfig_resolve_options(path.clone()),
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.clone()),
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}