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