1use std::mem::take;
2
3use anyhow::Result;
4use async_trait::async_trait;
5use serde_json::Value as JsonValue;
6use turbo_rcstr::{RcStr, rcstr};
7use turbo_tasks::{FxIndexMap, ResolvedVc, ValueDefault, Vc, fxindexset};
8use turbo_tasks_fs::{FileContent, FileJsonContent, FileSystemPath, FileSystemPathOption};
9use turbopack_core::{
10 asset::Asset,
11 context::AssetContext,
12 file_source::FileSource,
13 issue::{Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, StyledString},
14 reference_type::{ReferenceType, TypeScriptReferenceSubType},
15 resolve::{
16 AliasPattern, ModuleResolveResult, RequestKey, ResolveErrorMode,
17 error::handle_resolve_error,
18 node::node_cjs_resolve_options,
19 options::{
20 ConditionValue, ImportMap, ImportMapping, ResolveIntoPackage, ResolveModules,
21 ResolveOptions,
22 },
23 origin::ResolveOrigin,
24 parse::Request,
25 pattern::Pattern,
26 resolve,
27 },
28 source::Source,
29};
30
31use crate::ecmascript::get_condition_maps;
32
33#[turbo_tasks::value(shared)]
34struct TsConfigIssue {
35 severity: IssueSeverity,
36 source: IssueSource,
37 message: RcStr,
38}
39
40#[turbo_tasks::function]
41async fn json_only(resolve_options: Vc<ResolveOptions>) -> Result<Vc<ResolveOptions>> {
42 let mut opts = resolve_options.owned().await?;
43 opts.extensions = vec![rcstr!(".json")];
44 Ok(opts.cell())
45}
46
47type TsConfig = (Vc<FileJsonContent>, ResolvedVc<Box<dyn Source>>);
48
49#[tracing::instrument(skip_all)]
50pub async fn read_tsconfigs(
51 mut data: Vc<FileContent>,
52 mut tsconfig: ResolvedVc<Box<dyn Source>>,
53 resolve_options: Vc<ResolveOptions>,
54) -> Result<Vec<TsConfig>> {
55 let mut configs = Vec::new();
56 let resolve_options = json_only(resolve_options);
57 loop {
58 if let FileContent::Content(file) = &*data.await?
60 && file.content().is_empty()
61 {
62 break;
63 }
64
65 let parsed_data = data.parse_json_with_comments();
66 match &*parsed_data.await? {
67 FileJsonContent::Unparsable(e) => {
68 let message = format!("tsconfig is not parseable: invalid JSON: {}", e.message);
69 let source = IssueSource::from_unparsable_json(tsconfig, e);
70 TsConfigIssue {
71 severity: IssueSeverity::Error,
72 source,
73 message: message.into(),
74 }
75 .resolved_cell()
76 .emit();
77 }
78 FileJsonContent::NotFound => {
79 TsConfigIssue {
80 severity: IssueSeverity::Error,
81 source: IssueSource::from_source_only(tsconfig),
82 message: rcstr!("tsconfig not found"),
83 }
84 .resolved_cell()
85 .emit();
86 }
87 FileJsonContent::Content(json) => {
88 configs.push((parsed_data, tsconfig));
89 if let Some(extends) = json["extends"].as_str() {
90 let resolved = resolve_extends(*tsconfig, extends, resolve_options).await?;
91 if let Some(source) = resolved {
92 data = source.content().file_content();
93 tsconfig = source;
94 continue;
95 } else {
96 TsConfigIssue {
97 severity: IssueSeverity::Error,
98 source: IssueSource::from_source_only(tsconfig),
100 message: format!("extends: \"{extends}\" doesn't resolve correctly")
101 .into(),
102 }
103 .resolved_cell()
104 .emit();
105 }
106 }
107 }
108 }
109 break;
110 }
111 Ok(configs)
112}
113
114#[tracing::instrument(skip_all)]
117async fn resolve_extends(
118 tsconfig: Vc<Box<dyn Source>>,
119 extends: &str,
120 resolve_options: Vc<ResolveOptions>,
121) -> Result<Option<ResolvedVc<Box<dyn Source>>>> {
122 let parent_dir = tsconfig.ident().await?.path.parent();
123 let request = Request::parse_string(extends.into());
124
125 match &*request.await? {
130 Request::Windows { path: Pattern::Constant(path), .. } |
133 Request::ServerRelative { path: Pattern::Constant(path), .. } => {
135 resolve_extends_rooted_or_relative(parent_dir, request, resolve_options, path).await
136 }
137
138 Request::Relative {
141 path: Pattern::Constant(path),
142 ..
143 } if path.starts_with("./") || path.starts_with("../") => {
144 resolve_extends_rooted_or_relative(parent_dir, request, resolve_options, path).await
145 }
146
147 Request::Empty => {
149 let request = Request::parse_string(rcstr!("./tsconfig"));
150 Ok(resolve(parent_dir,
151 ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined), request, resolve_options).await?.first_source())
152 }
153
154 _ => {
157 let result = resolve(parent_dir.clone(), ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined), request, resolve_options).await?;
158 if let Some(source) = result.first_source() {
159 return Ok(Some(source));
160 }
161 let request = Request::parse_string(format!("{extends}/tsconfig").into());
162 Ok(resolve(parent_dir, ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined), request, resolve_options).await?.first_source())
163 }
164 }
165}
166
167async fn resolve_extends_rooted_or_relative(
168 lookup_path: FileSystemPath,
169 request: Vc<Request>,
170 resolve_options: Vc<ResolveOptions>,
171 path: &str,
172) -> Result<Option<ResolvedVc<Box<dyn Source>>>> {
173 let result = resolve(
174 lookup_path.clone(),
175 ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined),
176 request,
177 resolve_options,
178 )
179 .await?;
180
181 let mut result = result.first_source();
182
183 if !path.ends_with(".json") && result.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 .await?
195 .first_source();
196 }
197 Ok(result)
198}
199
200pub async fn read_from_tsconfigs<T>(
201 configs: &[TsConfig],
202 accessor: impl Fn(&JsonValue, ResolvedVc<Box<dyn Source>>) -> Option<T>,
203) -> Result<Option<T>> {
204 for (config, source) in configs.iter() {
205 if let FileJsonContent::Content(json) = &*config.await?
206 && let Some(result) = accessor(json, *source)
207 {
208 return Ok(Some(result));
209 }
210 }
211 Ok(None)
212}
213
214#[turbo_tasks::value]
216#[derive(Default)]
217pub struct TsConfigResolveOptions {
218 base_url: Option<FileSystemPath>,
219 import_map: Option<ResolvedVc<ImportMap>>,
220 is_module_resolution_nodenext: bool,
221}
222
223#[turbo_tasks::value_impl]
224impl ValueDefault for TsConfigResolveOptions {
225 #[turbo_tasks::function]
226 fn value_default() -> Vc<Self> {
227 Self::default().cell()
228 }
229}
230
231#[turbo_tasks::function]
232async fn try_join_base_url(
233 source: ResolvedVc<Box<dyn Source>>,
234 base_url: RcStr,
235) -> Result<Vc<FileSystemPathOption>> {
236 Ok(Vc::cell(
237 source.ident().await?.path.parent().try_join(&base_url),
238 ))
239}
240
241#[turbo_tasks::function]
243pub async fn tsconfig_resolve_options(
244 tsconfig: FileSystemPath,
245) -> Result<Vc<TsConfigResolveOptions>> {
246 let configs = read_tsconfigs(
247 tsconfig.read(),
248 ResolvedVc::upcast(FileSource::new(tsconfig.clone()).to_resolved().await?),
249 node_cjs_resolve_options(tsconfig.root().owned().await?),
250 )
251 .await?;
252
253 if configs.is_empty() {
254 return Ok(Default::default());
255 }
256
257 let base_url = if let Some(base_url) = read_from_tsconfigs(&configs, |json, source| {
258 json["compilerOptions"]["baseUrl"]
259 .as_str()
260 .map(|base_url| try_join_base_url(*source, base_url.into()))
261 })
262 .await?
263 {
264 base_url.owned().await?
265 } else {
266 None
267 };
268
269 let mut all_paths = FxIndexMap::default();
270 for (content, source) in configs.iter().rev() {
271 if let FileJsonContent::Content(json) = &*content.await?
272 && let JsonValue::Object(paths) = &json["compilerOptions"]["paths"]
273 {
274 let mut context_dir = source.ident().await?.path.parent();
275 if let Some(base_url) = json["compilerOptions"]["baseUrl"].as_str()
276 && let Some(new_context) = context_dir.try_join(base_url)
277 {
278 context_dir = new_context;
279 };
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.await?.is_unresolvable() {
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#[async_trait]
519#[turbo_tasks::value_impl]
520impl Issue for TsConfigIssue {
521 fn severity(&self) -> IssueSeverity {
522 self.severity
523 }
524
525 async fn title(&self) -> anyhow::Result<StyledString> {
526 Ok(StyledString::Text(rcstr!(
527 "An issue occurred while parsing a tsconfig.json file."
528 )))
529 }
530
531 async fn file_path(&self) -> anyhow::Result<FileSystemPath> {
532 self.source.file_path().await
533 }
534
535 async fn description(&self) -> anyhow::Result<Option<StyledString>> {
536 Ok(Some(StyledString::Text(self.message.clone())))
537 }
538
539 fn stage(&self) -> IssueStage {
540 IssueStage::Analysis
541 }
542
543 fn source(&self) -> Option<IssueSource> {
544 Some(self.source)
545 }
546}