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 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#[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("./tsconfig".into());
153 Ok(resolve(parent_dir,
154 Value::new(ReferenceType::TypeScript(TypeScriptReferenceSubType::Undefined)), request, resolve_options).first_source())
155 }
156
157 _ => {
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 !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#[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#[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 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 resolve_options.modules.insert(
361 0,
362 ResolveModules::Path {
363 dir: base_url,
364 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}