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