turbopack_resolve/
typescript.rs

1use std::{collections::HashMap, fmt::Write, mem::take};
2
3use anyhow::Result;
4use serde_json::Value as JsonValue;
5use turbo_rcstr::RcStr;
6use turbo_tasks::{ResolvedVc, Value, ValueDefault, Vc, fxindexset};
7use turbo_tasks_fs::{FileContent, FileJsonContent, FileSystemPath};
8use turbopack_core::{
9    asset::Asset,
10    context::AssetContext,
11    file_source::FileSource,
12    ident::AssetIdent,
13    issue::{Issue, IssueExt, IssueSeverity, IssueStage, OptionStyledString, StyledString},
14    reference_type::{ReferenceType, TypeScriptReferenceSubType},
15    resolve::{
16        AliasPattern, ModuleResolveResult, handle_resolve_error,
17        node::node_cjs_resolve_options,
18        options::{
19            ConditionValue, ImportMap, ImportMapping, ResolveIntoPackage, ResolveModules,
20            ResolveOptions,
21        },
22        origin::{ResolveOrigin, ResolveOriginExt},
23        parse::Request,
24        pattern::Pattern,
25        resolve,
26    },
27    source::{OptionSource, Source},
28};
29
30use crate::ecmascript::get_condition_maps;
31
32#[turbo_tasks::value(shared)]
33pub struct TsConfigIssue {
34    pub severity: ResolvedVc<IssueSeverity>,
35    pub source_ident: ResolvedVc<AssetIdent>,
36    pub message: RcStr,
37}
38
39#[turbo_tasks::function]
40async fn json_only(resolve_options: Vc<ResolveOptions>) -> Result<Vc<ResolveOptions>> {
41    let mut opts = resolve_options.owned().await?;
42    opts.extensions = vec![".json".into()];
43    Ok(opts.cell())
44}
45
46type TsConfig = (Vc<FileJsonContent>, ResolvedVc<Box<dyn Source>>);
47
48#[tracing::instrument(skip_all)]
49pub async fn read_tsconfigs(
50    mut data: Vc<FileContent>,
51    mut tsconfig: ResolvedVc<Box<dyn Source>>,
52    resolve_options: Vc<ResolveOptions>,
53) -> Result<Vec<TsConfig>> {
54    let mut configs = Vec::new();
55    let resolve_options = json_only(resolve_options);
56    loop {
57        // tsc ignores empty config files.
58        if let FileContent::Content(file) = &*data.await? {
59            if file.content().is_empty() {
60                break;
61            }
62        }
63
64        let parsed_data = data.parse_json_with_comments();
65        match &*parsed_data.await? {
66            FileJsonContent::Unparseable(e) => {
67                let mut message = "tsconfig is not parseable: invalid JSON: ".to_string();
68                if let FileContent::Content(content) = &*data.await? {
69                    let text = content.content().to_str()?;
70                    e.write_with_content(&mut message, text.as_ref())?;
71                } else {
72                    write!(message, "{e}")?;
73                }
74                TsConfigIssue {
75                    severity: IssueSeverity::Error.resolved_cell(),
76                    source_ident: tsconfig.ident().to_resolved().await?,
77                    message: message.into(),
78                }
79                .resolved_cell()
80                .emit();
81            }
82            FileJsonContent::NotFound => {
83                TsConfigIssue {
84                    severity: IssueSeverity::Error.resolved_cell(),
85                    source_ident: tsconfig.ident().to_resolved().await?,
86                    message: "tsconfig not found".into(),
87                }
88                .resolved_cell()
89                .emit();
90            }
91            FileJsonContent::Content(json) => {
92                configs.push((parsed_data, tsconfig));
93                if let Some(extends) = json["extends"].as_str() {
94                    let resolved = resolve_extends(*tsconfig, extends, resolve_options).await?;
95                    if let Some(source) = *resolved.await? {
96                        data = source.content().file_content();
97                        tsconfig = source;
98                        continue;
99                    } else {
100                        TsConfigIssue {
101                            severity: IssueSeverity::Error.resolved_cell(),
102                            source_ident: tsconfig.ident().to_resolved().await?,
103                            message: format!("extends: \"{extends}\" doesn't resolve correctly")
104                                .into(),
105                        }
106                        .resolved_cell()
107                        .emit();
108                    }
109                }
110            }
111        }
112        break;
113    }
114    Ok(configs)
115}
116
117/// Resolves tsconfig files according to TS's implementation:
118/// https://github.com/microsoft/TypeScript/blob/611a912d/src/compiler/commandLineParser.ts#L3294-L3326
119#[tracing::instrument(skip_all)]
120async fn resolve_extends(
121    tsconfig: Vc<Box<dyn Source>>,
122    extends: &str,
123    resolve_options: Vc<ResolveOptions>,
124) -> Result<Vc<OptionSource>> {
125    let parent_dir = tsconfig.ident().path().parent();
126    let request = Request::parse_string(extends.into());
127
128    // TS's resolution is weird, and has special behavior for different import
129    // types. There might be multiple alternatives like
130    // "some/path/node_modules/xyz/abc.json" and "some/node_modules/xyz/abc.json".
131    // We only want to use the first one.
132    match &*request.await? {
133        // TS has special behavior for "rooted" paths (absolute paths):
134        // https://github.com/microsoft/TypeScript/blob/611a912d/src/compiler/commandLineParser.ts#L3303-L3313
135        Request::Windows { path: Pattern::Constant(path), .. } |
136        // Server relative is treated as absolute
137        Request::ServerRelative { path: Pattern::Constant(path), .. } => {
138            resolve_extends_rooted_or_relative(parent_dir, request, resolve_options, path).await
139        }
140
141        // TS has special behavior for (explicitly) './' and '../', but not '.' nor '..':
142        // https://github.com/microsoft/TypeScript/blob/611a912d/src/compiler/commandLineParser.ts#L3303-L3313
143        Request::Relative {
144            path: Pattern::Constant(path),
145            ..
146        } if path.starts_with("./") || path.starts_with("../") => {
147            resolve_extends_rooted_or_relative(parent_dir, request, resolve_options, path).await
148        }
149
150        // An empty extends is treated as "./tsconfig"
151        Request::Empty => {
152            let request = Request::parse_string("./tsconfig".into());
153            Ok(resolve(parent_dir,
154                Value::new(ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined)), request, resolve_options).first_source())
155        }
156
157        // All other types are treated as module imports, and potentially joined with
158        // "tsconfig.json". This includes "relative" imports like '.' and '..'.
159        _ => {
160            let mut result = resolve(parent_dir, Value::new(ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined)), request, resolve_options).first_source();
161            if result.await?.is_none() {
162                let request = Request::parse_string(format!("{extends}/tsconfig").into());
163                result = resolve(parent_dir, Value::new(ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined)), request, resolve_options).first_source();
164            }
165            Ok(result)
166        }
167    }
168}
169
170async fn resolve_extends_rooted_or_relative(
171    lookup_path: Vc<FileSystemPath>,
172    request: Vc<Request>,
173    resolve_options: Vc<ResolveOptions>,
174    path: &str,
175) -> Result<Vc<OptionSource>> {
176    let mut result = resolve(
177        lookup_path,
178        Value::new(ReferenceType::TypeScript(
179            TypeScriptReferenceSubType::Undefined,
180        )),
181        request,
182        resolve_options,
183    )
184    .first_source();
185
186    // If the file doesn't end with ".json" and we can't find the file, then we have
187    // to try again with it.
188    // https://github.com/microsoft/TypeScript/blob/611a912d/src/compiler/commandLineParser.ts#L3305
189    if !path.ends_with(".json") && result.await?.is_none() {
190        let request = Request::parse_string(format!("{path}.json").into());
191        result = resolve(
192            lookup_path,
193            Value::new(ReferenceType::TypeScript(
194                TypeScriptReferenceSubType::Undefined,
195            )),
196            request,
197            resolve_options,
198        )
199        .first_source();
200    }
201    Ok(result)
202}
203
204pub async fn read_from_tsconfigs<T>(
205    configs: &[TsConfig],
206    accessor: impl Fn(&JsonValue, ResolvedVc<Box<dyn Source>>) -> Option<T>,
207) -> Result<Option<T>> {
208    for (config, source) in configs.iter() {
209        if let FileJsonContent::Content(json) = &*config.await? {
210            if let Some(result) = accessor(json, *source) {
211                return Ok(Some(result));
212            }
213        }
214    }
215    Ok(None)
216}
217
218/// Resolve options specific to tsconfig.json.
219#[turbo_tasks::value]
220#[derive(Default)]
221pub struct TsConfigResolveOptions {
222    base_url: Option<ResolvedVc<FileSystemPath>>,
223    import_map: Option<ResolvedVc<ImportMap>>,
224    is_module_resolution_nodenext: bool,
225}
226
227#[turbo_tasks::value_impl]
228impl ValueDefault for TsConfigResolveOptions {
229    #[turbo_tasks::function]
230    fn value_default() -> Vc<Self> {
231        Self::default().cell()
232    }
233}
234
235/// Returns the resolve options
236#[turbo_tasks::function]
237pub async fn tsconfig_resolve_options(
238    tsconfig: Vc<FileSystemPath>,
239) -> Result<Vc<TsConfigResolveOptions>> {
240    let configs = read_tsconfigs(
241        tsconfig.read(),
242        ResolvedVc::upcast(FileSource::new(tsconfig).to_resolved().await?),
243        node_cjs_resolve_options(tsconfig.root()),
244    )
245    .await?;
246
247    if configs.is_empty() {
248        return Ok(Default::default());
249    }
250
251    let base_url = if let Some(base_url) = read_from_tsconfigs(&configs, |json, source| {
252        json["compilerOptions"]["baseUrl"]
253            .as_str()
254            .map(|base_url| source.ident().path().parent().try_join(base_url.into()))
255    })
256    .await?
257    {
258        *base_url.await?
259    } else {
260        None
261    };
262
263    let mut all_paths = HashMap::new();
264    for (content, source) in configs.iter().rev() {
265        if let FileJsonContent::Content(json) = &*content.await? {
266            if let JsonValue::Object(paths) = &json["compilerOptions"]["paths"] {
267                let mut context_dir = source.ident().path().parent();
268                if let Some(base_url) = json["compilerOptions"]["baseUrl"].as_str() {
269                    if let Some(new_context) = *context_dir.try_join(base_url.into()).await? {
270                        context_dir = *new_context;
271                    }
272                };
273                let context_dir = context_dir.to_resolved().await?;
274                for (key, value) in paths.iter() {
275                    if let JsonValue::Array(vec) = value {
276                        let entries = vec
277                            .iter()
278                            .filter_map(|entry| {
279                                let entry = entry.as_str();
280
281                                if entry.map(|e| e.ends_with(".d.ts")).unwrap_or_default() {
282                                    return None;
283                                }
284
285                                entry.map(|s| {
286                                    // tsconfig paths are always relative requests
287                                    if s.starts_with("./") || s.starts_with("../") {
288                                        s.into()
289                                    } else {
290                                        format!("./{s}").into()
291                                    }
292                                })
293                            })
294                            .collect();
295                        all_paths.insert(
296                            key.to_string(),
297                            ImportMapping::primary_alternatives(entries, Some(context_dir)),
298                        );
299                    } else {
300                        TsConfigIssue {
301                            severity: IssueSeverity::Warning.resolved_cell(),
302                            source_ident: source.ident().to_resolved().await?,
303                            message: format!(
304                                "compilerOptions.paths[{key}] doesn't contains an array as \
305                                 expected\n{key}: {value:#}",
306                                key = serde_json::to_string(key)?,
307                                value = value
308                            )
309                            .into(),
310                        }
311                        .resolved_cell()
312                        .emit()
313                    }
314                }
315            }
316        }
317    }
318
319    let import_map = if !all_paths.is_empty() {
320        let mut import_map = ImportMap::empty();
321        for (key, value) in all_paths {
322            import_map.insert_alias(AliasPattern::parse(key), value.resolved_cell());
323        }
324        Some(import_map.resolved_cell())
325    } else {
326        None
327    };
328
329    let is_module_resolution_nodenext = read_from_tsconfigs(&configs, |json, _| {
330        json["compilerOptions"]["moduleResolution"]
331            .as_str()
332            .map(|module_resolution| module_resolution.eq_ignore_ascii_case("nodenext"))
333    })
334    .await?
335    .unwrap_or_default();
336
337    Ok(TsConfigResolveOptions {
338        base_url,
339        import_map,
340        is_module_resolution_nodenext,
341    }
342    .cell())
343}
344
345#[turbo_tasks::function]
346pub fn tsconfig() -> Vc<Vec<RcStr>> {
347    Vc::cell(vec!["tsconfig.json".into(), "jsconfig.json".into()])
348}
349
350#[turbo_tasks::function]
351pub async fn apply_tsconfig_resolve_options(
352    resolve_options: Vc<ResolveOptions>,
353    tsconfig_resolve_options: Vc<TsConfigResolveOptions>,
354) -> Result<Vc<ResolveOptions>> {
355    let tsconfig_resolve_options = tsconfig_resolve_options.await?;
356    let mut resolve_options = resolve_options.owned().await?;
357    if let Some(base_url) = tsconfig_resolve_options.base_url {
358        // We want to resolve in `compilerOptions.baseUrl` first, then in other
359        // locations as a fallback.
360        resolve_options.modules.insert(
361            0,
362            ResolveModules::Path {
363                dir: base_url,
364                // tsconfig basepath doesn't apply to json requests
365                excluded_extensions: ResolvedVc::cell(fxindexset![".json".into()]),
366            },
367        );
368    }
369    if let Some(tsconfig_import_map) = tsconfig_resolve_options.import_map {
370        resolve_options.import_map = Some(
371            resolve_options
372                .import_map
373                .map(|import_map| import_map.extend(*tsconfig_import_map))
374                .unwrap_or(*tsconfig_import_map)
375                .to_resolved()
376                .await?,
377        );
378    }
379    resolve_options.enable_typescript_with_output_extension =
380        tsconfig_resolve_options.is_module_resolution_nodenext;
381
382    Ok(resolve_options.cell())
383}
384
385#[turbo_tasks::function]
386pub async fn type_resolve(
387    origin: Vc<Box<dyn ResolveOrigin>>,
388    request: Vc<Request>,
389) -> Result<Vc<ModuleResolveResult>> {
390    let ty = Value::new(ReferenceType::TypeScript(
391        TypeScriptReferenceSubType::Undefined,
392    ));
393    let context_path = origin.origin_path().parent();
394    let options = origin.resolve_options(ty.clone());
395    let options = apply_typescript_types_options(options);
396    let types_request = if let Request::Module {
397        module: m,
398        path: p,
399        query: _,
400        fragment: _,
401    } = &*request.await?
402    {
403        let m = if let Some(stripped) = m.strip_prefix('@') {
404            stripped.replace('/', "__").into()
405        } else {
406            m.clone()
407        };
408        Some(Request::module(
409            format!("@types/{m}").into(),
410            Value::new(p.clone()),
411            Vc::<RcStr>::default(),
412            Vc::<RcStr>::default(),
413        ))
414    } else {
415        None
416    };
417    let context_path = context_path.resolve().await?;
418    let result = if let Some(types_request) = types_request {
419        let result1 = resolve(
420            context_path,
421            Value::new(ReferenceType::TypeScript(
422                TypeScriptReferenceSubType::Undefined,
423            )),
424            request,
425            options,
426        );
427        if !*result1.is_unresolvable().await? {
428            result1
429        } else {
430            resolve(
431                context_path,
432                Value::new(ReferenceType::TypeScript(
433                    TypeScriptReferenceSubType::Undefined,
434                )),
435                types_request,
436                options,
437            )
438        }
439    } else {
440        resolve(
441            context_path,
442            Value::new(ReferenceType::TypeScript(
443                TypeScriptReferenceSubType::Undefined,
444            )),
445            request,
446            options,
447        )
448    };
449    let result = as_typings_result(
450        origin
451            .asset_context()
452            .process_resolve_result(result, ty.clone()),
453    );
454    handle_resolve_error(
455        result,
456        ty,
457        origin.origin_path(),
458        request,
459        options,
460        false,
461        None,
462    )
463    .await
464}
465
466#[turbo_tasks::function]
467pub async fn as_typings_result(result: Vc<ModuleResolveResult>) -> Result<Vc<ModuleResolveResult>> {
468    let mut result = result.owned().await?;
469    result.primary = IntoIterator::into_iter(take(&mut result.primary))
470        .map(|(mut k, v)| {
471            k.conditions.insert("types".to_string(), true);
472            (k, v)
473        })
474        .collect();
475    Ok(result.cell())
476}
477
478#[turbo_tasks::function]
479async fn apply_typescript_types_options(
480    resolve_options: Vc<ResolveOptions>,
481) -> Result<Vc<ResolveOptions>> {
482    let mut resolve_options = resolve_options.owned().await?;
483    resolve_options.extensions = vec![".tsx".into(), ".ts".into(), ".d.ts".into()];
484    resolve_options.into_package = resolve_options
485        .into_package
486        .drain(..)
487        .filter_map(|into| {
488            if let ResolveIntoPackage::ExportsField {
489                mut conditions,
490                unspecified_conditions,
491            } = into
492            {
493                conditions.insert("types".into(), ConditionValue::Set);
494                Some(ResolveIntoPackage::ExportsField {
495                    conditions,
496                    unspecified_conditions,
497                })
498            } else {
499                None
500            }
501        })
502        .collect();
503    resolve_options
504        .into_package
505        .push(ResolveIntoPackage::MainField {
506            field: "types".into(),
507        });
508    for conditions in get_condition_maps(&mut resolve_options) {
509        conditions.insert("types".into(), ConditionValue::Set);
510    }
511    Ok(resolve_options.into())
512}
513
514#[turbo_tasks::value_impl]
515impl Issue for TsConfigIssue {
516    #[turbo_tasks::function]
517    fn severity(&self) -> Vc<IssueSeverity> {
518        *self.severity
519    }
520
521    #[turbo_tasks::function]
522    fn title(&self) -> Vc<StyledString> {
523        StyledString::Text("An issue occurred while parsing a tsconfig.json file.".into()).cell()
524    }
525
526    #[turbo_tasks::function]
527    fn file_path(&self) -> Vc<FileSystemPath> {
528        self.source_ident.path()
529    }
530
531    #[turbo_tasks::function]
532    fn description(&self) -> Vc<OptionStyledString> {
533        Vc::cell(Some(
534            StyledString::Text(self.message.clone()).resolved_cell(),
535        ))
536    }
537
538    #[turbo_tasks::function]
539    fn stage(&self) -> Vc<IssueStage> {
540        IssueStage::Analysis.cell()
541    }
542}