1use std::{collections::HashMap, fmt::Write, 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};
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: 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![rcstr!(".json")];
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 if let FileContent::Content(file) = &*data.await?
59 && file.content().is_empty()
60 {
61 break;
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,
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,
85 source_ident: tsconfig.ident().to_resolved().await?,
86 message: rcstr!("tsconfig not found"),
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,
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#[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 match &*request.await? {
133 Request::Windows { path: Pattern::Constant(path), .. } |
136 Request::ServerRelative { path: Pattern::Constant(path), .. } => {
138 resolve_extends_rooted_or_relative(parent_dir, request, resolve_options, path).await
139 }
140
141 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 Request::Empty => {
152 let request = Request::parse_string(rcstr!("./tsconfig"));
153 Ok(resolve(parent_dir,
154 ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined), request, resolve_options).first_source())
155 }
156
157 _ => {
160 let mut result = resolve(parent_dir, 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, 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 ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined),
179 request,
180 resolve_options,
181 )
182 .first_source();
183
184 if !path.ends_with(".json") && result.await?.is_none() {
188 let request = Request::parse_string(format!("{path}.json").into());
189 result = resolve(
190 lookup_path,
191 ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined),
192 request,
193 resolve_options,
194 )
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<ResolvedVc<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]
233pub async fn tsconfig_resolve_options(
234 tsconfig: Vc<FileSystemPath>,
235) -> Result<Vc<TsConfigResolveOptions>> {
236 let configs = read_tsconfigs(
237 tsconfig.read(),
238 ResolvedVc::upcast(FileSource::new(tsconfig).to_resolved().await?),
239 node_cjs_resolve_options(tsconfig.root()),
240 )
241 .await?;
242
243 if configs.is_empty() {
244 return Ok(Default::default());
245 }
246
247 let base_url = if let Some(base_url) = read_from_tsconfigs(&configs, |json, source| {
248 json["compilerOptions"]["baseUrl"]
249 .as_str()
250 .map(|base_url| source.ident().path().parent().try_join(base_url.into()))
251 })
252 .await?
253 {
254 *base_url.await?
255 } else {
256 None
257 };
258
259 let mut all_paths = HashMap::new();
260 for (content, source) in configs.iter().rev() {
261 if let FileJsonContent::Content(json) = &*content.await?
262 && let JsonValue::Object(paths) = &json["compilerOptions"]["paths"]
263 {
264 let mut context_dir = source.ident().path().parent();
265 if let Some(base_url) = json["compilerOptions"]["baseUrl"].as_str()
266 && let Some(new_context) = *context_dir.try_join(base_url.into()).await?
267 {
268 context_dir = *new_context;
269 };
270 let context_dir = context_dir.to_resolved().await?;
271 for (key, value) in paths.iter() {
272 if let JsonValue::Array(vec) = value {
273 let entries = vec
274 .iter()
275 .filter_map(|entry| {
276 let entry = entry.as_str();
277
278 if entry.map(|e| e.ends_with(".d.ts")).unwrap_or_default() {
279 return None;
280 }
281
282 entry.map(|s| {
283 if s.starts_with("./") || s.starts_with("../") {
285 s.into()
286 } else {
287 format!("./{s}").into()
288 }
289 })
290 })
291 .collect();
292 all_paths.insert(
293 key.to_string(),
294 ImportMapping::primary_alternatives(entries, Some(context_dir)),
295 );
296 } else {
297 TsConfigIssue {
298 severity: IssueSeverity::Warning,
299 source_ident: source.ident().to_resolved().await?,
300 message: format!(
301 "compilerOptions.paths[{key}] doesn't contains an array as \
302 expected\n{key}: {value:#}",
303 key = serde_json::to_string(key)?,
304 value = value
305 )
306 .into(),
307 }
308 .resolved_cell()
309 .emit()
310 }
311 }
312 }
313 }
314
315 let import_map = if !all_paths.is_empty() {
316 let mut import_map = ImportMap::empty();
317 for (key, value) in all_paths {
318 import_map.insert_alias(AliasPattern::parse(key), value.resolved_cell());
319 }
320 Some(import_map.resolved_cell())
321 } else {
322 None
323 };
324
325 let is_module_resolution_nodenext = read_from_tsconfigs(&configs, |json, _| {
326 json["compilerOptions"]["moduleResolution"]
327 .as_str()
328 .map(|module_resolution| module_resolution.eq_ignore_ascii_case("nodenext"))
329 })
330 .await?
331 .unwrap_or_default();
332
333 Ok(TsConfigResolveOptions {
334 base_url,
335 import_map,
336 is_module_resolution_nodenext,
337 }
338 .cell())
339}
340
341#[turbo_tasks::function]
342pub fn tsconfig() -> Vc<Vec<RcStr>> {
343 Vc::cell(vec![rcstr!("tsconfig.json"), rcstr!("jsconfig.json")])
344}
345
346#[turbo_tasks::function]
347pub async fn apply_tsconfig_resolve_options(
348 resolve_options: Vc<ResolveOptions>,
349 tsconfig_resolve_options: Vc<TsConfigResolveOptions>,
350) -> Result<Vc<ResolveOptions>> {
351 let tsconfig_resolve_options = tsconfig_resolve_options.await?;
352 let mut resolve_options = resolve_options.owned().await?;
353 if let Some(base_url) = tsconfig_resolve_options.base_url {
354 resolve_options.modules.insert(
357 0,
358 ResolveModules::Path {
359 dir: base_url,
360 excluded_extensions: ResolvedVc::cell(fxindexset![rcstr!(".json")]),
362 },
363 );
364 }
365 if let Some(tsconfig_import_map) = tsconfig_resolve_options.import_map {
366 resolve_options.import_map = Some(
367 resolve_options
368 .import_map
369 .map(|import_map| import_map.extend(*tsconfig_import_map))
370 .unwrap_or(*tsconfig_import_map)
371 .to_resolved()
372 .await?,
373 );
374 }
375 resolve_options.enable_typescript_with_output_extension =
376 tsconfig_resolve_options.is_module_resolution_nodenext;
377
378 Ok(resolve_options.cell())
379}
380
381#[turbo_tasks::function]
382pub async fn type_resolve(
383 origin: Vc<Box<dyn ResolveOrigin>>,
384 request: Vc<Request>,
385) -> Result<Vc<ModuleResolveResult>> {
386 let ty = ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined);
387 let context_path = origin.origin_path().parent();
388 let options = origin.resolve_options(ty.clone());
389 let options = apply_typescript_types_options(options);
390 let types_request = if let Request::Module {
391 module: m,
392 path: p,
393 query: _,
394 fragment: _,
395 } = &*request.await?
396 {
397 let m = if let Some(stripped) = m.strip_prefix('@') {
398 stripped.replace('/', "__").into()
399 } else {
400 m.clone()
401 };
402 Some(Request::module(
403 format!("@types/{m}").into(),
404 p.clone(),
405 RcStr::default(),
406 RcStr::default(),
407 ))
408 } else {
409 None
410 };
411 let context_path = context_path.resolve().await?;
412 let result = if let Some(types_request) = types_request {
413 let result1 = resolve(
414 context_path,
415 ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined),
416 request,
417 options,
418 );
419 if !*result1.is_unresolvable().await? {
420 result1
421 } else {
422 resolve(
423 context_path,
424 ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined),
425 types_request,
426 options,
427 )
428 }
429 } else {
430 resolve(
431 context_path,
432 ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined),
433 request,
434 options,
435 )
436 };
437 let result = as_typings_result(
438 origin
439 .asset_context()
440 .process_resolve_result(result, ty.clone()),
441 );
442 handle_resolve_error(
443 result,
444 ty,
445 origin.origin_path(),
446 request,
447 options,
448 false,
449 None,
450 )
451 .await
452}
453
454#[turbo_tasks::function]
455pub async fn as_typings_result(result: Vc<ModuleResolveResult>) -> Result<Vc<ModuleResolveResult>> {
456 let mut result = result.owned().await?;
457 result.primary = IntoIterator::into_iter(take(&mut result.primary))
458 .map(|(mut k, v)| {
459 k.conditions.insert("types".to_string(), true);
460 (k, v)
461 })
462 .collect();
463 Ok(result.cell())
464}
465
466#[turbo_tasks::function]
467async fn apply_typescript_types_options(
468 resolve_options: Vc<ResolveOptions>,
469) -> Result<Vc<ResolveOptions>> {
470 let mut resolve_options = resolve_options.owned().await?;
471 resolve_options.extensions = vec![rcstr!(".tsx"), rcstr!(".ts"), rcstr!(".d.ts")];
472 resolve_options.into_package = resolve_options
473 .into_package
474 .drain(..)
475 .filter_map(|into| {
476 if let ResolveIntoPackage::ExportsField {
477 mut conditions,
478 unspecified_conditions,
479 } = into
480 {
481 conditions.insert(rcstr!("types"), ConditionValue::Set);
482 Some(ResolveIntoPackage::ExportsField {
483 conditions,
484 unspecified_conditions,
485 })
486 } else {
487 None
488 }
489 })
490 .collect();
491 resolve_options
492 .into_package
493 .push(ResolveIntoPackage::MainField {
494 field: rcstr!("types"),
495 });
496 for conditions in get_condition_maps(&mut resolve_options) {
497 conditions.insert(rcstr!("types"), ConditionValue::Set);
498 }
499 Ok(resolve_options.into())
500}
501
502#[turbo_tasks::value_impl]
503impl Issue for TsConfigIssue {
504 fn severity(&self) -> IssueSeverity {
505 self.severity
506 }
507
508 #[turbo_tasks::function]
509 fn title(&self) -> Vc<StyledString> {
510 StyledString::Text(rcstr!(
511 "An issue occurred while parsing a tsconfig.json file."
512 ))
513 .cell()
514 }
515
516 #[turbo_tasks::function]
517 fn file_path(&self) -> Vc<FileSystemPath> {
518 self.source_ident.path()
519 }
520
521 #[turbo_tasks::function]
522 fn description(&self) -> Vc<OptionStyledString> {
523 Vc::cell(Some(
524 StyledString::Text(self.message.clone()).resolved_cell(),
525 ))
526 }
527
528 #[turbo_tasks::function]
529 fn stage(&self) -> Vc<IssueStage> {
530 IssueStage::Analysis.cell()
531 }
532}