1use std::{
2 borrow::Cow,
3 fmt::Debug,
4 hash::BuildHasherDefault,
5 path::{Path, PathBuf},
6 sync::Arc,
7};
8
9use compact_str::CompactString;
10use indexmap::IndexMap;
11use rustc_hash::FxHasher;
12use serde::Deserialize;
13
14use crate::{TsconfigReferences, path::PathUtil, replace_bom_with_whitespace};
15
16const TEMPLATE_VARIABLE: &str = "${configDir}";
25
26pub type CompilerOptionsPathsMap = IndexMap<String, Vec<PathBuf>, BuildHasherDefault<FxHasher>>;
27
28#[derive(Clone, Debug, Deserialize)]
32pub struct ProjectReference {
33 pub path: PathBuf,
34}
35
36#[derive(Clone, Debug, Default, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct TsConfig {
39 #[serde(skip)]
42 pub root: bool,
43
44 #[serde(skip)]
47 should_build: bool,
48
49 #[serde(skip)]
51 pub path: PathBuf,
52
53 #[serde(default)]
54 pub files: Option<Vec<PathBuf>>,
55
56 #[serde(default)]
57 pub include: Option<Vec<PathBuf>>,
58
59 #[serde(default)]
60 pub exclude: Option<Vec<PathBuf>>,
61
62 #[serde(default)]
63 pub extends: Option<ExtendsField>,
64
65 #[serde(default)]
66 pub compiler_options: CompilerOptions,
67
68 #[serde(default)]
69 pub references: Vec<ProjectReference>,
70
71 #[serde(skip)]
75 pub references_resolved: Vec<Arc<Self>>,
76}
77
78impl TsConfig {
79 pub fn parse(
94 root: bool,
95 path: &Path,
96 canonical_path: &Path,
97 json: String,
98 ) -> Result<Self, serde_json::Error> {
99 let mut json = json.into_bytes();
100 replace_bom_with_whitespace(&mut json);
101 _ = json_strip_comments::strip_slice(&mut json);
102 let mut tsconfig: Self = if json.iter().all(u8::is_ascii_whitespace) {
103 Self::default()
104 } else {
105 serde_json::from_slice(&json)?
106 };
107 tsconfig.root = root;
108 tsconfig.path = path.to_path_buf();
109 let canonical_directory = canonical_path.parent().unwrap();
110 tsconfig.compiler_options.paths_base =
111 tsconfig.compiler_options.base_url.as_ref().map_or_else(
112 || canonical_directory.to_path_buf(),
113 |base_url| {
114 if base_url.to_string_lossy().starts_with(TEMPLATE_VARIABLE) {
115 base_url.clone()
116 } else {
117 canonical_directory.normalize_with(base_url)
118 }
119 },
120 );
121 if let Some(root_dirs) = &mut tsconfig.compiler_options.root_dirs {
122 for root_dir in root_dirs.iter_mut() {
123 if !root_dir.to_string_lossy().starts_with(TEMPLATE_VARIABLE) {
124 *root_dir = canonical_directory.normalize_with(&root_dir);
125 }
126 }
127 }
128 if let Some(out_dir) = &mut tsconfig.compiler_options.out_dir
129 && !out_dir.to_string_lossy().starts_with(TEMPLATE_VARIABLE)
130 {
131 *out_dir = canonical_directory.normalize_with(&out_dir);
132 }
133 if let Some(declaration_dir) = &mut tsconfig.compiler_options.declaration_dir
134 && !declaration_dir.to_string_lossy().starts_with(TEMPLATE_VARIABLE)
135 {
136 *declaration_dir = canonical_directory.normalize_with(&declaration_dir);
137 }
138 Ok(tsconfig)
139 }
140
141 #[must_use]
144 pub fn root(&self) -> bool {
145 self.root
146 }
147
148 #[must_use]
150 pub fn should_build(&self) -> bool {
151 self.should_build
152 }
153
154 pub fn set_should_build(&mut self, should_build: bool) {
156 self.should_build = should_build;
157 }
158
159 #[must_use]
163 pub fn path(&self) -> &Path {
164 &self.path
165 }
166
167 #[must_use]
173 pub fn directory(&self) -> &Path {
174 debug_assert!(self.path.file_name().is_some());
175 self.path.parent().unwrap()
176 }
177
178 pub(crate) fn extends(&self) -> impl Iterator<Item = &str> {
180 let specifiers = match &self.extends {
181 Some(ExtendsField::Single(specifier)) => {
182 vec![specifier.as_str()]
183 }
184 Some(ExtendsField::Multiple(specifiers)) => {
185 specifiers.iter().map(String::as_str).collect()
186 }
187 None => Vec::new(),
188 };
189 specifiers.into_iter()
190 }
191
192 pub(crate) fn load_references(&mut self, references: TsconfigReferences) -> bool {
196 match references {
197 TsconfigReferences::Disabled => {
198 self.references.clear();
199 }
200 TsconfigReferences::Auto => {}
201 }
202 !self.references.is_empty()
203 }
204
205 #[expect(clippy::too_many_lines, reason = "field-by-field merge of every inherited setting")]
207 pub(crate) fn extend_tsconfig(&mut self, tsconfig: &Self) {
208 if self.files.is_none()
209 && let Some(files) = &tsconfig.files
210 {
211 self.files = Some(files.clone());
212 }
213
214 if self.include.is_none()
215 && let Some(include) = &tsconfig.include
216 {
217 self.include = Some(include.clone());
218 }
219
220 if self.exclude.is_none()
221 && let Some(exclude) = &tsconfig.exclude
222 {
223 self.exclude = Some(exclude.clone());
224 }
225
226 let compiler_options = &mut self.compiler_options;
227
228 if compiler_options.base_url.is_none() {
229 compiler_options.base_url.clone_from(&tsconfig.compiler_options.base_url);
230 if tsconfig.compiler_options.base_url.is_some() {
231 compiler_options.paths_base.clone_from(&tsconfig.compiler_options.paths_base);
232 }
233 }
234 if compiler_options.paths.is_none() {
235 if compiler_options.base_url.is_none() && tsconfig.compiler_options.base_url.is_none() {
236 compiler_options.paths_base.clone_from(&tsconfig.compiler_options.paths_base);
237 }
238 compiler_options.paths.clone_from(&tsconfig.compiler_options.paths);
239 }
240
241 if compiler_options.experimental_decorators.is_none()
242 && let Some(experimental_decorators) =
243 &tsconfig.compiler_options.experimental_decorators
244 {
245 compiler_options.experimental_decorators = Some(*experimental_decorators);
246 }
247
248 if compiler_options.emit_decorator_metadata.is_none()
249 && let Some(emit_decorator_metadata) =
250 &tsconfig.compiler_options.emit_decorator_metadata
251 {
252 compiler_options.emit_decorator_metadata = Some(*emit_decorator_metadata);
253 }
254
255 if compiler_options.strict.is_none()
256 && let Some(strict) = &tsconfig.compiler_options.strict
257 {
258 compiler_options.strict = Some(*strict);
259 }
260
261 if compiler_options.strict_null_checks.is_none()
262 && let Some(strict_null_checks) = &tsconfig.compiler_options.strict_null_checks
263 {
264 compiler_options.strict_null_checks = Some(*strict_null_checks);
265 }
266
267 if compiler_options.use_define_for_class_fields.is_none()
268 && let Some(use_define_for_class_fields) =
269 &tsconfig.compiler_options.use_define_for_class_fields
270 {
271 compiler_options.use_define_for_class_fields = Some(*use_define_for_class_fields);
272 }
273
274 if compiler_options.rewrite_relative_import_extensions.is_none()
275 && let Some(rewrite_relative_import_extensions) =
276 &tsconfig.compiler_options.rewrite_relative_import_extensions
277 {
278 compiler_options.rewrite_relative_import_extensions =
279 Some(*rewrite_relative_import_extensions);
280 }
281
282 if compiler_options.jsx.is_none()
283 && let Some(jsx) = &tsconfig.compiler_options.jsx
284 {
285 compiler_options.jsx = Some(jsx.clone());
286 }
287
288 if compiler_options.jsx_factory.is_none()
289 && let Some(jsx_factory) = &tsconfig.compiler_options.jsx_factory
290 {
291 compiler_options.jsx_factory = Some(jsx_factory.clone());
292 }
293
294 if compiler_options.jsx_fragment_factory.is_none()
295 && let Some(jsx_fragment_factory) = &tsconfig.compiler_options.jsx_fragment_factory
296 {
297 compiler_options.jsx_fragment_factory = Some(jsx_fragment_factory.clone());
298 }
299
300 if compiler_options.jsx_import_source.is_none()
301 && let Some(jsx_import_source) = &tsconfig.compiler_options.jsx_import_source
302 {
303 compiler_options.jsx_import_source = Some(jsx_import_source.clone());
304 }
305
306 if compiler_options.verbatim_module_syntax.is_none()
307 && let Some(verbatim_module_syntax) = &tsconfig.compiler_options.verbatim_module_syntax
308 {
309 compiler_options.verbatim_module_syntax = Some(*verbatim_module_syntax);
310 }
311
312 if compiler_options.preserve_value_imports.is_none()
313 && let Some(preserve_value_imports) = &tsconfig.compiler_options.preserve_value_imports
314 {
315 compiler_options.preserve_value_imports = Some(*preserve_value_imports);
316 }
317
318 if compiler_options.imports_not_used_as_values.is_none()
319 && let Some(imports_not_used_as_values) =
320 &tsconfig.compiler_options.imports_not_used_as_values
321 {
322 compiler_options.imports_not_used_as_values = Some(imports_not_used_as_values.clone());
323 }
324
325 if compiler_options.target.is_none()
326 && let Some(target) = &tsconfig.compiler_options.target
327 {
328 compiler_options.target = Some(target.clone());
329 }
330
331 if compiler_options.module.is_none()
332 && let Some(module) = &tsconfig.compiler_options.module
333 {
334 compiler_options.module = Some(module.clone());
335 }
336
337 if compiler_options.allow_js.is_none()
338 && let Some(allow_js) = &tsconfig.compiler_options.allow_js
339 {
340 compiler_options.allow_js = Some(*allow_js);
341 }
342
343 if compiler_options.root_dirs.is_none()
344 && let Some(root_dirs) = &tsconfig.compiler_options.root_dirs
345 {
346 compiler_options.root_dirs = Some(root_dirs.clone());
347 }
348
349 if compiler_options.out_dir.is_none()
350 && let Some(out_dir) = &tsconfig.compiler_options.out_dir
351 {
352 compiler_options.out_dir = Some(out_dir.clone());
353 }
354
355 if compiler_options.declaration_dir.is_none()
356 && let Some(declaration_dir) = &tsconfig.compiler_options.declaration_dir
357 {
358 compiler_options.declaration_dir = Some(declaration_dir.clone());
359 }
360
361 if compiler_options.resolve_json_module.is_none()
362 && let Some(resolve_json_module) = &tsconfig.compiler_options.resolve_json_module
363 {
364 compiler_options.resolve_json_module = Some(*resolve_json_module);
365 }
366
367 if compiler_options.check_js.is_none()
368 && let Some(check_js) = &tsconfig.compiler_options.check_js
369 {
370 compiler_options.check_js = Some(*check_js);
371 }
372 }
373
374 #[must_use]
380 pub(crate) fn build(mut self) -> Self {
381 if !self.should_build {
384 return self;
385 }
386
387 let config_dir = self.directory().to_path_buf();
388
389 if let Some(files) = self.files.take() {
391 self.files = Some(files.into_iter().map(|p| self.adjust_path(p)).collect());
392 }
393
394 if let Some(includes) = self.include.take() {
396 self.include = Some(includes.into_iter().map(|p| self.adjust_path(p)).collect());
397 }
398
399 if let Some(excludes) = self.exclude.take() {
401 self.exclude = Some(excludes.into_iter().map(|p| self.adjust_path(p)).collect());
402 }
403
404 if let Some(base_url) = &self.compiler_options.base_url {
405 self.compiler_options.base_url = Some(self.adjust_path(base_url.clone()));
406 }
407
408 if let Some(stripped_path) =
409 self.compiler_options.paths_base.to_string_lossy().strip_prefix(TEMPLATE_VARIABLE)
410 {
411 self.compiler_options.paths_base =
412 config_dir.join(stripped_path.trim_start_matches('/'));
413 }
414
415 if let Some(root_dirs) = &mut self.compiler_options.root_dirs {
416 for root_dir in root_dirs.iter_mut() {
417 if let Some(stripped_path) =
418 root_dir.to_string_lossy().strip_prefix(TEMPLATE_VARIABLE)
419 {
420 *root_dir = config_dir.join(stripped_path.trim_start_matches('/'));
421 }
422 }
423 }
424
425 if let Some(out_dir) = &mut self.compiler_options.out_dir
426 && let Some(stripped_path) = out_dir.to_string_lossy().strip_prefix(TEMPLATE_VARIABLE)
427 {
428 *out_dir = config_dir.join(stripped_path.trim_start_matches('/'));
429 }
430 if let Some(declaration_dir) = &mut self.compiler_options.declaration_dir
431 && let Some(stripped_path) =
432 declaration_dir.to_string_lossy().strip_prefix(TEMPLATE_VARIABLE)
433 {
434 *declaration_dir = config_dir.join(stripped_path.trim_start_matches('/'));
435 }
436
437 if let Some(paths_map) = &mut self.compiler_options.paths {
438 for paths in paths_map.values_mut() {
440 for path in paths {
441 *path = if let Some(stripped_path) =
442 path.to_string_lossy().strip_prefix(TEMPLATE_VARIABLE)
443 {
444 config_dir.join(stripped_path.trim_start_matches('/'))
445 } else {
446 self.compiler_options.paths_base.normalize_with(&path)
447 };
448 }
449 }
450 self.compiler_options.compiled_paths =
451 Some(Arc::new(CompiledTsconfigPaths::new(paths_map)));
452 } else {
453 self.compiler_options.compiled_paths = None;
454 }
455
456 self
457 }
458
459 #[expect(clippy::option_if_let_else, reason = "the if/else reads clearer than `map_or_else`")]
460 fn adjust_path(&self, path: PathBuf) -> PathBuf {
461 if let Some(stripped) = path.to_string_lossy().strip_prefix(TEMPLATE_VARIABLE) {
462 self.directory().join(stripped.trim_start_matches('/'))
463 } else {
464 self.directory().normalize_with(path)
465 }
466 }
467
468 #[must_use]
472 pub(crate) fn resolve_references_then_self_paths(
473 &self,
474 path: &Path,
475 specifier: &str,
476 ) -> Vec<PathBuf> {
477 for tsconfig in &self.references_resolved {
478 if path.starts_with(&tsconfig.compiler_options.paths_base) {
479 return tsconfig.resolve_path_alias(specifier);
480 }
481 }
482 self.resolve_path_alias(specifier)
483 }
484
485 #[must_use]
492 pub(crate) fn resolve_path_alias(&self, specifier: &str) -> Vec<PathBuf> {
493 if specifier.starts_with('.') {
494 return Vec::new();
495 }
496
497 let compiler_options = &self.compiler_options;
498
499 let Some(paths_map) = &compiler_options.paths else {
500 return vec![];
501 };
502
503 if let Some(paths) = paths_map.get(specifier) {
504 return paths.clone();
505 }
506
507 if let Some(compiled_paths) = &compiler_options.compiled_paths
508 && let Some(paths) = compiled_paths.resolve(specifier)
509 {
510 return paths;
511 }
512
513 Vec::new()
514 }
515
516 pub(crate) fn resolve_base_url(&self, specifier: &str) -> Option<PathBuf> {
517 self.compiler_options
518 .base_url
519 .is_some()
520 .then(|| self.compiler_options.paths_base.normalize_with(specifier))
521 }
522
523 #[must_use]
536 pub fn resolve_path_alias_or_base_url(&self, specifier: &str) -> Vec<PathBuf> {
537 let mut paths = self.resolve_path_alias(specifier);
538 if paths.is_empty()
539 && let Some(base_url_path) = self.resolve_base_url(specifier)
540 {
541 paths.push(base_url_path);
542 }
543 paths
544 }
545}
546
547#[derive(Clone, Debug, Default, Deserialize)]
551#[serde(rename_all = "camelCase")]
552pub struct CompilerOptions {
553 pub base_url: Option<PathBuf>,
554
555 pub paths: Option<CompilerOptionsPathsMap>,
557
558 #[serde(skip)]
560 compiled_paths: Option<Arc<CompiledTsconfigPaths>>,
561
562 #[serde(skip)]
564 pub(crate) paths_base: PathBuf,
565
566 pub experimental_decorators: Option<bool>,
568
569 pub emit_decorator_metadata: Option<bool>,
571
572 pub strict: Option<bool>,
574
575 pub strict_null_checks: Option<bool>,
577
578 pub use_define_for_class_fields: Option<bool>,
580
581 pub rewrite_relative_import_extensions: Option<bool>,
583
584 pub jsx: Option<String>,
586
587 pub jsx_factory: Option<String>,
589
590 pub jsx_fragment_factory: Option<String>,
592
593 pub jsx_import_source: Option<String>,
595
596 pub verbatim_module_syntax: Option<bool>,
598
599 pub preserve_value_imports: Option<bool>,
601
602 pub imports_not_used_as_values: Option<String>,
604
605 pub target: Option<String>,
607
608 pub module: Option<String>,
610
611 pub allow_js: Option<bool>,
613
614 pub root_dirs: Option<Vec<PathBuf>>,
616
617 pub out_dir: Option<PathBuf>,
619
620 pub declaration_dir: Option<PathBuf>,
622
623 pub resolve_json_module: Option<bool>,
625
626 pub check_js: Option<bool>,
628}
629
630#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
634#[serde(untagged)]
635pub enum ExtendsField {
636 Single(String),
637 Multiple(Vec<String>),
638}
639
640#[derive(Clone, Copy)]
641enum GlobPattern<'a> {
642 Pattern(&'a [PathBuf]),
643 All,
644}
645
646fn to_forward_slashes(s: Cow<'_, str>) -> Cow<'_, str> {
648 if s.contains('\\') { Cow::Owned(s.replace('\\', "/")) } else { s }
649}
650
651impl TsConfig {
653 pub(crate) fn resolve_tsconfig_solution(tsconfig: Arc<Self>, path: &Path) -> Arc<Self> {
654 if !tsconfig.references_resolved.is_empty()
655 && let Some(solution_tsconfig) = tsconfig
656 .references_resolved
657 .iter()
658 .find(|referenced| referenced.is_file_included_in_tsconfig(path))
659 .map(Arc::clone)
660 {
661 return solution_tsconfig;
662 }
663 tsconfig
664 }
665
666 pub(crate) fn claims_ownership_of(&self, path: &Path) -> bool {
672 if self.references_resolved.iter().any(|r| r.is_file_included_in_tsconfig(path)) {
675 return true;
676 }
677 let is_solution_style = !self.references_resolved.is_empty()
683 && matches!(self.files.as_deref(), Some([]))
684 && matches!(self.include.as_deref(), Some([]));
685 if is_solution_style {
686 return false;
687 }
688 self.is_file_included_in_tsconfig(path)
689 }
690
691 fn is_file_included_in_tsconfig(&self, path: &Path) -> bool {
692 if self.files.as_ref().is_some_and(|files| files.iter().any(|file| Path::new(file) == path))
694 {
695 return true;
696 }
697 if self.is_extensionless_or_uncompiled_js(path) {
700 return false;
701 }
702 let is_included = self.include.as_ref().map_or_else(
704 || {
705 if self.files.is_some() {
706 false
707 } else {
708 self.is_glob_matches(path, GlobPattern::All)
709 }
710 },
711 |include_patterns| self.is_glob_matches(path, GlobPattern::Pattern(include_patterns)),
712 );
713 if is_included {
715 return self.exclude.as_ref().is_none_or(|exclude_patterns| {
716 !self.is_glob_matches(path, GlobPattern::Pattern(exclude_patterns))
717 });
718 }
719 false
720 }
721
722 fn is_glob_matches(&self, path: &Path, pattern: GlobPattern) -> bool {
723 match pattern {
724 GlobPattern::All => {
728 path.starts_with(self.directory())
729 && self.is_file_extension_allowed_in_tsconfig(path)
730 }
731 GlobPattern::Pattern(patterns) => {
732 let path_str = to_forward_slashes(path.to_string_lossy());
733 patterns.iter().any(|pattern| {
734 let pattern = to_forward_slashes(pattern.to_string_lossy());
735 self.is_glob_match(pattern.as_ref(), path, path_str.as_ref())
736 })
737 }
738 }
739 }
740
741 fn is_glob_match(&self, pattern: &str, path: &Path, path_str: &str) -> bool {
742 if pattern == path_str {
743 return true;
744 }
745 let after_last_slash = pattern.rsplit('/').next().unwrap_or(pattern);
748 let needs_implicit_glob = !after_last_slash.contains(['.', '*', '?']);
749 let pattern = if needs_implicit_glob {
750 Cow::Owned(format!(
751 "{pattern}{}",
752 if pattern.ends_with('/') { "**/*" } else { "/**/*" }
753 ))
754 } else {
755 Cow::Borrowed(pattern)
756 };
757 if pattern.ends_with('*') && !self.is_file_extension_allowed_in_tsconfig(path) {
766 return false;
767 }
768 fast_glob::glob_match(pattern.as_ref(), path_str)
769 }
770
771 fn is_file_extension_allowed_in_tsconfig(&self, path: &Path) -> bool {
772 const TS_EXTENSIONS: [&str; 4] = ["ts", "tsx", "mts", "cts"];
773 const JS_EXTENSIONS: [&str; 4] = ["js", "jsx", "mjs", "cjs"];
774 let allow_js = self.compiler_options.allow_js.is_some_and(|b| b);
775 path.extension().and_then(|ext| ext.to_str()).is_some_and(|ext| {
776 TS_EXTENSIONS.contains(&ext)
777 || if allow_js { JS_EXTENSIONS.contains(&ext) } else { false }
778 })
779 }
780
781 fn is_extensionless_or_uncompiled_js(&self, path: &Path) -> bool {
786 let allow_js = self.compiler_options.allow_js.is_some_and(|b| b);
787 path.extension()
788 .and_then(|ext| ext.to_str())
789 .is_none_or(|ext| !allow_js && matches!(ext, "js" | "jsx" | "mjs" | "cjs"))
790 }
791}
792
793#[derive(Clone, Debug, Default)]
794struct CompiledTsconfigPaths {
795 wildcard_patterns: Vec<CompiledTsconfigPathPattern>,
796}
797
798#[derive(Clone, Debug)]
799struct CompiledTsconfigPathPattern {
800 prefix: CompactString,
801 suffix: CompactString,
802 prefix_len: usize,
803 suffix_len: usize,
804 targets: Vec<CompiledTsconfigPathTarget>,
805}
806
807#[derive(Clone, Debug)]
808enum CompiledTsconfigPathTarget {
809 Static(PathBuf),
810 Wildcard { prefix: CompactString, suffix: CompactString },
811}
812
813impl CompiledTsconfigPaths {
814 fn new(paths_map: &CompilerOptionsPathsMap) -> Self {
815 let mut wildcard_patterns =
816 Vec::<CompiledTsconfigPathPattern>::with_capacity(paths_map.len());
817 for (key, paths) in paths_map {
818 let Some((prefix, suffix)) = key.split_once('*') else {
819 continue;
820 };
821 let targets = paths
822 .iter()
823 .map(|path| {
824 let path_str = path.to_string_lossy();
825 path_str.split_once('*').map_or_else(
826 || CompiledTsconfigPathTarget::Static(path.clone()),
827 |(target_prefix, target_suffix)| CompiledTsconfigPathTarget::Wildcard {
828 prefix: CompactString::new(target_prefix),
829 suffix: CompactString::new(target_suffix),
830 },
831 )
832 })
833 .collect::<Vec<_>>();
834 let pattern = CompiledTsconfigPathPattern {
835 prefix: CompactString::new(prefix),
836 suffix: CompactString::new(suffix),
837 prefix_len: prefix.len(),
838 suffix_len: suffix.len(),
839 targets,
840 };
841
842 let index = wildcard_patterns
844 .iter()
845 .position(|existing| existing.prefix_len < pattern.prefix_len)
846 .unwrap_or(wildcard_patterns.len());
847 wildcard_patterns.insert(index, pattern);
848 }
849
850 Self { wildcard_patterns }
851 }
852
853 fn resolve(&self, specifier: &str) -> Option<Vec<PathBuf>> {
854 self.wildcard_patterns.iter().find_map(|pattern| {
855 if !specifier.starts_with(pattern.prefix.as_str())
856 || !specifier.ends_with(pattern.suffix.as_str())
857 || specifier.len() < pattern.prefix_len + pattern.suffix_len
858 {
859 return None;
860 }
861 let wildcard = &specifier[pattern.prefix_len..specifier.len() - pattern.suffix_len];
862 Some(pattern.targets.iter().map(|target| target.resolve(wildcard)).collect())
863 })
864 }
865}
866
867impl CompiledTsconfigPathTarget {
868 fn resolve(&self, wildcard: &str) -> PathBuf {
869 match self {
870 Self::Static(path) => path.clone(),
871 Self::Wildcard { prefix, suffix } => {
872 let mut resolved =
873 String::with_capacity(prefix.len() + wildcard.len() + suffix.len());
874 resolved.push_str(prefix.as_str());
875 resolved.push_str(wildcard);
876 resolved.push_str(suffix.as_str());
877 PathBuf::from(resolved)
878 }
879 }
880 }
881}