1use std::path::{Path, PathBuf};
6
7use fallow_extract::visitor::extract_import_from_callable;
8use oxc_allocator::Allocator;
9#[allow(clippy::wildcard_imports, reason = "many AST types used")]
10use oxc_ast::ast::*;
11use oxc_ast_visit::{Visit, walk};
12use oxc_parser::Parser;
13use oxc_span::SourceType;
14use rustc_hash::FxHashSet;
15
16#[must_use]
18pub(crate) fn extract_imports(source: &str, path: &Path) -> Vec<String> {
19 extract_from_source(source, path, |program| {
20 let mut sources = Vec::new();
21 for stmt in &program.body {
22 if let Statement::ImportDeclaration(decl) = stmt {
23 sources.push(decl.source.value.to_string());
24 }
25 }
26 Some(sources)
27 })
28 .unwrap_or_default()
29}
30
31#[must_use]
33pub(crate) fn extract_imports_and_requires(source: &str, path: &Path) -> Vec<String> {
34 extract_from_source(source, path, |program| {
35 let mut sources = Vec::new();
36 for stmt in &program.body {
37 match stmt {
38 Statement::ImportDeclaration(decl) => {
39 sources.push(decl.source.value.to_string());
40 }
41 Statement::ExpressionStatement(expr) => {
42 if let Expression::CallExpression(call) = &expr.expression
43 && is_require_call(call)
44 && let Some(s) = get_require_source(call)
45 {
46 sources.push(s);
47 }
48 }
49 _ => {}
50 }
51 }
52 Some(sources)
53 })
54 .unwrap_or_default()
55}
56
57#[must_use]
59pub(crate) fn extract_config_string_array(
60 source: &str,
61 path: &Path,
62 prop_path: &[&str],
63) -> Vec<String> {
64 extract_from_source(source, path, |program| {
65 let obj = find_config_object(program)?;
66 get_nested_string_array_from_object(obj, prop_path)
67 })
68 .unwrap_or_default()
69}
70
71#[must_use]
73pub(crate) fn extract_config_string(
74 source: &str,
75 path: &Path,
76 prop_path: &[&str],
77) -> Option<String> {
78 extract_from_source(source, path, |program| {
79 let obj = find_config_object(program)?;
80 get_nested_string_from_object(obj, prop_path)
81 })
82}
83
84#[must_use]
86pub(crate) fn extract_config_command(
87 source: &str,
88 path: &Path,
89 prop_path: &[&str],
90) -> Option<String> {
91 extract_from_source(source, path, |program| {
92 let obj = find_config_object(program)?;
93 get_nested_command_from_object(obj, prop_path)
94 })
95}
96
97#[must_use]
100pub(crate) fn extract_config_property_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
101 extract_from_source(source, path, |program| {
102 let obj = find_config_object(program)?;
103 let mut values = Vec::new();
104 if let Some(prop) = find_property(obj, key) {
105 collect_all_string_values(&prop.value, &mut values);
106 }
107 Some(values)
108 })
109 .unwrap_or_default()
110}
111
112#[must_use]
114pub(crate) fn extract_config_shallow_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
115 extract_from_source(source, path, |program| {
116 let obj = find_config_object(program)?;
117 let prop = find_property(obj, key)?;
118 Some(collect_shallow_string_values(&prop.value))
119 })
120 .unwrap_or_default()
121}
122
123#[must_use]
125pub(crate) fn extract_config_shallow_strings_or_object_property(
126 source: &str,
127 path: &Path,
128 key: &str,
129 object_property: &str,
130) -> Vec<String> {
131 extract_from_source(source, path, |program| {
132 let obj = find_config_object(program)?;
133 let prop = find_property(obj, key)?;
134 Some(collect_shallow_string_or_object_property_values(
135 &prop.value,
136 object_property,
137 ))
138 })
139 .unwrap_or_default()
140}
141
142#[must_use]
148pub(crate) fn extract_config_array_objects_shallow_strings_or_object_property(
149 source: &str,
150 path: &Path,
151 array_key: &str,
152 key: &str,
153 object_property: &str,
154) -> Vec<String> {
155 extract_from_source(source, path, |program| {
156 let obj = find_config_object(program)?;
157 let prop = find_property(obj, array_key)?;
158 let Expression::ArrayExpression(arr) = &prop.value else {
159 return None;
160 };
161 let values = arr
162 .elements
163 .iter()
164 .filter_map(|element| match element.as_expression() {
165 Some(Expression::ObjectExpression(element_obj)) => find_property(element_obj, key),
166 _ => None,
167 })
168 .flat_map(|inner| {
169 collect_shallow_string_or_object_property_values(&inner.value, object_property)
170 })
171 .collect();
172 Some(values)
173 })
174 .unwrap_or_default()
175}
176
177#[must_use]
179pub(crate) fn extract_config_nested_shallow_strings(
180 source: &str,
181 path: &Path,
182 outer_path: &[&str],
183 key: &str,
184) -> Vec<String> {
185 extract_from_source(source, path, |program| {
186 let obj = find_config_object(program)?;
187 let nested = get_nested_expression(obj, outer_path)?;
188 if let Expression::ObjectExpression(nested_obj) = nested {
189 let prop = find_property(nested_obj, key)?;
190 Some(collect_shallow_string_values(&prop.value))
191 } else {
192 None
193 }
194 })
195 .unwrap_or_default()
196}
197
198pub(crate) fn property_expr<'a>(
200 obj: &'a ObjectExpression<'a>,
201 key: &str,
202) -> Option<&'a Expression<'a>> {
203 find_property(obj, key).map(|prop| &prop.value)
204}
205
206pub(crate) fn property_object<'a>(
208 obj: &'a ObjectExpression<'a>,
209 key: &str,
210) -> Option<&'a ObjectExpression<'a>> {
211 property_expr(obj, key).and_then(object_expression)
212}
213
214pub(crate) fn property_string(obj: &ObjectExpression<'_>, key: &str) -> Option<String> {
216 property_expr(obj, key).and_then(expression_to_string)
217}
218
219pub(crate) fn object_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ObjectExpression<'a>> {
221 match expr {
222 Expression::ObjectExpression(obj) => Some(obj),
223 Expression::ParenthesizedExpression(paren) => object_expression(&paren.expression),
224 Expression::TSSatisfiesExpression(ts_sat) => object_expression(&ts_sat.expression),
225 Expression::TSAsExpression(ts_as) => object_expression(&ts_as.expression),
226 _ => None,
227 }
228}
229
230pub(crate) fn array_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
232 match expr {
233 Expression::ArrayExpression(arr) => Some(arr),
234 Expression::ParenthesizedExpression(paren) => array_expression(&paren.expression),
235 Expression::TSSatisfiesExpression(ts_sat) => array_expression(&ts_sat.expression),
236 Expression::TSAsExpression(ts_as) => array_expression(&ts_as.expression),
237 _ => None,
238 }
239}
240
241pub(crate) fn path_from_config_string(raw: &str) -> PathBuf {
244 PathBuf::from(raw.replace('\\', "/"))
245}
246
247pub(crate) fn path_to_config_string(path: &Path) -> String {
249 path.to_string_lossy().replace('\\', "/")
250}
251
252pub(crate) fn expression_to_path(expr: &Expression<'_>) -> Option<PathBuf> {
254 expression_to_path_string(expr).map(|path| path_from_config_string(&path))
255}
256
257pub(crate) fn expression_to_path_values(expr: &Expression<'_>) -> Vec<PathBuf> {
259 match expr {
260 Expression::ArrayExpression(arr) => arr
261 .elements
262 .iter()
263 .filter_map(|element| element.as_expression().and_then(expression_to_path))
264 .collect(),
265 _ => expression_to_path(expr).into_iter().collect(),
266 }
267}
268
269pub(crate) fn is_disabled_expression(expr: &Expression<'_>) -> bool {
271 matches!(expr, Expression::BooleanLiteral(boolean) if !boolean.value)
272 || matches!(expr, Expression::NullLiteral(_))
273}
274
275#[must_use]
277pub(crate) fn extract_config_truthy_bool_or_object(
278 source: &str,
279 path: &Path,
280 prop_path: &[&str],
281) -> bool {
282 extract_from_source(source, path, |program| {
283 let obj = find_config_object(program)?;
284 let expr = get_nested_expression(obj, prop_path)?;
285 Some(is_truthy_bool_or_object(expr))
286 })
287 .unwrap_or(false)
288}
289
290fn is_truthy_bool_or_object(expr: &Expression<'_>) -> bool {
291 match expr {
292 Expression::BooleanLiteral(boolean) => boolean.value,
293 Expression::ObjectExpression(_) => true,
294 Expression::ParenthesizedExpression(paren) => is_truthy_bool_or_object(&paren.expression),
295 Expression::TSSatisfiesExpression(ts_sat) => is_truthy_bool_or_object(&ts_sat.expression),
296 Expression::TSAsExpression(ts_as) => is_truthy_bool_or_object(&ts_as.expression),
297 _ => false,
298 }
299}
300
301#[must_use]
303pub(crate) fn extract_config_object_keys(
304 source: &str,
305 path: &Path,
306 prop_path: &[&str],
307) -> Vec<String> {
308 extract_from_source(source, path, |program| {
309 let obj = find_config_object(program)?;
310 get_nested_object_keys(obj, prop_path)
311 })
312 .unwrap_or_default()
313}
314
315#[must_use]
318pub(crate) fn extract_config_string_or_array(
319 source: &str,
320 path: &Path,
321 prop_path: &[&str],
322) -> Vec<String> {
323 extract_from_source(source, path, |program| {
324 let obj = find_config_object(program)?;
325 get_nested_string_or_array(obj, prop_path, PathScope::Program(program))
326 })
327 .unwrap_or_default()
328}
329
330#[must_use]
337pub(crate) fn extract_config_array_or_object_string_or_array(
338 source: &str,
339 path: &Path,
340 prop_path: &[&str],
341) -> Vec<String> {
342 extract_from_source(source, path, |program| {
343 let Some(arr) = find_default_export_array(program) else {
344 let obj = find_config_object(program)?;
345 return get_nested_string_or_array(obj, prop_path, PathScope::Program(program));
346 };
347 let values = arr
348 .elements
349 .iter()
350 .filter_map(|element| element.as_expression().and_then(object_expression))
351 .filter_map(|element_obj| {
352 get_nested_string_or_array(element_obj, prop_path, PathScope::Program(program))
353 })
354 .flatten()
355 .collect();
356 Some(values)
357 })
358 .unwrap_or_default()
359}
360
361#[must_use]
363pub(crate) fn extract_config_path(
364 source: &str,
365 path: &Path,
366 prop_path: &[&str],
367) -> Option<PathBuf> {
368 extract_from_source(source, path, |program| {
369 let obj = find_config_object(program)?;
370 let expr = get_nested_expression(obj, prop_path)?;
371 expression_to_path(expr)
372 })
373}
374
375#[must_use]
377pub(crate) fn extract_config_array_nested_string_or_array(
378 source: &str,
379 path: &Path,
380 array_path: &[&str],
381 inner_path: &[&str],
382) -> Vec<String> {
383 extract_from_source(source, path, |program| {
384 let obj = find_config_object(program)?;
385 let array_expr = get_nested_expression(obj, array_path)?;
386 let Expression::ArrayExpression(arr) = array_expr else {
387 return None;
388 };
389 let mut results = Vec::new();
390 for element in &arr.elements {
391 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
392 && let Some(values) =
393 get_nested_string_or_array(element_obj, inner_path, PathScope::Program(program))
394 {
395 results.extend(values);
396 }
397 }
398 if results.is_empty() {
399 None
400 } else {
401 Some(results)
402 }
403 })
404 .unwrap_or_default()
405}
406
407#[must_use]
414pub(crate) fn extract_config_array_object_fields(
415 source: &str,
416 path: &Path,
417 array_path: &[&str],
418 keys: &[&str],
419) -> Vec<Vec<Vec<String>>> {
420 extract_from_source(source, path, |program| {
421 let obj = find_config_object(program)?;
422 let Expression::ArrayExpression(arr) = get_nested_expression(obj, array_path)? else {
423 return None;
424 };
425 let elements = arr
426 .elements
427 .iter()
428 .filter_map(|element| match element.as_expression() {
429 Some(Expression::ObjectExpression(element_obj)) => Some(
430 keys.iter()
431 .map(|key| {
432 get_nested_string_or_array(
433 element_obj,
434 &[key],
435 PathScope::Program(program),
436 )
437 .unwrap_or_default()
438 })
439 .collect(),
440 ),
441 _ => None,
442 })
443 .collect();
444 Some(elements)
445 })
446 .unwrap_or_default()
447}
448
449#[must_use]
451pub(crate) fn extract_config_object_nested_string_or_array(
452 source: &str,
453 path: &Path,
454 object_path: &[&str],
455 inner_path: &[&str],
456) -> Vec<String> {
457 extract_config_object_nested(source, path, object_path, |value_obj| {
458 get_nested_string_or_array(value_obj, inner_path, PathScope::Bare)
459 })
460}
461
462#[must_use]
464pub(crate) fn extract_config_object_nested_strings(
465 source: &str,
466 path: &Path,
467 object_path: &[&str],
468 inner_path: &[&str],
469) -> Vec<String> {
470 extract_config_object_nested(source, path, object_path, |value_obj| {
471 get_nested_string_from_object(value_obj, inner_path).map(|s| vec![s])
472 })
473}
474
475fn extract_config_object_nested(
477 source: &str,
478 path: &Path,
479 object_path: &[&str],
480 extract_fn: impl Fn(&ObjectExpression<'_>) -> Option<Vec<String>>,
481) -> Vec<String> {
482 extract_from_source(source, path, |program| {
483 let obj = find_config_object(program)?;
484 let obj_expr = get_nested_expression(obj, object_path)?;
485 let Expression::ObjectExpression(target_obj) = obj_expr else {
486 return None;
487 };
488 let mut results = Vec::new();
489 for prop in &target_obj.properties {
490 if let ObjectPropertyKind::ObjectProperty(p) = prop
491 && let Expression::ObjectExpression(value_obj) = &p.value
492 && let Some(values) = extract_fn(value_obj)
493 {
494 results.extend(values);
495 }
496 }
497 if results.is_empty() {
498 None
499 } else {
500 Some(results)
501 }
502 })
503 .unwrap_or_default()
504}
505
506#[must_use]
508pub(crate) fn extract_config_require_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
509 extract_from_source(source, path, |program| {
510 let obj = find_config_object(program)?;
511 let prop = find_property(obj, key)?;
512 Some(collect_require_sources(&prop.value))
513 })
514 .unwrap_or_default()
515}
516
517#[must_use]
519pub(crate) fn extract_config_aliases(
520 source: &str,
521 path: &Path,
522 prop_path: &[&str],
523) -> Vec<(String, String)> {
524 extract_config_aliases_kinded(source, path, prop_path)
525 .into_iter()
526 .map(|(find, replacement, _is_bare)| (find, replacement))
527 .collect()
528}
529
530#[must_use]
532pub(crate) fn extract_config_path_aliases(
533 source: &str,
534 path: &Path,
535 prop_path: &[&str],
536) -> Vec<(String, PathBuf)> {
537 extract_config_aliases_kinded(source, path, prop_path)
538 .into_iter()
539 .map(|(find, replacement, _is_bare)| (find, path_from_config_string(&replacement)))
540 .collect()
541}
542
543#[must_use]
545pub(crate) fn extract_config_aliases_kinded(
546 source: &str,
547 path: &Path,
548 prop_path: &[&str],
549) -> Vec<(String, String, bool)> {
550 extract_from_source(source, path, |program| {
551 let obj = find_config_object(program)?;
552 let expr = get_nested_expression(obj, prop_path)?;
553 let mut visited = FxHashSet::default();
554 let aliases = resolve_alias_pairs_kinded(program, path, expr, &mut visited, 0);
555 (!aliases.is_empty()).then_some(aliases)
556 })
557 .unwrap_or_default()
558}
559
560#[must_use]
564pub(crate) fn extract_config_array_nested_aliases_kinded(
565 source: &str,
566 path: &Path,
567 array_path: &[&str],
568 alias_path: &[&str],
569) -> Vec<(String, String, bool)> {
570 extract_from_source(source, path, |program| {
571 let obj = find_config_object(program)?;
572 let array_expr = get_nested_expression(obj, array_path)?;
573 let Expression::ArrayExpression(arr) = array_expr else {
574 return None;
575 };
576 let mut results = Vec::new();
577 for element in &arr.elements {
578 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
579 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
580 {
581 results.extend(expression_to_alias_pairs_kinded(alias_expr));
582 }
583 }
584 (!results.is_empty()).then_some(results)
585 })
586 .unwrap_or_default()
587}
588
589#[must_use]
591pub(crate) fn extract_default_export_array_aliases_kinded(
592 source: &str,
593 path: &Path,
594 alias_path: &[&str],
595) -> Vec<(String, String, bool)> {
596 extract_from_source(source, path, |program| {
597 let arr = find_default_export_array(program)?;
598 let mut results = Vec::new();
599 for element in &arr.elements {
600 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
601 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
602 {
603 results.extend(expression_to_alias_pairs_kinded(alias_expr));
604 }
605 }
606 (!results.is_empty()).then_some(results)
607 })
608 .unwrap_or_default()
609}
610
611#[must_use]
613pub(crate) fn config_default_export_unreachable(source: &str, path: &Path) -> bool {
614 extract_from_source(source, path, |program| {
615 let reachable =
616 find_config_object(program).is_some() || find_default_export_array(program).is_some();
617 Some(reachable)
618 })
619 .is_some_and(|reachable| !reachable)
620}
621
622#[must_use]
628pub(crate) fn extract_config_array_object_strings(
629 source: &str,
630 path: &Path,
631 array_path: &[&str],
632 key: &str,
633) -> Vec<String> {
634 extract_from_source(source, path, |program| {
635 let obj = find_config_object(program)?;
636 let array_expr = get_nested_expression(obj, array_path)?;
637 let Expression::ArrayExpression(arr) = array_expr else {
638 return None;
639 };
640
641 let mut results = Vec::new();
642 for element in &arr.elements {
643 let Some(expr) = element.as_expression() else {
644 continue;
645 };
646 match expr {
647 Expression::ObjectExpression(item) => {
648 if let Some(prop) = find_property(item, key)
649 && let Some(value) = expression_to_path_string(&prop.value)
650 {
651 results.push(value);
652 }
653 }
654 _ => {
655 if let Some(value) = expression_to_path_string(expr) {
656 results.push(value);
657 }
658 }
659 }
660 }
661
662 (!results.is_empty()).then_some(results)
663 })
664 .unwrap_or_default()
665}
666
667#[must_use]
672pub(crate) fn extract_config_static_dir_entries(
673 source: &str,
674 path: &Path,
675 array_path: &[&str],
676) -> Vec<(String, Option<String>)> {
677 extract_from_source(source, path, |program| {
678 let obj = find_config_object(program)?;
679 let array_expr = get_nested_expression(obj, array_path)?;
680 let Expression::ArrayExpression(arr) = array_expr else {
681 return None;
682 };
683
684 let mut results = Vec::new();
685 for element in &arr.elements {
686 let Some(expr) = element.as_expression() else {
687 continue;
688 };
689 match expr {
690 Expression::ObjectExpression(item) => {
691 if let Some(from) = property_string(item, "from") {
692 let to = property_string(item, "to");
693 results.push((from, to));
694 }
695 }
696 _ => {
697 if let Some(from) = expression_to_path_string(expr) {
698 results.push((from, None));
699 }
700 }
701 }
702 }
703
704 (!results.is_empty()).then_some(results)
705 })
706 .unwrap_or_default()
707}
708
709#[must_use]
711pub(crate) fn extract_config_array_object_command_pairs(
712 source: &str,
713 path: &Path,
714 array_path: &[&str],
715 primary_key: &str,
716 secondary_key: &str,
717) -> Vec<(String, Option<String>)> {
718 extract_from_source(source, path, |program| {
719 let obj = find_config_object(program)?;
720 let array_expr = get_nested_expression(obj, array_path)?;
721 let Expression::ArrayExpression(arr) = array_expr else {
722 return None;
723 };
724
725 let mut results = Vec::new();
726 for element in &arr.elements {
727 let Some(Expression::ObjectExpression(item)) = element.as_expression() else {
728 continue;
729 };
730 let Some(primary) = find_property(item, primary_key)
731 .and_then(|prop| expression_to_command(&prop.value))
732 else {
733 continue;
734 };
735 let secondary = find_property(item, secondary_key)
736 .and_then(|prop| expression_to_path_string(&prop.value));
737 results.push((primary, secondary));
738 }
739
740 (!results.is_empty()).then_some(results)
741 })
742 .unwrap_or_default()
743}
744
745#[must_use]
791pub(crate) fn extract_lazy_imports_in_array(
792 source: &str,
793 path: &Path,
794 prop_path: &[&str],
795) -> Vec<String> {
796 extract_from_source(source, path, |program| {
797 let obj = find_config_object(program)?;
798 let array_expr = get_nested_expression(obj, prop_path)?;
799 let Expression::ArrayExpression(arr) = array_expr else {
800 return None;
801 };
802 let mut specs = Vec::new();
803 for element in &arr.elements {
804 let Some(expr) = element.as_expression() else {
805 continue;
806 };
807 if let Some(spec) = lazy_import_specifier(expr) {
808 specs.push(spec);
809 }
810 }
811 (!specs.is_empty()).then_some(specs)
812 })
813 .unwrap_or_default()
814}
815
816fn lazy_import_specifier(expr: &Expression<'_>) -> Option<String> {
829 let callable = match expr {
830 Expression::ObjectExpression(obj) => &find_property(obj, "file")?.value,
831 _ => expr,
832 };
833 let import_expr = extract_import_from_callable(callable)?;
834 expression_to_string(&import_expr.source)
835}
836
837#[must_use]
842pub(crate) fn extract_config_array_tuple_heads(
843 source: &str,
844 path: &Path,
845 array_path: &[&str],
846) -> Vec<String> {
847 extract_from_source(source, path, |program| {
848 let obj = find_config_object(program)?;
849 let Expression::ArrayExpression(entries) = get_nested_expression(obj, array_path)? else {
850 return None;
851 };
852 let heads = entries
853 .elements
854 .iter()
855 .filter_map(|entry| match entry.as_expression() {
856 Some(Expression::ArrayExpression(tuple)) => tuple
857 .elements
858 .first()
859 .and_then(ArrayExpressionElement::as_expression)
860 .and_then(expression_to_string),
861 _ => None,
862 })
863 .collect();
864 Some(heads)
865 })
866 .unwrap_or_default()
867}
868
869#[must_use]
876fn extract_config_plugin_option_string(
877 source: &str,
878 path: &Path,
879 plugins_path: &[&str],
880 plugin_name: &str,
881 option_key: &str,
882) -> Option<String> {
883 extract_from_source(source, path, |program| {
884 let obj = find_config_object(program)?;
885 let plugins_expr = get_nested_expression(obj, plugins_path)?;
886 let Expression::ArrayExpression(plugins) = plugins_expr else {
887 return None;
888 };
889
890 for entry in &plugins.elements {
891 let Some(Expression::ArrayExpression(tuple)) = entry.as_expression() else {
892 continue;
893 };
894 let Some(plugin_expr) = tuple
895 .elements
896 .first()
897 .and_then(ArrayExpressionElement::as_expression)
898 else {
899 continue;
900 };
901 if expression_to_string(plugin_expr).as_deref() != Some(plugin_name) {
902 continue;
903 }
904
905 let Some(options_expr) = tuple
906 .elements
907 .get(1)
908 .and_then(ArrayExpressionElement::as_expression)
909 else {
910 continue;
911 };
912 let Expression::ObjectExpression(options_obj) = options_expr else {
913 continue;
914 };
915 let option = find_property(options_obj, option_key)?;
916 return expression_to_path_string(&option.value);
917 }
918
919 None
920 })
921}
922
923#[must_use]
925pub(crate) fn extract_config_plugin_option_string_from_paths(
926 source: &str,
927 path: &Path,
928 plugin_paths: &[&[&str]],
929 plugin_name: &str,
930 option_key: &str,
931) -> Option<String> {
932 plugin_paths.iter().find_map(|plugins_path| {
933 extract_config_plugin_option_string(source, path, plugins_path, plugin_name, option_key)
934 })
935}
936
937#[must_use]
940pub(crate) fn extract_vite_react_babel_dependencies(source: &str, path: &Path) -> Vec<String> {
941 extract_from_source(source, path, |program| {
942 let react_plugin_imports = collect_vite_react_plugin_imports(program);
943 if react_plugin_imports.is_empty() {
944 return None;
945 }
946
947 let obj = find_config_object(program)?;
948 let plugins = get_nested_expression(obj, &["plugins"])?;
949 let Expression::ArrayExpression(plugin_array) = plugins else {
950 return None;
951 };
952
953 let mut deps = Vec::new();
954 for element in &plugin_array.elements {
955 let Some(Expression::CallExpression(call)) = element.as_expression() else {
956 continue;
957 };
958 if !is_vite_react_plugin_call(call, &react_plugin_imports) {
959 continue;
960 }
961 let Some(Expression::ObjectExpression(options)) =
962 call.arguments.first().and_then(Argument::as_expression)
963 else {
964 continue;
965 };
966 collect_vite_react_babel_dependencies(options, &mut deps);
967 }
968
969 (!deps.is_empty()).then_some(deps)
970 })
971 .unwrap_or_default()
972}
973
974#[derive(Debug, Clone, Copy, PartialEq, Eq)]
976enum LeadingSlash {
977 RootRelative,
979 Filesystem,
981}
982
983#[must_use]
992pub(crate) fn normalize_config_path_buf(
993 raw: impl AsRef<Path>,
994 config_path: &Path,
995 root: &Path,
996) -> Option<PathBuf> {
997 normalize_path_value(raw.as_ref(), config_path, root, LeadingSlash::RootRelative)
998}
999
1000#[must_use]
1004pub(crate) fn normalize_filesystem_config_path_buf(
1005 raw: impl AsRef<Path>,
1006 config_path: &Path,
1007 root: &Path,
1008) -> Option<PathBuf> {
1009 normalize_path_value(raw.as_ref(), config_path, root, LeadingSlash::Filesystem)
1010}
1011
1012fn normalize_path_value(
1013 raw: &Path,
1014 config_path: &Path,
1015 root: &Path,
1016 leading_slash: LeadingSlash,
1017) -> Option<PathBuf> {
1018 if raw.as_os_str().is_empty() {
1019 return None;
1020 }
1021
1022 let raw_string = path_to_config_string(raw);
1023 let raw_path = Path::new(&raw_string);
1024 let root_relative_slash =
1025 leading_slash == LeadingSlash::RootRelative && raw_string.starts_with('/');
1026 let absolute = raw_path
1027 .is_absolute()
1028 .then(|| lexical_normalize(raw_path))
1029 .filter(|absolute| absolute.starts_with(root))
1030 .filter(|absolute| !(root_relative_slash && (absolute == root || root_in_root(root))));
1031 let candidate = if let Some(absolute) = absolute {
1032 absolute
1033 } else if let Some(stripped) = raw_string.strip_prefix('/') {
1034 match leading_slash {
1035 LeadingSlash::RootRelative => lexical_normalize(&root.join(stripped)),
1036 LeadingSlash::Filesystem => return None,
1037 }
1038 } else if raw_path.is_absolute() {
1039 return None;
1040 } else {
1041 let base = config_path.parent().unwrap_or(root);
1042 lexical_normalize(&base.join(raw_path))
1043 };
1044
1045 let relative = candidate.strip_prefix(root).ok()?;
1046 (!relative.as_os_str().is_empty()).then(|| relative.to_path_buf())
1047}
1048
1049fn root_in_root(root: &Path) -> bool {
1053 let root_string = path_to_config_string(root);
1054 let Some(stripped) = root_string.strip_prefix('/') else {
1055 return false;
1056 };
1057 !stripped.is_empty() && root.join(stripped).is_dir()
1058}
1059
1060#[must_use]
1063pub(crate) fn normalize_filesystem_config_path(
1064 raw: impl AsRef<Path>,
1065 config_path: &Path,
1066 root: &Path,
1067) -> Option<String> {
1068 normalize_filesystem_config_path_buf(raw, config_path, root)
1069 .map(|path| path_to_config_string(&path))
1070}
1071
1072#[must_use]
1074pub(crate) fn normalize_config_path(
1075 raw: impl AsRef<Path>,
1076 config_path: &Path,
1077 root: &Path,
1078) -> Option<String> {
1079 normalize_config_path_buf(raw, config_path, root).map(|path| path_to_config_string(&path))
1080}
1081
1082pub(crate) fn parent_relative_config_path(
1092 target: &str,
1093 config_path: &Path,
1094 root: &Path,
1095) -> Option<String> {
1096 if !target.starts_with("../") {
1097 return None;
1098 }
1099 let directory = config_path.parent().unwrap_or(root);
1100 let candidate = lexical_normalize(&directory.join(target));
1101 let root = lexical_normalize(root);
1102 let mut ancestor = root.as_path();
1103 let mut climbs = 0;
1104 while !candidate.starts_with(ancestor) {
1105 ancestor = ancestor.parent()?;
1106 climbs += 1;
1107 }
1108 let rest = candidate.strip_prefix(ancestor).ok()?;
1109 (climbs > 0 && !rest.as_os_str().is_empty())
1110 .then(|| format!("{}{}", "../".repeat(climbs), path_to_config_string(rest)))
1111}
1112
1113pub(crate) fn extract_from_source<T>(
1120 source: &str,
1121 path: &Path,
1122 extractor: impl FnOnce(&Program) -> Option<T>,
1123) -> Option<T> {
1124 let source_type = SourceType::from_path(path).unwrap_or_default();
1125 let alloc = Allocator::default();
1126
1127 let is_json = path
1128 .extension()
1129 .is_some_and(|ext| ext == "json" || ext == "jsonc");
1130 if is_json {
1131 let wrapped = format!("({source})");
1132 let parsed = Parser::new(&alloc, &wrapped, SourceType::mjs()).parse();
1133 return extractor(&parsed.program);
1134 }
1135
1136 let parsed = Parser::new(&alloc, source, source_type).parse();
1137 extractor(&parsed.program)
1138}
1139
1140#[derive(Default)]
1141struct ViteReactPluginImports {
1142 callables: Vec<String>,
1143 namespaces: Vec<String>,
1144}
1145
1146impl ViteReactPluginImports {
1147 fn is_empty(&self) -> bool {
1148 self.callables.is_empty() && self.namespaces.is_empty()
1149 }
1150}
1151
1152fn collect_vite_react_plugin_imports(program: &Program<'_>) -> ViteReactPluginImports {
1153 let mut imports = ViteReactPluginImports::default();
1154
1155 for stmt in &program.body {
1156 let Statement::ImportDeclaration(decl) = stmt else {
1157 continue;
1158 };
1159 if decl.source.value != "@vitejs/plugin-react" {
1160 continue;
1161 }
1162 let Some(specifiers) = &decl.specifiers else {
1163 continue;
1164 };
1165 for specifier in specifiers {
1166 match specifier {
1167 ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
1168 push_unique_string(&mut imports.callables, specifier.local.name.to_string());
1169 }
1170 ImportDeclarationSpecifier::ImportSpecifier(specifier)
1171 if specifier.imported.name().as_ref() == "default" =>
1172 {
1173 push_unique_string(&mut imports.callables, specifier.local.name.to_string());
1174 }
1175 ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
1176 push_unique_string(&mut imports.namespaces, specifier.local.name.to_string());
1177 }
1178 ImportDeclarationSpecifier::ImportSpecifier(_) => {}
1179 }
1180 }
1181 }
1182
1183 imports
1184}
1185
1186fn is_vite_react_plugin_call(call: &CallExpression<'_>, imports: &ViteReactPluginImports) -> bool {
1187 match &call.callee {
1188 Expression::Identifier(identifier) => imports
1189 .callables
1190 .iter()
1191 .any(|name| name == identifier.name.as_str()),
1192 Expression::StaticMemberExpression(member) if matches!(&member.object, Expression::Identifier(object) if imports.namespaces.iter().any(|name| name == object.name.as_str())) => {
1193 member.property.name == "default"
1194 }
1195 _ => false,
1196 }
1197}
1198
1199fn collect_vite_react_babel_dependencies(options: &ObjectExpression<'_>, deps: &mut Vec<String>) {
1200 let Some(babel) = property_object(options, "babel") else {
1201 return;
1202 };
1203 for key in ["plugins", "presets"] {
1204 let Some(prop) = find_property(babel, key) else {
1205 continue;
1206 };
1207 for raw in collect_shallow_string_values(&prop.value) {
1208 if let Some(dep) = vite_react_babel_dependency_name(&raw) {
1209 push_unique_string(deps, dep);
1210 }
1211 }
1212 }
1213}
1214
1215fn vite_react_babel_dependency_name(raw: &str) -> Option<String> {
1216 let raw = raw.trim();
1217 let specifier = raw.strip_prefix("module:").unwrap_or(raw).trim();
1218 if specifier.is_empty()
1219 || specifier.starts_with('.')
1220 || specifier.starts_with('/')
1221 || specifier.contains(':')
1222 || specifier.contains('\\')
1223 {
1224 return None;
1225 }
1226 Some(crate::resolve::extract_package_name(specifier))
1227}
1228
1229fn push_unique_string(items: &mut Vec<String>, value: String) {
1230 if !items.contains(&value) {
1231 items.push(value);
1232 }
1233}
1234
1235pub(crate) fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
1247 for stmt in &program.body {
1248 match stmt {
1249 Statement::ExportDefaultDeclaration(decl) => {
1250 let expr: Option<&Expression> = match &decl.declaration {
1251 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
1252 return Some(obj);
1253 }
1254 ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
1255 return extract_object_from_function(func);
1256 }
1257 _ => decl.declaration.as_expression(),
1258 };
1259 if let Some(expr) = expr {
1260 if let Some(obj) =
1261 resolve_call_config_object(program, expr, MAX_CONFIG_WRAPPER_DEPTH)
1262 {
1263 return Some(obj);
1264 }
1265 if let Some(obj) = extract_object_from_expression(expr) {
1266 return Some(obj);
1267 }
1268 if let Some(name) = unwrap_to_identifier_name(expr) {
1269 return find_variable_init_object(program, name);
1270 }
1271 if let Some(obj) = resolve_wrapped_config_object(program, expr) {
1272 return Some(obj);
1273 }
1274 }
1275 }
1276 Statement::ExpressionStatement(expr_stmt) => {
1277 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
1278 && is_module_exports_target(&assign.left)
1279 {
1280 if let Some(obj) =
1281 resolve_call_config_object(program, &assign.right, MAX_CONFIG_WRAPPER_DEPTH)
1282 {
1283 return Some(obj);
1284 }
1285 if let Some(obj) = extract_object_from_expression(&assign.right) {
1286 return Some(obj);
1287 }
1288 if let Some(name) = unwrap_to_identifier_name(&assign.right) {
1289 return find_variable_init_object(program, name);
1290 }
1291 return resolve_wrapped_config_object(program, &assign.right);
1292 }
1293 }
1294 _ => {}
1295 }
1296 }
1297
1298 if program.body.len() == 1
1299 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
1300 {
1301 match &expr_stmt.expression {
1302 Expression::ObjectExpression(obj) => return Some(obj),
1303 Expression::ParenthesizedExpression(paren) => {
1304 if let Expression::ObjectExpression(obj) = &paren.expression {
1305 return Some(obj);
1306 }
1307 }
1308 _ => {}
1309 }
1310 }
1311
1312 None
1313}
1314
1315pub(crate) fn extract_object_from_expression<'a>(
1317 expr: &'a Expression<'a>,
1318) -> Option<&'a ObjectExpression<'a>> {
1319 match expr {
1320 Expression::ObjectExpression(obj) => Some(obj),
1321 Expression::CallExpression(call) => {
1322 for arg in &call.arguments {
1323 match arg {
1324 Argument::ObjectExpression(obj) => return Some(obj),
1325 Argument::ArrowFunctionExpression(arrow) => {
1332 if let Some(obj) = extract_object_from_arrow_function(arrow) {
1333 return Some(obj);
1334 }
1335 }
1336 Argument::FunctionExpression(func) => {
1337 if let Some(obj) = extract_object_from_function(func) {
1338 return Some(obj);
1339 }
1340 }
1341 _ => {}
1342 }
1343 }
1344 None
1345 }
1346 Expression::ParenthesizedExpression(paren) => {
1347 extract_object_from_expression(&paren.expression)
1348 }
1349 Expression::TSSatisfiesExpression(ts_sat) => {
1350 extract_object_from_expression(&ts_sat.expression)
1351 }
1352 Expression::TSAsExpression(ts_as) => extract_object_from_expression(&ts_as.expression),
1353 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
1354 Expression::FunctionExpression(func) => extract_object_from_function(func),
1355 _ => None,
1356 }
1357}
1358
1359fn extract_object_from_arrow_function<'a>(
1360 arrow: &'a ArrowFunctionExpression<'a>,
1361) -> Option<&'a ObjectExpression<'a>> {
1362 match &arrow.body {
1363 ArrowFunctionBody::FunctionBody(body) => extract_object_from_function_body(body),
1364 body => body
1365 .as_expression()
1366 .and_then(extract_object_from_expression),
1367 }
1368}
1369
1370fn extract_object_from_function<'a>(func: &'a Function<'a>) -> Option<&'a ObjectExpression<'a>> {
1371 func.body
1372 .as_ref()
1373 .and_then(|body| extract_object_from_function_body(body))
1374}
1375
1376fn extract_object_from_function_body<'a>(
1385 body: &'a FunctionBody<'a>,
1386) -> Option<&'a ObjectExpression<'a>> {
1387 find_top_level_returned_object(&body.statements)
1388 .or_else(|| find_returned_object(&body.statements, MAX_CONFIG_BRANCH_DEPTH))
1389}
1390
1391const MAX_CONFIG_BRANCH_DEPTH: u8 = 4;
1393
1394fn find_top_level_returned_object<'a>(
1396 statements: &'a [Statement<'a>],
1397) -> Option<&'a ObjectExpression<'a>> {
1398 statements.iter().find_map(|stmt| match stmt {
1399 Statement::ReturnStatement(ret) => ret
1400 .argument
1401 .as_ref()
1402 .and_then(|argument| extract_object_from_expression(argument)),
1403 _ => None,
1404 })
1405}
1406
1407fn find_returned_object<'a>(
1416 statements: &'a [Statement<'a>],
1417 depth: u8,
1418) -> Option<&'a ObjectExpression<'a>> {
1419 if depth == 0 {
1420 return None;
1421 }
1422 for stmt in statements {
1423 let found = match stmt {
1424 Statement::ReturnStatement(ret) => ret
1425 .argument
1426 .as_ref()
1427 .and_then(|argument| extract_object_from_expression(argument)),
1428 Statement::BlockStatement(block) => find_returned_object(&block.body, depth - 1),
1429 Statement::IfStatement(if_stmt) => {
1430 find_returned_object_in_statement(&if_stmt.consequent, depth - 1).or_else(|| {
1431 if_stmt
1432 .alternate
1433 .as_ref()
1434 .and_then(|alt| find_returned_object_in_statement(alt, depth - 1))
1435 })
1436 }
1437 Statement::TryStatement(try_stmt) => {
1438 find_returned_object(&try_stmt.block.body, depth - 1)
1439 .or_else(|| {
1440 try_stmt
1441 .handler
1442 .as_ref()
1443 .and_then(|handler| find_returned_object(&handler.body.body, depth - 1))
1444 })
1445 .or_else(|| {
1446 try_stmt
1447 .finalizer
1448 .as_ref()
1449 .and_then(|finalizer| find_returned_object(&finalizer.body, depth - 1))
1450 })
1451 }
1452 Statement::SwitchStatement(switch) => switch
1453 .cases
1454 .iter()
1455 .find_map(|case| find_returned_object(&case.consequent, depth - 1)),
1456 _ => None,
1457 };
1458 if found.is_some() {
1459 return found;
1460 }
1461 }
1462 None
1463}
1464
1465fn find_returned_object_in_statement<'a>(
1466 stmt: &'a Statement<'a>,
1467 depth: u8,
1468) -> Option<&'a ObjectExpression<'a>> {
1469 match stmt {
1470 Statement::BlockStatement(block) => find_returned_object(&block.body, depth),
1471 other => find_returned_object(std::slice::from_ref(other), depth),
1472 }
1473}
1474
1475fn is_module_exports_target(target: &AssignmentTarget) -> bool {
1477 if let AssignmentTarget::StaticMemberExpression(member) = target
1478 && let Expression::Identifier(obj) = &member.object
1479 {
1480 return obj.name == "module" && member.property.name == "exports";
1481 }
1482 false
1483}
1484
1485fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
1489 match expr {
1490 Expression::Identifier(id) => Some(&id.name),
1491 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
1492 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
1493 _ => None,
1494 }
1495}
1496
1497pub(crate) fn find_variable_init_object<'a>(
1502 program: &'a Program,
1503 name: &str,
1504) -> Option<&'a ObjectExpression<'a>> {
1505 for decl in top_level_variable_declarations(program) {
1506 for declarator in &decl.declarations {
1507 if let BindingPattern::BindingIdentifier(id) = &declarator.id
1508 && id.name == name
1509 && let Some(init) = &declarator.init
1510 {
1511 return extract_object_from_expression(init);
1512 }
1513 }
1514 }
1515 None
1516}
1517
1518fn find_variable_init<'a>(program: &'a Program<'a>, name: &str) -> Option<&'a Expression<'a>> {
1520 top_level_variable_declarations(program)
1521 .flat_map(|decl| &decl.declarations)
1522 .find(|declarator| {
1523 matches!(&declarator.id, BindingPattern::BindingIdentifier(id) if id.name == name)
1524 })
1525 .and_then(|declarator| declarator.init.as_ref())
1526}
1527
1528fn exported_config_expression<'a>(program: &'a Program<'a>) -> Option<&'a Expression<'a>> {
1531 program.body.iter().find_map(|stmt| match stmt {
1532 Statement::ExportDefaultDeclaration(decl) => decl.declaration.as_expression(),
1533 Statement::ExpressionStatement(expr_stmt) => match &expr_stmt.expression {
1534 Expression::AssignmentExpression(assign) if is_module_exports_target(&assign.left) => {
1535 Some(&assign.right)
1536 }
1537 _ => None,
1538 },
1539 _ => None,
1540 })
1541}
1542
1543fn config_call<'a>(
1546 program: &'a Program<'a>,
1547 expr: &'a Expression<'a>,
1548 follow_binding: bool,
1549) -> Option<&'a CallExpression<'a>> {
1550 match expr {
1551 Expression::CallExpression(call) => Some(call),
1552 Expression::ParenthesizedExpression(paren) => {
1553 config_call(program, &paren.expression, follow_binding)
1554 }
1555 Expression::TSSatisfiesExpression(ts_sat) => {
1556 config_call(program, &ts_sat.expression, follow_binding)
1557 }
1558 Expression::TSAsExpression(ts_as) => {
1559 config_call(program, &ts_as.expression, follow_binding)
1560 }
1561 Expression::Identifier(id) if follow_binding => {
1562 config_call(program, find_variable_init(program, &id.name)?, false)
1563 }
1564 _ => None,
1565 }
1566}
1567
1568pub(crate) fn find_config_merge_arguments<'a>(
1577 program: &'a Program<'a>,
1578) -> Option<Vec<Option<&'a ObjectExpression<'a>>>> {
1579 let call = config_call(program, exported_config_expression(program)?, true)?;
1580 if call.arguments.len() < 2 {
1581 return None;
1582 }
1583 let resolve = |arg: &'a Argument<'a>| {
1584 let expr = arg.as_expression()?;
1585 if config_call(program, expr, true).is_some_and(|nested| nested.arguments.len() > 1) {
1586 return None;
1587 }
1588 resolve_config_argument(program, expr, MAX_CONFIG_WRAPPER_DEPTH)
1589 };
1590 Some(call.arguments.iter().map(resolve).collect())
1591}
1592
1593fn top_level_variable_declarations<'a>(
1596 program: &'a Program<'a>,
1597) -> impl Iterator<Item = &'a VariableDeclaration<'a>> {
1598 program.body.iter().filter_map(|stmt| match stmt {
1599 Statement::VariableDeclaration(decl) => Some(&**decl),
1600 Statement::ExportDeclaration(export) => match &export.declaration {
1601 Declaration::VariableDeclaration(decl) => Some(&**decl),
1602 _ => None,
1603 },
1604 _ => None,
1605 })
1606}
1607
1608pub(crate) fn find_stable_binding_init<'a>(
1620 program: &'a Program<'a>,
1621 name: &str,
1622) -> Option<&'a Expression<'a>> {
1623 if !holds_one_stable_object(program, name) {
1624 return None;
1625 }
1626 find_variable_init_expression(program, name)
1627}
1628
1629fn holds_one_stable_object(program: &Program<'_>, name: &str) -> bool {
1632 if !declares_top_level_const_or_let(program, name) {
1633 return false;
1634 }
1635 let mut usage = NameUsage::new(name);
1636 usage.visit_program(program);
1637 usage.bindings == 1 && usage.writes == 0
1638}
1639
1640fn declares_top_level_const_or_let(program: &Program<'_>, name: &str) -> bool {
1643 top_level_variable_declarations(program).any(|decl| {
1644 matches!(
1645 decl.kind,
1646 VariableDeclarationKind::Const | VariableDeclarationKind::Let
1647 ) && decl.declarations.iter().any(|declarator| {
1648 matches!(&declarator.id, BindingPattern::BindingIdentifier(id) if id.name == name)
1649 })
1650 })
1651}
1652
1653struct NameUsage<'n> {
1656 name: &'n str,
1657 bindings: usize,
1658 writes: usize,
1659 write_depth: usize,
1660}
1661
1662impl<'n> NameUsage<'n> {
1663 const fn new(name: &'n str) -> Self {
1664 Self {
1665 name,
1666 bindings: 0,
1667 writes: 0,
1668 write_depth: 0,
1669 }
1670 }
1671}
1672
1673impl<'a> Visit<'a> for NameUsage<'_> {
1674 fn visit_binding_identifier(&mut self, identifier: &BindingIdentifier<'a>) {
1675 if identifier.name == self.name {
1676 self.bindings += 1;
1677 }
1678 }
1679
1680 fn visit_identifier_reference(&mut self, identifier: &IdentifierReference<'a>) {
1681 if self.write_depth > 0 && identifier.name == self.name {
1682 self.writes += 1;
1683 }
1684 }
1685
1686 fn visit_assignment_target(&mut self, target: &AssignmentTarget<'a>) {
1687 self.write_depth += 1;
1688 walk::walk_assignment_target(self, target);
1689 self.write_depth -= 1;
1690 }
1691
1692 fn visit_update_expression(&mut self, expression: &UpdateExpression<'a>) {
1693 self.write_depth += 1;
1694 walk::walk_update_expression(self, expression);
1695 self.write_depth -= 1;
1696 }
1697
1698 fn visit_unary_expression(&mut self, expression: &UnaryExpression<'a>) {
1699 if expression.operator != UnaryOperator::Delete {
1700 walk::walk_unary_expression(self, expression);
1701 return;
1702 }
1703 self.write_depth += 1;
1704 walk::walk_unary_expression(self, expression);
1705 self.write_depth -= 1;
1706 }
1707}
1708
1709fn resolve_wrapped_config_object<'a>(
1722 program: &'a Program,
1723 expr: &'a Expression<'a>,
1724) -> Option<&'a ObjectExpression<'a>> {
1725 let call = match expr {
1726 Expression::CallExpression(call) => call,
1727 Expression::ParenthesizedExpression(paren) => {
1728 return resolve_wrapped_config_object(program, &paren.expression);
1729 }
1730 Expression::TSSatisfiesExpression(ts_sat) => {
1731 return resolve_wrapped_config_object(program, &ts_sat.expression);
1732 }
1733 Expression::TSAsExpression(ts_as) => {
1734 return resolve_wrapped_config_object(program, &ts_as.expression);
1735 }
1736 _ => return None,
1737 };
1738 for arg in &call.arguments {
1739 let Some(arg_expr) = arg.as_expression() else {
1740 continue;
1741 };
1742 if let Some(name) = unwrap_to_identifier_name(arg_expr)
1743 && let Some(obj) = find_variable_init_object(program, name)
1744 {
1745 return Some(obj);
1746 }
1747 if let Some(obj) = resolve_wrapped_config_object(program, arg_expr) {
1748 return Some(obj);
1749 }
1750 }
1751 None
1752}
1753
1754const MAX_CONFIG_WRAPPER_DEPTH: u8 = 3;
1759
1760fn resolve_call_config_object<'a>(
1771 program: &'a Program,
1772 expr: &'a Expression<'a>,
1773 depth: u8,
1774) -> Option<&'a ObjectExpression<'a>> {
1775 if depth == 0 {
1776 return None;
1777 }
1778 let call = match expr {
1779 Expression::CallExpression(call) => call,
1780 Expression::ParenthesizedExpression(paren) => {
1781 return resolve_call_config_object(program, &paren.expression, depth);
1782 }
1783 Expression::TSSatisfiesExpression(ts_sat) => {
1784 return resolve_call_config_object(program, &ts_sat.expression, depth);
1785 }
1786 Expression::TSAsExpression(ts_as) => {
1787 return resolve_call_config_object(program, &ts_as.expression, depth);
1788 }
1789 _ => return None,
1790 };
1791
1792 call.arguments
1793 .iter()
1794 .filter_map(oxc_ast::ast::Argument::as_expression)
1795 .find_map(|arg| resolve_config_argument(program, arg, depth))
1796}
1797
1798fn resolve_config_argument<'a>(
1800 program: &'a Program,
1801 expr: &'a Expression<'a>,
1802 depth: u8,
1803) -> Option<&'a ObjectExpression<'a>> {
1804 match expr {
1805 Expression::ObjectExpression(obj) => Some(obj),
1806 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
1807 Expression::FunctionExpression(func) => extract_object_from_function(func),
1808 Expression::ParenthesizedExpression(paren) => {
1809 resolve_config_argument(program, &paren.expression, depth)
1810 }
1811 Expression::TSSatisfiesExpression(ts_sat) => {
1812 resolve_config_argument(program, &ts_sat.expression, depth)
1813 }
1814 Expression::TSAsExpression(ts_as) => {
1815 resolve_config_argument(program, &ts_as.expression, depth)
1816 }
1817 Expression::CallExpression(_) => resolve_call_config_object(program, expr, depth - 1),
1818 _ => unwrap_to_identifier_name(expr)
1819 .and_then(|name| find_variable_init_object(program, name)),
1820 }
1821}
1822
1823pub(crate) fn find_property<'a>(
1825 obj: &'a ObjectExpression<'a>,
1826 key: &str,
1827) -> Option<&'a ObjectProperty<'a>> {
1828 for prop in &obj.properties {
1829 if let ObjectPropertyKind::ObjectProperty(p) = prop
1830 && property_key_matches(&p.key, key)
1831 {
1832 return Some(p);
1833 }
1834 }
1835 None
1836}
1837
1838fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
1840 match key {
1841 PropertyKey::StaticIdentifier(id) => id.name == name,
1842 PropertyKey::StringLiteral(s) => s.value == name,
1843 _ => false,
1844 }
1845}
1846
1847fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
1849 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
1850}
1851
1852fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
1854 find_property(obj, key)
1855 .map(|p| expression_to_string_array(&p.value))
1856 .unwrap_or_default()
1857}
1858
1859fn get_nested_string_array_from_object(
1861 obj: &ObjectExpression,
1862 path: &[&str],
1863) -> Option<Vec<String>> {
1864 if path.is_empty() {
1865 return None;
1866 }
1867 if path.len() == 1 {
1868 return Some(get_object_string_array_property(obj, path[0]));
1869 }
1870 let prop = find_property(obj, path[0])?;
1871 if let Expression::ObjectExpression(nested) = &prop.value {
1872 get_nested_string_array_from_object(nested, &path[1..])
1873 } else {
1874 None
1875 }
1876}
1877
1878fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
1880 if path.is_empty() {
1881 return None;
1882 }
1883 if path.len() == 1 {
1884 return get_object_string_property(obj, path[0]);
1885 }
1886 let prop = find_property(obj, path[0])?;
1887 if let Expression::ObjectExpression(nested) = &prop.value {
1888 get_nested_string_from_object(nested, &path[1..])
1889 } else {
1890 None
1891 }
1892}
1893
1894fn get_nested_command_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
1896 if path.is_empty() {
1897 return None;
1898 }
1899 if path.len() == 1 {
1900 return find_property(obj, path[0]).and_then(|prop| expression_to_command(&prop.value));
1901 }
1902 let prop = find_property(obj, path[0])?;
1903 if let Expression::ObjectExpression(nested) = &prop.value {
1904 get_nested_command_from_object(nested, &path[1..])
1905 } else {
1906 None
1907 }
1908}
1909
1910pub(crate) fn expression_to_string(expr: &Expression) -> Option<String> {
1912 match expr {
1913 Expression::StringLiteral(s) => Some(s.value.to_string()),
1914 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
1915 t.quasis.first().map(|q| q.value.raw.to_string())
1916 }
1917 _ => None,
1918 }
1919}
1920
1921fn expression_to_command(expr: &Expression) -> Option<String> {
1923 match expr {
1924 Expression::StringLiteral(s) => Some(s.value.to_string()),
1925 Expression::TemplateLiteral(template) => template_literal_to_command(template),
1926 Expression::ParenthesizedExpression(paren) => expression_to_command(&paren.expression),
1927 Expression::TSAsExpression(ts_as) => expression_to_command(&ts_as.expression),
1928 Expression::TSSatisfiesExpression(ts_sat) => expression_to_command(&ts_sat.expression),
1929 _ => None,
1930 }
1931}
1932
1933fn template_literal_to_command(template: &TemplateLiteral<'_>) -> Option<String> {
1934 let first = template.quasis.first()?.value.raw.as_str();
1935 if first.trim_start().is_empty() {
1936 return None;
1937 }
1938
1939 let mut command = String::new();
1940 for (idx, quasi) in template.quasis.iter().enumerate() {
1941 command.push_str(quasi.value.raw.as_str());
1942 if idx < template.expressions.len() {
1943 let next = template
1944 .quasis
1945 .get(idx + 1)
1946 .map_or("", |next| next.value.raw.as_str());
1947 if dynamic_template_boundary_splits_static_token(quasi.value.raw.as_str(), next) {
1948 return None;
1949 }
1950 command.push(' ');
1951 }
1952 }
1953
1954 Some(command)
1955}
1956
1957fn dynamic_template_boundary_splits_static_token(before: &str, after: &str) -> bool {
1958 before
1959 .chars()
1960 .next_back()
1961 .is_some_and(is_command_token_char)
1962 && after.chars().next().is_some_and(is_command_token_char)
1963}
1964
1965fn is_command_token_char(ch: char) -> bool {
1966 !ch.is_whitespace() && !matches!(ch, '&' | '|' | ';' | '"' | '\'')
1967}
1968
1969#[derive(Clone, Copy)]
1971enum PathScope<'p> {
1972 Bare,
1974 Program(&'p Program<'p>),
1976 Param { name: &'p str, value: &'p str },
1978}
1979
1980pub(crate) fn expression_to_path_string(expr: &Expression) -> Option<String> {
1982 path_string_in(expr, PathScope::Bare)
1983}
1984
1985fn path_string_in(expr: &Expression, scope: PathScope<'_>) -> Option<String> {
1986 match expr {
1987 Expression::ParenthesizedExpression(paren) => path_string_in(&paren.expression, scope),
1988 Expression::TSAsExpression(ts_as) => path_string_in(&ts_as.expression, scope),
1989 Expression::TSSatisfiesExpression(ts_sat) => path_string_in(&ts_sat.expression, scope),
1990 Expression::StaticMemberExpression(member) if member.property.name == "pathname" => {
1991 path_string_in(&member.object, scope)
1992 }
1993 Expression::CallExpression(call) => call_expression_to_path_string(call, scope),
1994 Expression::NewExpression(new_expr) => new_expression_to_path_string(new_expr, scope),
1995 _ => scoped_string(expr, scope),
1996 }
1997}
1998
1999fn scoped_string(expr: &Expression, scope: PathScope<'_>) -> Option<String> {
2001 match (expr, scope) {
2002 (Expression::Identifier(id), PathScope::Param { name, value }) if id.name == name => {
2003 Some(value.to_string())
2004 }
2005 _ => expression_to_string(expr),
2006 }
2007}
2008
2009fn call_expression_to_path_string(call: &CallExpression, scope: PathScope<'_>) -> Option<String> {
2010 if let PathScope::Program(program) = scope
2011 && let Expression::Identifier(id) = &call.callee
2012 && let Some(path) = local_helper_call_path(program, &id.name, call)
2013 {
2014 return Some(path);
2015 }
2016
2017 if matches!(&call.callee, Expression::Identifier(id) if id.name == "fileURLToPath") {
2018 return call
2019 .arguments
2020 .first()
2021 .and_then(Argument::as_expression)
2022 .and_then(|arg| path_string_in(arg, scope));
2023 }
2024
2025 let callee_name = match &call.callee {
2026 Expression::Identifier(id) => Some(id.name.as_str()),
2027 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
2028 _ => None,
2029 }?;
2030
2031 if !matches!(callee_name, "resolve" | "join") {
2032 return None;
2033 }
2034
2035 let mut segments = Vec::new();
2036 for (index, arg) in call.arguments.iter().enumerate() {
2037 let expr = arg.as_expression()?;
2038
2039 if is_dirname_anchor(expr) {
2040 if index == 0 {
2041 continue;
2042 }
2043 return None;
2044 }
2045
2046 segments.push(scoped_string(expr, scope)?);
2047 }
2048
2049 (!segments.is_empty()).then(|| join_path_segments(&segments))
2050}
2051
2052fn local_helper_call_path(program: &Program, name: &str, call: &CallExpression) -> Option<String> {
2059 let [argument] = call.arguments.as_slice() else {
2060 return None;
2061 };
2062 let value = expression_to_string(argument.as_expression()?)?;
2063 let (param, body) = find_local_path_helper(program, name)?;
2064 path_string_in(
2065 body,
2066 PathScope::Param {
2067 name: param,
2068 value: &value,
2069 },
2070 )
2071}
2072
2073fn find_local_path_helper<'a>(
2077 program: &'a Program<'a>,
2078 name: &str,
2079) -> Option<(&'a str, &'a Expression<'a>)> {
2080 let declared = program.body.iter().find_map(|stmt| {
2081 let func = match stmt {
2082 Statement::FunctionDeclaration(func) => func,
2083 Statement::ExportDeclaration(export) => match &export.declaration {
2084 Declaration::FunctionDeclaration(func) => func,
2085 _ => return None,
2086 },
2087 _ => return None,
2088 };
2089 func.id
2090 .as_ref()
2091 .is_some_and(|id| id.name == name)
2092 .then_some(&**func)
2093 });
2094 if let Some(func) = declared {
2095 return function_helper_body(func);
2096 }
2097 match find_variable_init_expression(program, name)?.without_parentheses() {
2098 Expression::ArrowFunctionExpression(arrow) => {
2099 let param = single_simple_param(&arrow.params)?;
2100 let body = match &arrow.body {
2101 ArrowFunctionBody::FunctionBody(body) => single_returned_expression(body)?,
2102 body => body.as_expression()?,
2103 };
2104 Some((param, body))
2105 }
2106 Expression::FunctionExpression(func) => function_helper_body(func),
2107 _ => None,
2108 }
2109}
2110
2111fn function_helper_body<'a>(func: &'a Function<'a>) -> Option<(&'a str, &'a Expression<'a>)> {
2112 let param = single_simple_param(&func.params)?;
2113 let body = single_returned_expression(func.body.as_ref()?)?;
2114 Some((param, body))
2115}
2116
2117fn single_simple_param<'a>(params: &'a FormalParameters<'a>) -> Option<&'a str> {
2118 let [param] = params.items.as_slice() else {
2119 return None;
2120 };
2121 if params.rest.is_some() || param.initializer.is_some() {
2122 return None;
2123 }
2124 match ¶m.pattern {
2125 BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
2126 _ => None,
2127 }
2128}
2129
2130fn single_returned_expression<'a>(body: &'a FunctionBody<'a>) -> Option<&'a Expression<'a>> {
2131 match body.statements.as_slice() {
2132 [Statement::ReturnStatement(ret)] => ret.argument.as_ref(),
2133 _ => None,
2134 }
2135}
2136
2137fn is_dirname_anchor(expr: &Expression) -> bool {
2142 match expr {
2143 Expression::Identifier(id) => id.name == "__dirname",
2144 Expression::StaticMemberExpression(member) => {
2145 member.property.name == "dirname" && is_import_meta_expression(&member.object)
2146 }
2147 _ => false,
2148 }
2149}
2150
2151fn is_import_meta_expression(expr: &Expression) -> bool {
2153 matches!(expr, Expression::ImportMeta(_))
2154}
2155
2156fn new_expression_to_path_string(new_expr: &NewExpression, scope: PathScope<'_>) -> Option<String> {
2157 if !matches!(&new_expr.callee, Expression::Identifier(id) if id.name == "URL") {
2158 return None;
2159 }
2160
2161 let source = new_expr
2162 .arguments
2163 .first()
2164 .and_then(Argument::as_expression)
2165 .and_then(|arg| scoped_string(arg, scope))?;
2166
2167 let base = new_expr
2168 .arguments
2169 .get(1)
2170 .and_then(Argument::as_expression)?;
2171 is_import_meta_url_expression(base).then_some(source)
2172}
2173
2174fn is_import_meta_url_expression(expr: &Expression) -> bool {
2175 if let Expression::StaticMemberExpression(member) = expr {
2176 member.property.name == "url"
2177 && matches!(
2178 member.object,
2179 Expression::ImportMeta(_) | Expression::NewTarget(_)
2180 )
2181 } else {
2182 false
2183 }
2184}
2185
2186fn join_path_segments(segments: &[String]) -> String {
2187 let mut joined = PathBuf::new();
2188 for segment in segments {
2189 joined.push(segment);
2190 }
2191 joined.to_string_lossy().replace('\\', "/")
2192}
2193
2194fn expression_to_alias_pairs_kinded(expr: &Expression) -> Vec<(String, String, bool)> {
2199 match expr {
2200 Expression::ObjectExpression(obj) => obj
2201 .properties
2202 .iter()
2203 .filter_map(|prop| {
2204 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
2205 return None;
2206 };
2207 let find = property_key_to_string(&prop.key)?;
2208 let (replacement, is_bare) =
2209 alias_replacement_kinded(&prop.value, PathScope::Bare)?;
2210 Some((find, replacement, is_bare))
2211 })
2212 .collect(),
2213 Expression::ArrayExpression(arr) => arr
2214 .elements
2215 .iter()
2216 .filter_map(|element| {
2217 let Expression::ObjectExpression(obj) = element.as_expression()? else {
2218 return None;
2219 };
2220 let find = find_property(obj, "find")
2221 .and_then(|prop| expression_to_string(&prop.value))?;
2222 let (replacement, is_bare) = find_property(obj, "replacement")
2223 .and_then(|prop| alias_replacement_kinded(&prop.value, PathScope::Bare))?;
2224 Some((find, replacement, is_bare))
2225 })
2226 .collect(),
2227 _ => Vec::new(),
2228 }
2229}
2230
2231fn alias_replacement_kinded(expr: &Expression, scope: PathScope<'_>) -> Option<(String, bool)> {
2238 match expr {
2239 Expression::ParenthesizedExpression(paren) => {
2240 alias_replacement_kinded(&paren.expression, scope)
2241 }
2242 Expression::TSAsExpression(ts_as) => alias_replacement_kinded(&ts_as.expression, scope),
2243 Expression::TSSatisfiesExpression(ts_sat) => {
2244 alias_replacement_kinded(&ts_sat.expression, scope)
2245 }
2246 Expression::StringLiteral(s) => {
2247 let value = s.value.to_string();
2248 let is_bare =
2249 !value.starts_with("./") && !value.starts_with("../") && !value.starts_with('/');
2250 Some((value, is_bare))
2251 }
2252 Expression::ArrayExpression(arr) => arr
2256 .elements
2257 .iter()
2258 .find_map(ArrayExpressionElement::as_expression)
2259 .and_then(|element| alias_replacement_kinded(element, scope)),
2260 _ => path_string_in(expr, scope).map(|value| (value, false)),
2261 }
2262}
2263
2264const MAX_ALIAS_RESOLVE_DEPTH: usize = 8;
2270
2271const ALIAS_SIBLING_EXTS: [&str; 6] = ["js", "mjs", "cjs", "ts", "mts", "cts"];
2278
2279fn resolve_alias_pairs_kinded(
2296 program: &Program,
2297 config_path: &Path,
2298 expr: &Expression,
2299 visited: &mut FxHashSet<PathBuf>,
2300 depth: usize,
2301) -> Vec<(String, String, bool)> {
2302 match expr {
2303 Expression::ParenthesizedExpression(paren) => {
2304 resolve_alias_pairs_kinded(program, config_path, &paren.expression, visited, depth)
2305 }
2306 Expression::TSAsExpression(ts_as) => {
2307 resolve_alias_pairs_kinded(program, config_path, &ts_as.expression, visited, depth)
2308 }
2309 Expression::TSSatisfiesExpression(ts_sat) => {
2310 resolve_alias_pairs_kinded(program, config_path, &ts_sat.expression, visited, depth)
2311 }
2312 Expression::ObjectExpression(obj) => {
2313 resolve_object_alias_pairs_kinded(program, config_path, obj, visited, depth)
2314 }
2315 Expression::ArrayExpression(arr) => {
2316 resolve_array_alias_pairs_kinded(program, config_path, arr, visited, depth)
2317 }
2318 Expression::Identifier(id) => {
2319 resolve_identifier_alias_pairs(program, config_path, id.name.as_str(), visited, depth)
2320 }
2321 _ => Vec::new(),
2322 }
2323}
2324
2325fn resolve_object_alias_pairs_kinded(
2328 program: &Program,
2329 config_path: &Path,
2330 obj: &ObjectExpression,
2331 visited: &mut FxHashSet<PathBuf>,
2332 depth: usize,
2333) -> Vec<(String, String, bool)> {
2334 let mut pairs = Vec::new();
2335 for prop in &obj.properties {
2336 match prop {
2337 ObjectPropertyKind::ObjectProperty(prop) => {
2338 if let Some(find) = property_key_to_string(&prop.key)
2339 && let Some((replacement, is_bare)) =
2340 alias_replacement_kinded(&prop.value, PathScope::Program(program))
2341 {
2342 pairs.push((find, replacement, is_bare));
2343 }
2344 }
2345 ObjectPropertyKind::SpreadProperty(spread) => {
2347 pairs.extend(resolve_alias_pairs_kinded(
2348 program,
2349 config_path,
2350 &spread.argument,
2351 visited,
2352 depth,
2353 ));
2354 }
2355 }
2356 }
2357 pairs
2358}
2359
2360fn resolve_array_alias_pairs_kinded(
2363 program: &Program,
2364 config_path: &Path,
2365 arr: &ArrayExpression,
2366 visited: &mut FxHashSet<PathBuf>,
2367 depth: usize,
2368) -> Vec<(String, String, bool)> {
2369 let mut pairs = Vec::new();
2370 for element in &arr.elements {
2371 match element {
2372 ArrayExpressionElement::SpreadElement(spread) => {
2374 pairs.extend(resolve_alias_pairs_kinded(
2375 program,
2376 config_path,
2377 &spread.argument,
2378 visited,
2379 depth,
2380 ));
2381 }
2382 _ => {
2383 if let Some(Expression::ObjectExpression(obj)) = element.as_expression()
2384 && let Some(find) = find_property(obj, "find")
2385 .and_then(|prop| expression_to_string(&prop.value))
2386 && let Some((replacement, is_bare)) = find_property(obj, "replacement")
2387 .and_then(|prop| {
2388 alias_replacement_kinded(&prop.value, PathScope::Program(program))
2389 })
2390 {
2391 pairs.push((find, replacement, is_bare));
2392 }
2393 }
2394 }
2395 }
2396 pairs
2397}
2398
2399fn resolve_identifier_alias_pairs(
2402 program: &Program,
2403 config_path: &Path,
2404 name: &str,
2405 visited: &mut FxHashSet<PathBuf>,
2406 depth: usize,
2407) -> Vec<(String, String, bool)> {
2408 if depth >= MAX_ALIAS_RESOLVE_DEPTH {
2409 return Vec::new();
2410 }
2411 if let Some(init) = find_variable_init_expression(program, name) {
2413 return resolve_alias_pairs_kinded(program, config_path, init, visited, depth + 1);
2414 }
2415 let Some((specifier, imported_name)) = find_relative_import_binding(program, name) else {
2417 return Vec::new();
2418 };
2419 resolve_imported_alias_pairs(
2420 config_path,
2421 &specifier,
2422 imported_name.as_deref(),
2423 visited,
2424 depth + 1,
2425 )
2426}
2427
2428fn resolve_imported_alias_pairs(
2431 config_path: &Path,
2432 specifier: &str,
2433 imported_name: Option<&str>,
2434 visited: &mut FxHashSet<PathBuf>,
2435 depth: usize,
2436) -> Vec<(String, String, bool)> {
2437 let Some((sibling_path, sibling_source)) = resolve_sibling_module(config_path, specifier)
2438 else {
2439 return Vec::new();
2440 };
2441 if !visited.insert(sibling_path.clone()) {
2442 return Vec::new();
2443 }
2444 extract_from_source(&sibling_source, &sibling_path, |program| {
2445 let init = find_exported_init(program, imported_name)?;
2446 let pairs = resolve_alias_pairs_kinded(program, &sibling_path, init, visited, depth);
2447 (!pairs.is_empty()).then_some(pairs)
2448 })
2449 .unwrap_or_default()
2450}
2451
2452fn find_variable_init_expression<'a>(
2457 program: &'a Program<'a>,
2458 name: &str,
2459) -> Option<&'a Expression<'a>> {
2460 for decl in top_level_variable_declarations(program) {
2461 for declarator in &decl.declarations {
2462 if let BindingPattern::BindingIdentifier(id) = &declarator.id
2463 && id.name == name
2464 && let Some(init) = &declarator.init
2465 {
2466 return Some(init);
2467 }
2468 }
2469 }
2470 None
2471}
2472
2473pub(crate) fn find_module_export_expression<'a>(
2476 program: &'a Program<'a>,
2477) -> Option<&'a Expression<'a>> {
2478 program.body.iter().find_map(|stmt| match stmt {
2479 Statement::ExportDefaultDeclaration(decl) => decl.declaration.as_expression(),
2480 Statement::ExpressionStatement(expr_stmt) => match &expr_stmt.expression {
2481 Expression::AssignmentExpression(assign) if is_module_exports_target(&assign.left) => {
2482 Some(&assign.right)
2483 }
2484 _ => None,
2485 },
2486 _ => None,
2487 })
2488}
2489
2490pub(crate) fn find_exported_init<'a>(
2495 program: &'a Program<'a>,
2496 name: Option<&str>,
2497) -> Option<&'a Expression<'a>> {
2498 match name {
2499 Some(name) => find_variable_init_expression(program, name),
2500 None => program.body.iter().find_map(|stmt| {
2501 if let Statement::ExportDefaultDeclaration(decl) = stmt {
2502 decl.declaration.as_expression()
2503 } else {
2504 None
2505 }
2506 }),
2507 }
2508}
2509
2510pub(crate) fn find_relative_import_binding(
2515 program: &Program,
2516 name: &str,
2517) -> Option<(String, Option<String>)> {
2518 for stmt in &program.body {
2519 let Statement::ImportDeclaration(decl) = stmt else {
2520 continue;
2521 };
2522 let specifier = decl.source.value.as_str();
2523 if !is_relative_specifier(specifier) {
2524 continue;
2525 }
2526 let Some(specifiers) = &decl.specifiers else {
2527 continue;
2528 };
2529 for spec in specifiers {
2530 match spec {
2531 ImportDeclarationSpecifier::ImportSpecifier(spec) if spec.local.name == name => {
2532 return Some((
2533 specifier.to_string(),
2534 Some(spec.imported.name().to_string()),
2535 ));
2536 }
2537 ImportDeclarationSpecifier::ImportDefaultSpecifier(spec)
2538 if spec.local.name == name =>
2539 {
2540 return Some((specifier.to_string(), None));
2541 }
2542 _ => {}
2543 }
2544 }
2545 }
2546 None
2547}
2548
2549pub(crate) fn is_relative_specifier(specifier: &str) -> bool {
2552 specifier.starts_with("./") || specifier.starts_with("../") || specifier.starts_with('/')
2553}
2554
2555pub(crate) fn resolve_sibling_module(
2561 config_path: &Path,
2562 specifier: &str,
2563) -> Option<(PathBuf, String)> {
2564 let parent = config_path.parent().unwrap_or(config_path);
2565 let direct = parent.join(specifier);
2566 if let Ok(source) = std::fs::read_to_string(&direct) {
2567 return Some((direct, source));
2568 }
2569 for ext in ALIAS_SIBLING_EXTS {
2570 let candidate = parent.join(format!("{specifier}.{ext}"));
2571 if let Ok(source) = std::fs::read_to_string(&candidate) {
2572 return Some((candidate, source));
2573 }
2574 }
2575 for ext in ALIAS_SIBLING_EXTS {
2576 let candidate = direct.join(format!("index.{ext}"));
2577 if let Ok(source) = std::fs::read_to_string(&candidate) {
2578 return Some((candidate, source));
2579 }
2580 }
2581 None
2582}
2583
2584fn find_default_export_array<'a>(program: &'a Program<'a>) -> Option<&'a ArrayExpression<'a>> {
2589 for stmt in &program.body {
2590 if let Statement::ExportDefaultDeclaration(decl) = stmt
2591 && let Some(expr) = decl.declaration.as_expression()
2592 {
2593 return array_from_expression(expr);
2594 }
2595 }
2596 None
2597}
2598
2599fn array_from_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
2600 match expr {
2601 Expression::ArrayExpression(arr) => Some(arr),
2602 Expression::ParenthesizedExpression(paren) => array_from_expression(&paren.expression),
2603 Expression::TSAsExpression(ts_as) => array_from_expression(&ts_as.expression),
2604 Expression::TSSatisfiesExpression(ts_sat) => array_from_expression(&ts_sat.expression),
2605 Expression::CallExpression(call) => call
2606 .arguments
2607 .first()
2608 .and_then(Argument::as_expression)
2609 .and_then(array_from_expression),
2610 _ => None,
2611 }
2612}
2613
2614pub(crate) fn lexical_normalize(path: &Path) -> PathBuf {
2615 let mut normalized = PathBuf::new();
2616
2617 for component in path.components() {
2618 match component {
2619 std::path::Component::CurDir => {}
2620 std::path::Component::ParentDir => {
2621 normalized.pop();
2622 }
2623 _ => normalized.push(component.as_os_str()),
2624 }
2625 }
2626
2627 normalized
2628}
2629
2630pub(crate) fn is_package_specifier(specifier: &str) -> bool {
2634 !specifier.is_empty()
2635 && specifier != "."
2636 && specifier != ".."
2637 && !specifier.starts_with("./")
2638 && !specifier.starts_with("../")
2639 && !specifier.starts_with('/')
2640 && !specifier.contains(':')
2641 && !specifier.contains('\\')
2642 && !specifier.chars().any(char::is_whitespace)
2643}
2644
2645fn expression_to_string_array(expr: &Expression) -> Vec<String> {
2647 match expr {
2648 Expression::ArrayExpression(arr) => arr
2649 .elements
2650 .iter()
2651 .filter_map(|el| match el {
2652 ArrayExpressionElement::SpreadElement(_) => None,
2653 _ => el.as_expression().and_then(expression_to_string),
2654 })
2655 .collect(),
2656 _ => vec![],
2657 }
2658}
2659
2660fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
2665 let mut values = Vec::new();
2666 match expr {
2667 Expression::StringLiteral(s) => {
2668 values.push(s.value.to_string());
2669 }
2670 Expression::ArrayExpression(arr) => {
2671 for el in &arr.elements {
2672 if let Some(inner) = el.as_expression() {
2673 match inner {
2674 Expression::StringLiteral(s) => {
2675 values.push(s.value.to_string());
2676 }
2677 Expression::ArrayExpression(sub_arr) => {
2678 if let Some(first) = sub_arr.elements.first()
2679 && let Some(first_expr) = first.as_expression()
2680 && let Some(s) = expression_to_string(first_expr)
2681 {
2682 values.push(s);
2683 }
2684 }
2685 _ => {}
2686 }
2687 }
2688 }
2689 }
2690 Expression::ObjectExpression(obj) => {
2691 for prop in &obj.properties {
2692 if let ObjectPropertyKind::ObjectProperty(p) = prop {
2693 match &p.value {
2694 Expression::StringLiteral(s) => {
2695 values.push(s.value.to_string());
2696 }
2697 Expression::ArrayExpression(sub_arr) => {
2698 if let Some(first) = sub_arr.elements.first()
2699 && let Some(first_expr) = first.as_expression()
2700 && let Some(s) = expression_to_string(first_expr)
2701 {
2702 values.push(s);
2703 }
2704 }
2705 _ => {}
2706 }
2707 }
2708 }
2709 }
2710 _ => {}
2711 }
2712 values
2713}
2714
2715fn collect_shallow_string_or_object_property_values(
2717 expr: &Expression,
2718 object_property: &str,
2719) -> Vec<String> {
2720 match expr {
2721 Expression::ArrayExpression(arr) => arr
2722 .elements
2723 .iter()
2724 .filter_map(|element| {
2725 element
2726 .as_expression()
2727 .and_then(|expr| shallow_string_or_object_property(expr, object_property))
2728 })
2729 .collect(),
2730 _ => shallow_string_or_object_property(expr, object_property)
2731 .into_iter()
2732 .collect(),
2733 }
2734}
2735
2736fn shallow_string_or_object_property(expr: &Expression, object_property: &str) -> Option<String> {
2737 match expr {
2738 Expression::ParenthesizedExpression(paren) => {
2739 shallow_string_or_object_property(&paren.expression, object_property)
2740 }
2741 Expression::TSSatisfiesExpression(ts_sat) => {
2742 shallow_string_or_object_property(&ts_sat.expression, object_property)
2743 }
2744 Expression::TSAsExpression(ts_as) => {
2745 shallow_string_or_object_property(&ts_as.expression, object_property)
2746 }
2747 Expression::ArrayExpression(sub_arr) => sub_arr
2748 .elements
2749 .first()
2750 .and_then(ArrayExpressionElement::as_expression)
2751 .and_then(expression_to_string),
2752 Expression::ObjectExpression(obj) => {
2753 find_property(obj, object_property).and_then(|prop| expression_to_string(&prop.value))
2754 }
2755 _ => expression_to_string(expr),
2756 }
2757}
2758
2759fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
2761 match expr {
2762 Expression::StringLiteral(s) => {
2763 values.push(s.value.to_string());
2764 }
2765 Expression::ArrayExpression(arr) => {
2766 for el in &arr.elements {
2767 if let Some(expr) = el.as_expression() {
2768 collect_all_string_values(expr, values);
2769 }
2770 }
2771 }
2772 Expression::ObjectExpression(obj) => {
2773 for prop in &obj.properties {
2774 if let ObjectPropertyKind::ObjectProperty(p) = prop {
2775 collect_all_string_values(&p.value, values);
2776 }
2777 }
2778 }
2779 _ => {}
2780 }
2781}
2782
2783fn property_key_to_string(key: &PropertyKey) -> Option<String> {
2785 match key {
2786 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
2787 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
2788 _ => None,
2789 }
2790}
2791
2792fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
2794 if path.is_empty() {
2795 return None;
2796 }
2797 let prop = find_property(obj, path[0])?;
2798 if path.len() == 1 {
2799 if let Expression::ObjectExpression(nested) = &prop.value {
2800 let keys = nested
2801 .properties
2802 .iter()
2803 .filter_map(|p| {
2804 if let ObjectPropertyKind::ObjectProperty(p) = p {
2805 property_key_to_string(&p.key)
2806 } else {
2807 None
2808 }
2809 })
2810 .collect();
2811 return Some(keys);
2812 }
2813 return None;
2814 }
2815 if let Expression::ObjectExpression(nested) = &prop.value {
2816 get_nested_object_keys(nested, &path[1..])
2817 } else {
2818 None
2819 }
2820}
2821
2822fn get_nested_expression<'a>(
2824 obj: &'a ObjectExpression<'a>,
2825 path: &[&str],
2826) -> Option<&'a Expression<'a>> {
2827 if path.is_empty() {
2828 return None;
2829 }
2830 let prop = find_property(obj, path[0])?;
2831 if path.len() == 1 {
2832 return Some(&prop.value);
2833 }
2834 if let Expression::ObjectExpression(nested) = &prop.value {
2835 get_nested_expression(nested, &path[1..])
2836 } else {
2837 None
2838 }
2839}
2840
2841fn get_nested_string_or_array(
2843 obj: &ObjectExpression,
2844 path: &[&str],
2845 scope: PathScope<'_>,
2846) -> Option<Vec<String>> {
2847 if path.is_empty() {
2848 return None;
2849 }
2850 if path.len() == 1 {
2851 let prop = find_property(obj, path[0])?;
2852 return Some(string_or_array_in(&prop.value, scope));
2853 }
2854 let prop = find_property(obj, path[0])?;
2855 if let Expression::ObjectExpression(nested) = &prop.value {
2856 get_nested_string_or_array(nested, &path[1..], scope)
2857 } else {
2858 None
2859 }
2860}
2861
2862pub(crate) fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
2870 string_or_array_in(expr, PathScope::Bare)
2871}
2872
2873fn string_or_array_in(expr: &Expression, scope: PathScope<'_>) -> Vec<String> {
2874 match expr {
2875 Expression::StringLiteral(s) => vec![s.value.to_string()],
2876 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
2877 .quasis
2878 .first()
2879 .map(|q| vec![q.value.raw.to_string()])
2880 .unwrap_or_default(),
2881 Expression::ArrayExpression(arr) => arr
2882 .elements
2883 .iter()
2884 .filter_map(|el| el.as_expression())
2885 .flat_map(|e| match e {
2886 Expression::ObjectExpression(obj) => find_property(obj, "input")
2887 .map(|p| string_or_array_in(&p.value, scope))
2888 .unwrap_or_default(),
2889 _ => path_string_in(e, scope).into_iter().collect(),
2890 })
2891 .collect(),
2892 Expression::ObjectExpression(obj) => obj
2893 .properties
2894 .iter()
2895 .flat_map(|p| {
2896 if let ObjectPropertyKind::ObjectProperty(p) = p {
2897 match &p.value {
2898 Expression::ArrayExpression(_) => string_or_array_in(&p.value, scope),
2899 Expression::ObjectExpression(value_obj) => {
2900 find_property(value_obj, "import")
2901 .map(|import_prop| string_or_array_in(&import_prop.value, scope))
2902 .unwrap_or_default()
2903 }
2904 _ => path_string_in(&p.value, scope).into_iter().collect(),
2905 }
2906 } else {
2907 Vec::new()
2908 }
2909 })
2910 .collect(),
2911 _ => path_string_in(expr, scope).into_iter().collect(),
2912 }
2913}
2914
2915fn collect_require_sources(expr: &Expression) -> Vec<String> {
2917 let mut sources = Vec::new();
2918 match expr {
2919 Expression::CallExpression(call) if is_require_call(call) => {
2920 if let Some(s) = get_require_source(call) {
2921 sources.push(s);
2922 }
2923 }
2924 Expression::ArrayExpression(arr) => {
2925 for el in &arr.elements {
2926 if let Some(inner) = el.as_expression() {
2927 match inner {
2928 Expression::CallExpression(call) if is_require_call(call) => {
2929 if let Some(s) = get_require_source(call) {
2930 sources.push(s);
2931 }
2932 }
2933 Expression::ArrayExpression(sub_arr) => {
2934 if let Some(first) = sub_arr.elements.first()
2935 && let Some(Expression::CallExpression(call)) =
2936 first.as_expression()
2937 && is_require_call(call)
2938 && let Some(s) = get_require_source(call)
2939 {
2940 sources.push(s);
2941 }
2942 }
2943 _ => {}
2944 }
2945 }
2946 }
2947 }
2948 _ => {}
2949 }
2950 sources
2951}
2952
2953pub(crate) fn is_require_call(call: &CallExpression) -> bool {
2955 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
2956}
2957
2958pub(crate) fn get_require_source(call: &CallExpression) -> Option<String> {
2960 call.arguments.first().and_then(|arg| {
2961 if let Argument::StringLiteral(s) = arg {
2962 Some(s.value.to_string())
2963 } else {
2964 None
2965 }
2966 })
2967}
2968
2969#[cfg(test)]
2970mod tests {
2971 use super::*;
2972 use std::path::PathBuf;
2973
2974 fn js_path() -> PathBuf {
2975 PathBuf::from("config.js")
2976 }
2977
2978 fn ts_path() -> PathBuf {
2979 PathBuf::from("config.ts")
2980 }
2981
2982 #[test]
2983 fn extract_config_array_tuple_heads_reads_first_string_of_each_tuple() {
2984 let source = r#"
2985 export default defineNuxtConfig({
2986 modules: ["@nuxt/image", ["@nuxt/content", { watch: false }], [someModule, {}], []]
2987 });
2988 "#;
2989 assert_eq!(
2990 extract_config_array_tuple_heads(source, &ts_path(), &["modules"]),
2991 vec!["@nuxt/content".to_string()]
2992 );
2993 }
2994
2995 #[test]
2996 fn extract_lazy_imports_bare_arrows() {
2997 let source = r"
2998 import { defineConfig } from '@adonisjs/core/app'
2999 export default defineConfig({
3000 preloads: [
3001 () => import('#start/routes'),
3002 () => import('#start/kernel'),
3003 ],
3004 })
3005 ";
3006 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["preloads"]);
3007 assert_eq!(specs, vec!["#start/routes", "#start/kernel"]);
3008 }
3009
3010 #[test]
3011 fn extract_lazy_imports_object_form_with_file_key() {
3012 let source = r"
3013 export default defineConfig({
3014 providers: [
3015 () => import('@adonisjs/core/providers/app_provider'),
3016 {
3017 file: () => import('@adonisjs/core/providers/repl_provider'),
3018 environment: ['repl', 'test'],
3019 },
3020 ],
3021 })
3022 ";
3023 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
3024 assert_eq!(
3025 specs,
3026 vec![
3027 "@adonisjs/core/providers/app_provider",
3028 "@adonisjs/core/providers/repl_provider",
3029 ]
3030 );
3031 }
3032
3033 #[test]
3034 fn extract_lazy_imports_block_body_with_return() {
3035 let source = r"
3036 export default defineConfig({
3037 commands: [
3038 () => { return import('@adonisjs/core/commands') },
3039 ],
3040 })
3041 ";
3042 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
3043 assert_eq!(specs, vec!["@adonisjs/core/commands"]);
3044 }
3045
3046 #[test]
3047 fn extract_lazy_imports_skips_unknown_element_shapes() {
3048 let source = r"
3049 export default defineConfig({
3050 commands: [
3051 'string-entry',
3052 42,
3053 { other: 'value' },
3054 () => import('@adonisjs/lucid/commands'),
3055 ],
3056 })
3057 ";
3058 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
3059 assert_eq!(specs, vec!["@adonisjs/lucid/commands"]);
3060 }
3061
3062 #[test]
3063 fn extract_lazy_imports_missing_property_returns_empty() {
3064 let source = r"
3065 export default defineConfig({
3066 preloads: [() => import('#start/routes')],
3067 })
3068 ";
3069 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
3070 assert!(specs.is_empty());
3071 }
3072
3073 #[test]
3074 fn extract_imports_basic() {
3075 let source = r"
3076 import foo from 'foo-pkg';
3077 import { bar } from '@scope/bar';
3078 export default {};
3079 ";
3080 let imports = extract_imports(source, &js_path());
3081 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
3082 }
3083
3084 #[test]
3085 fn extract_default_export_object_property() {
3086 let source = r#"export default { testDir: "./tests" };"#;
3087 let val = extract_config_string(source, &js_path(), &["testDir"]);
3088 assert_eq!(val, Some("./tests".to_string()));
3089 }
3090
3091 #[test]
3092 fn extract_define_config_property() {
3093 let source = r#"
3094 import { defineConfig } from 'vitest/config';
3095 export default defineConfig({
3096 test: {
3097 include: ["**/*.test.ts", "**/*.spec.ts"],
3098 setupFiles: ["./test/setup.ts"]
3099 }
3100 });
3101 "#;
3102 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
3103 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
3104
3105 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
3106 assert_eq!(setup, vec!["./test/setup.ts"]);
3107 }
3108
3109 #[test]
3110 fn extract_module_exports_property() {
3111 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
3112 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
3113 assert_eq!(val, Some("jsdom".to_string()));
3114 }
3115
3116 #[test]
3117 fn extract_nested_string_array() {
3118 let source = r#"
3119 export default {
3120 resolve: {
3121 alias: {
3122 "@": "./src"
3123 }
3124 },
3125 test: {
3126 include: ["src/**/*.test.ts"]
3127 }
3128 };
3129 "#;
3130 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
3131 assert_eq!(include, vec!["src/**/*.test.ts"]);
3132 }
3133
3134 #[test]
3135 fn extract_addons_array() {
3136 let source = r#"
3137 export default {
3138 addons: [
3139 "@storybook/addon-a11y",
3140 "@storybook/addon-docs",
3141 "@storybook/addon-links"
3142 ]
3143 };
3144 "#;
3145 let addons = extract_config_property_strings(source, &ts_path(), "addons");
3146 assert_eq!(
3147 addons,
3148 vec![
3149 "@storybook/addon-a11y",
3150 "@storybook/addon-docs",
3151 "@storybook/addon-links"
3152 ]
3153 );
3154 }
3155
3156 #[test]
3157 fn handle_empty_config() {
3158 let source = "";
3159 let result = extract_config_string(source, &js_path(), &["key"]);
3160 assert_eq!(result, None);
3161 }
3162
3163 #[test]
3164 fn object_keys_postcss_plugins() {
3165 let source = r"
3166 module.exports = {
3167 plugins: {
3168 autoprefixer: {},
3169 tailwindcss: {},
3170 'postcss-import': {}
3171 }
3172 };
3173 ";
3174 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3175 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
3176 }
3177
3178 #[test]
3179 fn object_keys_nested_path() {
3180 let source = r"
3181 export default {
3182 build: {
3183 plugins: {
3184 minify: {},
3185 compress: {}
3186 }
3187 }
3188 };
3189 ";
3190 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
3191 assert_eq!(keys, vec!["minify", "compress"]);
3192 }
3193
3194 #[test]
3195 fn object_keys_empty_object() {
3196 let source = r"export default { plugins: {} };";
3197 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3198 assert!(keys.is_empty());
3199 }
3200
3201 #[test]
3202 fn object_keys_non_object_returns_empty() {
3203 let source = r#"export default { plugins: ["a", "b"] };"#;
3204 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3205 assert!(keys.is_empty());
3206 }
3207
3208 #[test]
3209 fn string_or_array_single_string() {
3210 let source = r#"export default { entry: "./src/index.js" };"#;
3211 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3212 assert_eq!(result, vec!["./src/index.js"]);
3213 }
3214
3215 #[test]
3216 fn string_or_array_array() {
3217 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
3218 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3219 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
3220 }
3221
3222 #[test]
3223 fn string_or_array_object_values() {
3224 let source =
3225 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
3226 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3227 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
3228 }
3229
3230 #[test]
3231 fn array_or_object_string_or_array_reads_each_array_element() {
3232 let source = r#"
3233 const shared = { format: ["esm"] };
3234 export default defineConfig([
3235 { entry: "./src/a.ts" },
3236 ({ ...shared, entry: { b: "./src/b.ts" } }),
3237 { entry: ["./src/c.ts"] } satisfies Options,
3238 { format: ["cjs"] },
3239 shared,
3240 ] as Options[]);
3241 "#;
3242 let result = extract_config_array_or_object_string_or_array(source, &ts_path(), &["entry"]);
3243 assert_eq!(result, vec!["./src/a.ts", "./src/b.ts", "./src/c.ts"]);
3244 }
3245
3246 #[test]
3247 fn array_or_object_string_or_array_falls_back_to_config_object() {
3248 let source = r#"export default defineConfig({ entry: { main: "./src/main.ts" } });"#;
3249 let result = extract_config_array_or_object_string_or_array(source, &ts_path(), &["entry"]);
3250 assert_eq!(result, vec!["./src/main.ts"]);
3251 }
3252
3253 #[test]
3254 fn string_or_array_object_array_values() {
3255 let source = r#"export default { entry: { app: ["./src/polyfill.js", "./src/app.js"] } };"#;
3256 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3257 assert_eq!(result, vec!["./src/polyfill.js", "./src/app.js"]);
3258 }
3259
3260 #[test]
3261 fn string_or_array_webpack_entry_descriptors() {
3262 let source = r#"
3263 export default {
3264 entry: {
3265 app: {
3266 import: "./src/app.js",
3267 filename: "pages/app.js",
3268 dependOn: "shared",
3269 },
3270 admin: {
3271 import: ["./src/admin-polyfill.js", "./src/admin.js"],
3272 runtime: "runtime",
3273 },
3274 shared: ["react", "react-dom"],
3275 },
3276 };
3277 "#;
3278 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3279 assert_eq!(
3280 result,
3281 vec![
3282 "./src/app.js",
3283 "./src/admin-polyfill.js",
3284 "./src/admin.js",
3285 "react",
3286 "react-dom"
3287 ]
3288 );
3289 }
3290
3291 #[test]
3292 fn string_or_array_nested_path() {
3293 let source = r#"
3294 export default {
3295 build: {
3296 rollupOptions: {
3297 input: ["./index.html", "./about.html"]
3298 }
3299 }
3300 };
3301 "#;
3302 let result = extract_config_string_or_array(
3303 source,
3304 &js_path(),
3305 &["build", "rollupOptions", "input"],
3306 );
3307 assert_eq!(result, vec!["./index.html", "./about.html"]);
3308 }
3309
3310 #[test]
3311 fn string_or_array_template_literal() {
3312 let source = r"export default { entry: `./src/index.js` };";
3313 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3314 assert_eq!(result, vec!["./src/index.js"]);
3315 }
3316
3317 #[test]
3318 fn string_or_array_object_path_helper_values() {
3319 let source = r#"
3320 import { resolve, join } from "node:path";
3321 import path from "node:path";
3322 export default {
3323 build: {
3324 rollupOptions: {
3325 input: {
3326 app: resolve(__dirname, "src/app.ts"),
3327 modal: path.resolve(__dirname, "src/modal.ts"),
3328 tabs: join(__dirname, "src/tabs.ts"),
3329 styles: resolve(__dirname, "src/index.css"),
3330 },
3331 },
3332 },
3333 };
3334 "#;
3335 let result = extract_config_string_or_array(
3336 source,
3337 &js_path(),
3338 &["build", "rollupOptions", "input"],
3339 );
3340 assert_eq!(
3341 result,
3342 vec!["src/app.ts", "src/modal.ts", "src/tabs.ts", "src/index.css"]
3343 );
3344 }
3345
3346 #[test]
3347 fn string_or_array_array_path_helper_values() {
3348 let source = r#"
3349 import { resolve } from "node:path";
3350 export default {
3351 build: {
3352 rollupOptions: {
3353 input: [resolve(__dirname, "src/a.ts"), "./src/b.ts"],
3354 },
3355 },
3356 };
3357 "#;
3358 let result = extract_config_string_or_array(
3359 source,
3360 &js_path(),
3361 &["build", "rollupOptions", "input"],
3362 );
3363 assert_eq!(result, vec!["src/a.ts", "./src/b.ts"]);
3364 }
3365
3366 #[test]
3367 fn string_or_array_top_level_path_helper_call() {
3368 let source = r#"
3369 import { resolve } from "node:path";
3370 export default { build: { lib: { entry: resolve(__dirname, "src/index.ts") } } };
3371 "#;
3372 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
3373 assert_eq!(result, vec!["src/index.ts"]);
3374 }
3375
3376 #[test]
3377 fn string_or_array_import_meta_dirname_anchor() {
3378 let source = r#"
3379 import { resolve } from "node:path";
3380 export default {
3381 build: { lib: { entry: resolve(import.meta.dirname, "src/index.ts") } },
3382 };
3383 "#;
3384 let result = extract_config_string_or_array(source, &ts_path(), &["build", "lib", "entry"]);
3385 assert_eq!(result, vec!["src/index.ts"]);
3386 }
3387
3388 #[test]
3389 fn string_or_array_non_literal_path_helper_args_dropped() {
3390 let source = r#"
3391 import { resolve } from "node:path";
3392 export default { build: { lib: { entry: resolve(baseDir, "src/index.ts") } } };
3393 "#;
3394 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
3395 assert!(
3396 result.is_empty(),
3397 "non-literal path-helper args must be dropped: {result:?}"
3398 );
3399 }
3400
3401 #[test]
3402 fn require_strings_array() {
3403 let source = r"
3404 module.exports = {
3405 plugins: [
3406 require('autoprefixer'),
3407 require('postcss-import')
3408 ]
3409 };
3410 ";
3411 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3412 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
3413 }
3414
3415 #[test]
3416 fn require_strings_with_tuples() {
3417 let source = r"
3418 module.exports = {
3419 plugins: [
3420 require('autoprefixer'),
3421 [require('postcss-preset-env'), { stage: 3 }]
3422 ]
3423 };
3424 ";
3425 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3426 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
3427 }
3428
3429 #[test]
3430 fn require_strings_empty_array() {
3431 let source = r"module.exports = { plugins: [] };";
3432 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3433 assert!(deps.is_empty());
3434 }
3435
3436 #[test]
3437 fn require_strings_no_require_calls() {
3438 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
3439 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3440 assert!(deps.is_empty());
3441 }
3442
3443 #[test]
3444 fn extract_aliases_from_object_with_file_url_to_path() {
3445 let source = r#"
3446 import { defineConfig } from 'vite';
3447 import { fileURLToPath, URL } from 'node:url';
3448
3449 export default defineConfig({
3450 resolve: {
3451 alias: {
3452 "@": fileURLToPath(new URL("./src", import.meta.url))
3453 }
3454 }
3455 });
3456 "#;
3457
3458 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
3459 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
3460 }
3461
3462 #[test]
3463 fn extract_aliases_from_array_form() {
3464 let source = r#"
3465 export default {
3466 resolve: {
3467 alias: [
3468 { find: "@", replacement: "./src" },
3469 { find: "$utils", replacement: "src/lib/utils" }
3470 ]
3471 }
3472 };
3473 "#;
3474
3475 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
3476 assert_eq!(
3477 aliases,
3478 vec![
3479 ("@".to_string(), "./src".to_string()),
3480 ("$utils".to_string(), "src/lib/utils".to_string())
3481 ]
3482 );
3483 }
3484
3485 #[test]
3486 fn extract_aliases_from_object_with_array_values() {
3487 let source = r#"
3488 ({
3489 compilerOptions: {
3490 paths: {
3491 "@/*": ["./src/*"],
3492 "@shared/*": ["./shared/*", "./fallback/*"]
3493 }
3494 }
3495 })
3496 "#;
3497
3498 let aliases = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
3499 assert_eq!(
3500 aliases,
3501 vec![
3502 ("@/*".to_string(), "./src/*".to_string()),
3503 ("@shared/*".to_string(), "./shared/*".to_string())
3504 ]
3505 );
3506 }
3507
3508 #[test]
3509 fn extract_array_object_strings_mixed_forms() {
3510 let source = r#"
3511 export default {
3512 components: [
3513 "~/components",
3514 { path: "@/feature-components" }
3515 ]
3516 };
3517 "#;
3518
3519 let values =
3520 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
3521 assert_eq!(
3522 values,
3523 vec![
3524 "~/components".to_string(),
3525 "@/feature-components".to_string()
3526 ]
3527 );
3528 }
3529
3530 #[test]
3531 fn extract_config_plugin_option_string_from_json() {
3532 let source = r#"{
3533 "expo": {
3534 "plugins": [
3535 ["expo-router", { "root": "src/app" }]
3536 ]
3537 }
3538 }"#;
3539
3540 let value = extract_config_plugin_option_string(
3541 source,
3542 &json_path(),
3543 &["expo", "plugins"],
3544 "expo-router",
3545 "root",
3546 );
3547
3548 assert_eq!(value, Some("src/app".to_string()));
3549 }
3550
3551 #[test]
3552 fn extract_config_plugin_option_string_from_top_level_plugins() {
3553 let source = r#"{
3554 "plugins": [
3555 ["expo-router", { "root": "./src/routes" }]
3556 ]
3557 }"#;
3558
3559 let value = extract_config_plugin_option_string_from_paths(
3560 source,
3561 &json_path(),
3562 &[&["plugins"], &["expo", "plugins"]],
3563 "expo-router",
3564 "root",
3565 );
3566
3567 assert_eq!(value, Some("./src/routes".to_string()));
3568 }
3569
3570 #[test]
3571 fn extract_config_plugin_option_string_from_ts_config() {
3572 let source = r"
3573 export default {
3574 expo: {
3575 plugins: [
3576 ['expo-router', { root: './src/app' }]
3577 ]
3578 }
3579 };
3580 ";
3581
3582 let value = extract_config_plugin_option_string(
3583 source,
3584 &ts_path(),
3585 &["expo", "plugins"],
3586 "expo-router",
3587 "root",
3588 );
3589
3590 assert_eq!(value, Some("./src/app".to_string()));
3591 }
3592
3593 #[test]
3594 fn extract_config_plugin_option_string_returns_none_when_plugin_missing() {
3595 let source = r#"{
3596 "expo": {
3597 "plugins": [
3598 ["expo-font", {}]
3599 ]
3600 }
3601 }"#;
3602
3603 let value = extract_config_plugin_option_string(
3604 source,
3605 &json_path(),
3606 &["expo", "plugins"],
3607 "expo-router",
3608 "root",
3609 );
3610
3611 assert_eq!(value, None);
3612 }
3613
3614 #[test]
3615 fn vite_react_babel_dependencies_extract_plain_tuple_and_prefixed_entries() {
3616 let source = r#"
3617 import react from "@vitejs/plugin-react";
3618
3619 export default defineConfig({
3620 plugins: [
3621 react({
3622 babel: {
3623 plugins: [
3624 "babel-plugin-plain",
3625 ["module:@preact/signals-react-transform", { mode: "auto" }],
3626 ],
3627 presets: [["@babel/preset-react", { runtime: "automatic" }]],
3628 },
3629 }),
3630 ],
3631 });
3632 "#;
3633
3634 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3635
3636 assert_eq!(
3637 deps,
3638 vec![
3639 "babel-plugin-plain".to_string(),
3640 "@preact/signals-react-transform".to_string(),
3641 "@babel/preset-react".to_string(),
3642 ]
3643 );
3644 }
3645
3646 #[test]
3647 fn vite_react_babel_dependencies_support_default_alias_import() {
3648 let source = r#"
3649 import { default as viteReact } from "@vitejs/plugin-react";
3650
3651 export default {
3652 plugins: [
3653 viteReact({
3654 babel: {
3655 plugins: [["module:@scope/pkg/plugin", {}]],
3656 },
3657 }),
3658 ],
3659 };
3660 "#;
3661
3662 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3663
3664 assert_eq!(deps, vec!["@scope/pkg".to_string()]);
3665 }
3666
3667 #[test]
3668 fn vite_react_babel_dependencies_ignore_unrelated_plugin_calls() {
3669 let source = r#"
3670 import vue from "@vitejs/plugin-vue";
3671
3672 export default {
3673 plugins: [
3674 vue({
3675 babel: {
3676 plugins: ["@preact/signals-react-transform"],
3677 },
3678 }),
3679 ],
3680 };
3681 "#;
3682
3683 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3684
3685 assert!(deps.is_empty());
3686 }
3687
3688 #[test]
3689 fn vite_react_babel_dependencies_skip_relative_and_protocol_entries() {
3690 let source = r#"
3691 import react from "@vitejs/plugin-react";
3692
3693 export default {
3694 plugins: [
3695 react({
3696 babel: {
3697 plugins: ["./local-plugin", "module:./local-prefixed", "http://example.com/plugin"],
3698 },
3699 }),
3700 ],
3701 };
3702 "#;
3703
3704 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3705
3706 assert!(deps.is_empty());
3707 }
3708
3709 #[test]
3710 fn normalize_config_path_relative_to_root() {
3711 let config_path = PathBuf::from("/project/vite.config.ts");
3712 let root = PathBuf::from("/project");
3713
3714 assert_eq!(
3715 normalize_config_path("./src/lib", &config_path, &root),
3716 Some("src/lib".to_string())
3717 );
3718 assert_eq!(
3719 normalize_config_path("/src/lib", &config_path, &root),
3720 Some("src/lib".to_string())
3721 );
3722 }
3723
3724 #[test]
3725 fn normalize_config_path_mixed_separators_and_parent_dirs() {
3726 let config_path = PathBuf::from("/project/config/vite.config.ts");
3727 let root = PathBuf::from("/project");
3728
3729 assert_eq!(
3730 normalize_config_path(".\\src\\..\\app\\lib", &config_path, &root),
3731 Some("config/app/lib".to_string())
3732 );
3733 }
3734
3735 #[test]
3738 fn normalize_config_path_reads_an_absolute_path_under_root_as_absolute() {
3739 let root = std::env::temp_dir().join("fallow-2806-project");
3740 let config_path = root.join("vite.config.ts");
3741 let absolute = path_to_config_string(&root.join("src").join("lib"));
3742
3743 assert_eq!(
3744 normalize_config_path(&absolute, &config_path, &root),
3745 Some("src/lib".to_string())
3746 );
3747 assert_eq!(
3748 normalize_filesystem_config_path(&absolute, &config_path, &root),
3749 Some("src/lib".to_string())
3750 );
3751 }
3752
3753 #[cfg(unix)]
3758 #[test]
3759 fn normalize_config_path_keeps_the_vite_root_in_root_reading() {
3760 let temp = tempfile::tempdir().expect("temp dir");
3761 let root = temp.path().join("src");
3762 std::fs::create_dir_all(&root).expect("root dir");
3763 let config_path = root.join("vite.config.ts");
3764 let root_string = path_to_config_string(&root);
3765 let nested = root_string.trim_start_matches('/').to_string();
3766 let under = format!("{root_string}/lib");
3767
3768 assert_eq!(
3769 normalize_config_path(&root_string, &config_path, &root),
3770 Some(nested.clone())
3771 );
3772 assert_eq!(
3773 normalize_config_path(&under, &config_path, &root),
3774 Some("lib".to_string())
3775 );
3776
3777 std::fs::create_dir_all(root.join(&nested)).expect("root-in-root dir");
3778 assert_eq!(
3779 normalize_config_path(&under, &config_path, &root),
3780 Some(format!("{nested}/lib"))
3781 );
3782 assert_eq!(
3783 normalize_filesystem_config_path(&under, &config_path, &root),
3784 Some("lib".to_string())
3785 );
3786 }
3787
3788 #[test]
3791 fn normalize_filesystem_config_path_does_not_read_a_leading_slash_as_root_relative() {
3792 let config_path = PathBuf::from("/project/config/webpack.config.js");
3793 let root = PathBuf::from("/project");
3794
3795 assert_eq!(
3796 normalize_filesystem_config_path("/src/lib", &config_path, &root),
3797 None
3798 );
3799 assert_eq!(
3800 normalize_filesystem_config_path("./src/lib", &config_path, &root),
3801 Some("config/src/lib".to_string())
3802 );
3803 assert_eq!(
3804 normalize_config_path("/src/lib", &config_path, &root),
3805 Some("src/lib".to_string())
3806 );
3807 }
3808
3809 #[test]
3810 fn normalize_config_path_leading_slash_stays_project_relative() {
3811 let config_path = PathBuf::from("/project/vite.config.ts");
3812 let root = PathBuf::from("/project");
3813
3814 assert_eq!(
3815 normalize_config_path("/src\\lib", &config_path, &root),
3816 Some("src/lib".to_string())
3817 );
3818 }
3819
3820 #[test]
3821 fn json_wrapped_in_parens_string() {
3822 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
3823 let val = extract_config_string(source, &js_path(), &["extends"]);
3824 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
3825 }
3826
3827 #[test]
3828 fn json_wrapped_in_parens_nested_array() {
3829 let source =
3830 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
3831 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
3832 assert_eq!(types, vec!["node", "jest"]);
3833
3834 let include = extract_config_string_array(source, &js_path(), &["include"]);
3835 assert_eq!(include, vec!["src/**/*"]);
3836 }
3837
3838 #[test]
3839 fn json_wrapped_in_parens_object_keys() {
3840 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
3841 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3842 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
3843 }
3844
3845 fn json_path() -> PathBuf {
3846 PathBuf::from("config.json")
3847 }
3848
3849 #[test]
3850 fn json_file_parsed_correctly() {
3851 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
3852 let val = extract_config_string(source, &json_path(), &["key"]);
3853 assert_eq!(val, Some("value".to_string()));
3854
3855 let list = extract_config_string_array(source, &json_path(), &["list"]);
3856 assert_eq!(list, vec!["a", "b"]);
3857 }
3858
3859 #[test]
3860 fn jsonc_file_parsed_correctly() {
3861 let source = r#"{"key": "value"}"#;
3862 let path = PathBuf::from("tsconfig.jsonc");
3863 let val = extract_config_string(source, &path, &["key"]);
3864 assert_eq!(val, Some("value".to_string()));
3865 }
3866
3867 #[test]
3868 fn extract_define_config_arrow_function() {
3869 let source = r#"
3870 import { defineConfig } from 'vite';
3871 export default defineConfig(() => ({
3872 test: {
3873 include: ["**/*.test.ts"]
3874 }
3875 }));
3876 "#;
3877 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
3878 assert_eq!(include, vec!["**/*.test.ts"]);
3879 }
3880
3881 #[test]
3885 fn extract_define_config_block_body_arrow_function() {
3886 let source = r#"
3887 import { defineConfig } from 'vite';
3888 export default defineConfig(({ mode }) => {
3889 const isProduction = mode === 'production';
3890 return {
3891 test: {
3892 environment: "jsdom",
3893 setupFiles: "./tests/setup.ts"
3894 },
3895 base: isProduction ? '/app/' : '/'
3896 };
3897 });
3898 "#;
3899 assert_eq!(
3900 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
3901 Some("jsdom")
3902 );
3903 assert_eq!(
3904 extract_config_string(source, &ts_path(), &["test", "setupFiles"]).as_deref(),
3905 Some("./tests/setup.ts")
3906 );
3907 }
3908
3909 #[test]
3910 fn extract_define_config_function_expression() {
3911 let source = r#"
3912 import { defineConfig } from 'vite';
3913 export default defineConfig(function () {
3914 return { test: { environment: "happy-dom" } };
3915 });
3916 "#;
3917 assert_eq!(
3918 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
3919 Some("happy-dom")
3920 );
3921 }
3922
3923 #[test]
3927 fn wrapper_options_object_does_not_shadow_named_config_arg() {
3928 let source = r#"
3929 const nextConfig = { pageExtensions: ["page.tsx"] };
3930 module.exports = withSentryConfig(nextConfig, { org: "o", project: "p", silent: true });
3931 "#;
3932 assert_eq!(
3933 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
3934 vec!["page.tsx"]
3935 );
3936 }
3937
3938 #[test]
3939 fn wrapper_options_object_does_not_shadow_named_config_arg_esm() {
3940 let source = r#"
3941 const nextConfig = { pageExtensions: ["page.tsx"] };
3942 export default withSentryConfig(nextConfig, { org: "o", project: "p" });
3943 "#;
3944 assert_eq!(
3945 extract_config_string_array(source, &ts_path(), &["pageExtensions"]),
3946 vec!["page.tsx"]
3947 );
3948 }
3949
3950 #[test]
3952 fn merge_config_extracts_nested_define_config_object() {
3953 let source = r#"
3954 import { defineConfig, mergeConfig } from 'vitest/config';
3955 import viteConfig from './vite.config';
3956 export default mergeConfig(viteConfig, defineConfig({
3957 test: { environment: "jsdom" }
3958 }));
3959 "#;
3960 assert_eq!(
3961 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
3962 Some("jsdom")
3963 );
3964 }
3965
3966 #[test]
3967 fn define_config_wrapping_merge_config_extracts_object() {
3968 let source = r#"
3969 import { defineConfig, mergeConfig } from 'vitest/config';
3970 import base from './base';
3971 export default defineConfig(mergeConfig(base, { test: { environment: "jsdom" } }));
3972 "#;
3973 assert_eq!(
3974 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
3975 Some("jsdom")
3976 );
3977 }
3978
3979 #[test]
3982 fn inline_object_at_argument_zero_still_wins() {
3983 let source = r#"
3984 const unrelated = { pageExtensions: ["wrong.tsx"] };
3985 module.exports = withSentryConfig({ pageExtensions: ["right.tsx"] }, { org: "o" });
3986 "#;
3987 assert_eq!(
3988 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
3989 vec!["right.tsx"]
3990 );
3991 }
3992
3993 #[test]
3997 fn conditional_config_callback_extracts_branch_return() {
3998 let source = r#"
3999 import { defineConfig } from 'vite';
4000 export default defineConfig(({ command }) => {
4001 if (command === "serve") {
4002 return { test: { environment: "jsdom" } };
4003 } else {
4004 return { test: { environment: "node" } };
4005 }
4006 });
4007 "#;
4008 assert_eq!(
4009 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4010 Some("jsdom"),
4011 "with no top-level return, the first branch in source order wins"
4012 );
4013 }
4014
4015 #[test]
4016 fn try_block_return_is_extracted() {
4017 let source = r#"
4018 export default (() => {
4019 try {
4020 return { test: { environment: "jsdom" } };
4021 } catch (e) {
4022 return { test: { environment: "node" } };
4023 }
4024 });
4025 "#;
4026 assert_eq!(
4027 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4028 Some("jsdom")
4029 );
4030 }
4031
4032 #[test]
4033 fn switch_case_return_is_extracted() {
4034 let source = r#"
4035 import { defineConfig } from 'vite';
4036 export default defineConfig(({ mode }) => {
4037 switch (mode) {
4038 case "test":
4039 return { test: { environment: "jsdom" } };
4040 default:
4041 return { test: { environment: "node" } };
4042 }
4043 });
4044 "#;
4045 assert_eq!(
4046 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4047 Some("jsdom")
4048 );
4049 }
4050
4051 #[test]
4055 fn guard_clause_does_not_shadow_the_top_level_return() {
4056 let source = r#"
4057 import { defineConfig } from 'vite';
4058 export default defineConfig(({ mode }) => {
4059 if (!mode) {
4060 return {};
4061 }
4062 return { test: { environment: "jsdom" } };
4063 });
4064 "#;
4065 assert_eq!(
4066 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4067 Some("jsdom"),
4068 "a return at the callback's own level is the main config"
4069 );
4070 }
4071
4072 #[test]
4075 fn else_if_chain_does_not_shadow_the_top_level_return() {
4076 let source = r#"
4077 import { defineConfig } from 'vite';
4078 export default defineConfig(({ mode }) => {
4079 if (mode === "a") {
4080 return { base: "/a/" };
4081 } else if (mode === "b") {
4082 return { base: "/b/" };
4083 }
4084 return { test: { environment: "jsdom" } };
4085 });
4086 "#;
4087 assert_eq!(
4088 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4089 Some("jsdom")
4090 );
4091 }
4092
4093 #[test]
4094 fn extract_config_from_default_export_function_declaration() {
4095 let source = r#"
4096 export default function createConfig() {
4097 return {
4098 clientModules: ["./src/client/global.js"]
4099 };
4100 }
4101 "#;
4102
4103 let client_modules = extract_config_string_array(source, &ts_path(), &["clientModules"]);
4104 assert_eq!(client_modules, vec!["./src/client/global.js"]);
4105 }
4106
4107 #[test]
4108 fn extract_config_from_default_export_async_function_declaration() {
4109 let source = r#"
4110 export default async function createConfigAsync() {
4111 return {
4112 docs: {
4113 path: "knowledge"
4114 }
4115 };
4116 }
4117 "#;
4118
4119 let docs_path = extract_config_string(source, &ts_path(), &["docs", "path"]);
4120 assert_eq!(docs_path, Some("knowledge".to_string()));
4121 }
4122
4123 #[test]
4124 fn extract_config_from_exported_arrow_function_identifier() {
4125 let source = r#"
4126 const config = async () => {
4127 return {
4128 themes: ["classic"]
4129 };
4130 };
4131
4132 export default config;
4133 "#;
4134
4135 let themes = extract_config_shallow_strings(source, &ts_path(), "themes");
4136 assert_eq!(themes, vec!["classic"]);
4137 }
4138
4139 #[test]
4140 fn module_exports_nested_string() {
4141 let source = r#"
4142 module.exports = {
4143 resolve: {
4144 alias: {
4145 "@": "./src"
4146 }
4147 }
4148 };
4149 "#;
4150 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
4151 assert_eq!(val, Some("./src".to_string()));
4152 }
4153
4154 #[test]
4155 fn property_strings_nested_objects() {
4156 let source = r#"
4157 export default {
4158 plugins: {
4159 group1: { a: "val-a" },
4160 group2: { b: "val-b" }
4161 }
4162 };
4163 "#;
4164 let values = extract_config_property_strings(source, &js_path(), "plugins");
4165 assert!(values.contains(&"val-a".to_string()));
4166 assert!(values.contains(&"val-b".to_string()));
4167 }
4168
4169 #[test]
4170 fn property_strings_missing_key_returns_empty() {
4171 let source = r#"export default { other: "value" };"#;
4172 let values = extract_config_property_strings(source, &js_path(), "missing");
4173 assert!(values.is_empty());
4174 }
4175
4176 #[test]
4177 fn shallow_strings_tuple_array() {
4178 let source = r#"
4179 module.exports = {
4180 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
4181 };
4182 "#;
4183 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
4184 assert_eq!(values, vec!["default", "jest-junit"]);
4185 assert!(!values.contains(&"reports".to_string()));
4186 }
4187
4188 #[test]
4189 fn shallow_strings_single_string() {
4190 let source = r#"export default { preset: "ts-jest" };"#;
4191 let values = extract_config_shallow_strings(source, &js_path(), "preset");
4192 assert_eq!(values, vec!["ts-jest"]);
4193 }
4194
4195 #[test]
4196 fn shallow_strings_missing_key() {
4197 let source = r#"export default { other: "val" };"#;
4198 let values = extract_config_shallow_strings(source, &js_path(), "missing");
4199 assert!(values.is_empty());
4200 }
4201
4202 #[test]
4203 fn shallow_strings_or_object_property_alias_objects() {
4204 let source = r#"
4205 export default {
4206 jsPlugins: [
4207 "eslint-plugin-playwright",
4208 ["eslint-plugin-regexp", { rules: {} }],
4209 { name: "short", specifier: "eslint-plugin-with-long-name" }
4210 ]
4211 };
4212 "#;
4213 let values = extract_config_shallow_strings_or_object_property(
4214 source,
4215 &ts_path(),
4216 "jsPlugins",
4217 "specifier",
4218 );
4219 assert_eq!(
4220 values,
4221 vec![
4222 "eslint-plugin-playwright",
4223 "eslint-plugin-regexp",
4224 "eslint-plugin-with-long-name"
4225 ]
4226 );
4227 }
4228
4229 #[test]
4230 fn nested_shallow_strings_vitest_reporters() {
4231 let source = r#"
4232 export default {
4233 test: {
4234 reporters: ["default", "vitest-sonar-reporter"]
4235 }
4236 };
4237 "#;
4238 let values =
4239 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4240 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
4241 }
4242
4243 #[test]
4244 fn nested_shallow_strings_tuple_format() {
4245 let source = r#"
4246 export default {
4247 test: {
4248 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
4249 }
4250 };
4251 "#;
4252 let values =
4253 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4254 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
4255 }
4256
4257 #[test]
4258 fn nested_shallow_strings_missing_outer() {
4259 let source = r"export default { other: {} };";
4260 let values =
4261 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4262 assert!(values.is_empty());
4263 }
4264
4265 #[test]
4266 fn nested_shallow_strings_missing_inner() {
4267 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
4268 let values =
4269 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4270 assert!(values.is_empty());
4271 }
4272
4273 #[test]
4274 fn string_or_array_missing_path() {
4275 let source = r"export default {};";
4276 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
4277 assert!(result.is_empty());
4278 }
4279
4280 #[test]
4281 fn string_or_array_non_string_values() {
4282 let source = r"export default { entry: [42, true] };";
4283 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
4284 assert!(result.is_empty());
4285 }
4286
4287 #[test]
4288 fn array_nested_extraction() {
4289 let source = r#"
4290 export default defineConfig({
4291 test: {
4292 projects: [
4293 {
4294 test: {
4295 setupFiles: ["./test/setup-a.ts"]
4296 }
4297 },
4298 {
4299 test: {
4300 setupFiles: "./test/setup-b.ts"
4301 }
4302 }
4303 ]
4304 }
4305 });
4306 "#;
4307 let results = extract_config_array_nested_string_or_array(
4308 source,
4309 &ts_path(),
4310 &["test", "projects"],
4311 &["test", "setupFiles"],
4312 );
4313 assert!(results.contains(&"./test/setup-a.ts".to_string()));
4314 assert!(results.contains(&"./test/setup-b.ts".to_string()));
4315 }
4316
4317 #[test]
4318 fn array_nested_empty_when_no_array() {
4319 let source = r#"export default { test: { projects: "not-an-array" } };"#;
4320 let results = extract_config_array_nested_string_or_array(
4321 source,
4322 &js_path(),
4323 &["test", "projects"],
4324 &["test", "setupFiles"],
4325 );
4326 assert!(results.is_empty());
4327 }
4328
4329 #[test]
4330 fn object_nested_extraction() {
4331 let source = r#"{
4332 "projects": {
4333 "app-one": {
4334 "architect": {
4335 "build": {
4336 "options": {
4337 "styles": ["src/styles.css"]
4338 }
4339 }
4340 }
4341 }
4342 }
4343 }"#;
4344 let results = extract_config_object_nested_string_or_array(
4345 source,
4346 &json_path(),
4347 &["projects"],
4348 &["architect", "build", "options", "styles"],
4349 );
4350 assert_eq!(results, vec!["src/styles.css"]);
4351 }
4352
4353 #[test]
4354 fn array_with_object_input_form_extracted() {
4355 let source = r#"{
4356 "projects": {
4357 "app": {
4358 "architect": {
4359 "build": {
4360 "options": {
4361 "styles": [
4362 "src/styles.scss",
4363 { "input": "src/theme.scss", "bundleName": "theme", "inject": false },
4364 { "bundleName": "lazy-only" }
4365 ]
4366 }
4367 }
4368 }
4369 }
4370 }
4371 }"#;
4372 let results = extract_config_object_nested_string_or_array(
4373 source,
4374 &json_path(),
4375 &["projects"],
4376 &["architect", "build", "options", "styles"],
4377 );
4378 assert!(
4379 results.contains(&"src/styles.scss".to_string()),
4380 "string form must still work: {results:?}"
4381 );
4382 assert!(
4383 results.contains(&"src/theme.scss".to_string()),
4384 "object form with `input` must be extracted: {results:?}"
4385 );
4386 assert!(
4387 !results.contains(&"lazy-only".to_string()),
4388 "bundleName must not be misinterpreted as a path: {results:?}"
4389 );
4390 assert!(
4391 !results.contains(&"theme".to_string()),
4392 "bundleName from full object must not leak: {results:?}"
4393 );
4394 }
4395
4396 #[test]
4397 fn object_nested_strings_extraction() {
4398 let source = r#"{
4399 "targets": {
4400 "build": {
4401 "executor": "@angular/build:application"
4402 },
4403 "test": {
4404 "executor": "@nx/vite:test"
4405 }
4406 }
4407 }"#;
4408 let results =
4409 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
4410 assert!(results.contains(&"@angular/build:application".to_string()));
4411 assert!(results.contains(&"@nx/vite:test".to_string()));
4412 }
4413
4414 #[test]
4415 fn require_strings_direct_call() {
4416 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
4417 let deps = extract_config_require_strings(source, &js_path(), "adapter");
4418 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
4419 }
4420
4421 #[test]
4422 fn require_strings_no_matching_key() {
4423 let source = r"module.exports = { other: require('something') };";
4424 let deps = extract_config_require_strings(source, &js_path(), "plugins");
4425 assert!(deps.is_empty());
4426 }
4427
4428 #[test]
4429 fn extract_imports_no_imports() {
4430 let source = r"export default {};";
4431 let imports = extract_imports(source, &js_path());
4432 assert!(imports.is_empty());
4433 }
4434
4435 #[test]
4436 fn extract_imports_side_effect_import() {
4437 let source = r"
4438 import 'polyfill';
4439 import './local-setup';
4440 export default {};
4441 ";
4442 let imports = extract_imports(source, &js_path());
4443 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
4444 }
4445
4446 #[test]
4447 fn extract_imports_mixed_specifiers() {
4448 let source = r"
4449 import defaultExport from 'module-a';
4450 import { named } from 'module-b';
4451 import * as ns from 'module-c';
4452 export default {};
4453 ";
4454 let imports = extract_imports(source, &js_path());
4455 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
4456 }
4457
4458 #[test]
4459 fn template_literal_in_string_or_array() {
4460 let source = r"export default { entry: `./src/index.ts` };";
4461 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
4462 assert_eq!(result, vec!["./src/index.ts"]);
4463 }
4464
4465 #[test]
4466 fn template_literal_in_config_string() {
4467 let source = r"export default { testDir: `./tests` };";
4468 let val = extract_config_string(source, &js_path(), &["testDir"]);
4469 assert_eq!(val, Some("./tests".to_string()));
4470 }
4471
4472 #[test]
4473 fn template_literal_command_recovers_static_command_tokens() {
4474 let source = r"
4475 const PORT = 3000;
4476 export default {
4477 webServer: {
4478 command: `pnpm exec srvx --port ${PORT} --hostname 127.0.0.1`
4479 }
4480 };
4481 ";
4482 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
4483 assert_eq!(
4484 val,
4485 Some("pnpm exec srvx --port --hostname 127.0.0.1".to_string())
4486 );
4487 }
4488
4489 #[test]
4490 fn template_literal_command_skips_dynamic_prefix() {
4491 let source = r"
4492 export default {
4493 webServer: { command: `${serverCommand} && pnpm exec srvx` }
4494 };
4495 ";
4496 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
4497 assert!(val.is_none());
4498 }
4499
4500 #[test]
4501 fn template_literal_command_skips_split_static_token() {
4502 let source = r"
4503 export default {
4504 webServer: { command: `pnpm exec sr${part}vx --port 3000` }
4505 };
4506 ";
4507 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
4508 assert!(val.is_none());
4509 }
4510
4511 #[test]
4512 fn array_object_command_pairs_recover_template_command() {
4513 let source = r"
4514 const PORT = 3000;
4515 export default {
4516 webServer: [
4517 {
4518 command: `pnpm exec srvx --port ${PORT}`,
4519 cwd: 'apps/web'
4520 }
4521 ]
4522 };
4523 ";
4524 let pairs = extract_config_array_object_command_pairs(
4525 source,
4526 &ts_path(),
4527 &["webServer"],
4528 "command",
4529 "cwd",
4530 );
4531 assert_eq!(
4532 pairs,
4533 vec![(
4534 "pnpm exec srvx --port ".to_string(),
4535 Some("apps/web".to_string())
4536 )]
4537 );
4538 }
4539
4540 #[test]
4541 fn nested_string_array_empty_path() {
4542 let source = r#"export default { items: ["a", "b"] };"#;
4543 let result = extract_config_string_array(source, &js_path(), &[]);
4544 assert!(result.is_empty());
4545 }
4546
4547 #[test]
4548 fn nested_string_empty_path() {
4549 let source = r#"export default { key: "val" };"#;
4550 let result = extract_config_string(source, &js_path(), &[]);
4551 assert!(result.is_none());
4552 }
4553
4554 #[test]
4555 fn object_keys_empty_path() {
4556 let source = r"export default { plugins: {} };";
4557 let result = extract_config_object_keys(source, &js_path(), &[]);
4558 assert!(result.is_empty());
4559 }
4560
4561 #[test]
4562 fn no_config_object_returns_empty() {
4563 let source = r"const x = 42;";
4564 let result = extract_config_string(source, &js_path(), &["key"]);
4565 assert!(result.is_none());
4566
4567 let arr = extract_config_string_array(source, &js_path(), &["items"]);
4568 assert!(arr.is_empty());
4569
4570 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
4571 assert!(keys.is_empty());
4572 }
4573
4574 #[test]
4575 fn property_with_string_key() {
4576 let source = r#"export default { "string-key": "value" };"#;
4577 let val = extract_config_string(source, &js_path(), &["string-key"]);
4578 assert_eq!(val, Some("value".to_string()));
4579 }
4580
4581 #[test]
4582 fn nested_navigation_through_non_object() {
4583 let source = r#"export default { level1: "not-an-object" };"#;
4584 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
4585 assert!(val.is_none());
4586 }
4587
4588 #[test]
4589 fn variable_reference_untyped() {
4590 let source = r#"
4591 const config = {
4592 testDir: "./tests"
4593 };
4594 export default config;
4595 "#;
4596 let val = extract_config_string(source, &js_path(), &["testDir"]);
4597 assert_eq!(val, Some("./tests".to_string()));
4598 }
4599
4600 #[test]
4601 fn variable_reference_to_an_exported_const() {
4602 for source in [
4603 r#"
4604 export const config = { testDir: "./tests" };
4605 export default config;
4606 "#,
4607 r#"
4608 export const config = { testDir: "./tests" };
4609 module.exports = config;
4610 "#,
4611 ] {
4612 let val = extract_config_string(source, &js_path(), &["testDir"]);
4613 assert_eq!(val, Some("./tests".to_string()), "source: {source}");
4614 }
4615 }
4616
4617 #[test]
4618 fn variable_reference_with_type_annotation() {
4619 let source = r#"
4620 import type { StorybookConfig } from '@storybook/react-vite';
4621 const config: StorybookConfig = {
4622 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
4623 framework: "@storybook/react-vite"
4624 };
4625 export default config;
4626 "#;
4627 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
4628 assert_eq!(
4629 addons,
4630 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
4631 );
4632
4633 let framework = extract_config_string(source, &ts_path(), &["framework"]);
4634 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
4635 }
4636
4637 #[test]
4638 fn variable_reference_with_define_config() {
4639 let source = r#"
4640 import { defineConfig } from 'vitest/config';
4641 const config = defineConfig({
4642 test: {
4643 include: ["**/*.test.ts"]
4644 }
4645 });
4646 export default config;
4647 "#;
4648 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
4649 assert_eq!(include, vec!["**/*.test.ts"]);
4650 }
4651
4652 #[test]
4653 fn ts_satisfies_direct_export() {
4654 let source = r#"
4655 export default {
4656 testDir: "./tests"
4657 } satisfies PlaywrightTestConfig;
4658 "#;
4659 let val = extract_config_string(source, &ts_path(), &["testDir"]);
4660 assert_eq!(val, Some("./tests".to_string()));
4661 }
4662
4663 #[test]
4664 fn ts_as_direct_export() {
4665 let source = r#"
4666 export default {
4667 testDir: "./tests"
4668 } as const;
4669 "#;
4670 let val = extract_config_string(source, &ts_path(), &["testDir"]);
4671 assert_eq!(val, Some("./tests".to_string()));
4672 }
4673
4674 fn aliases(source: &str) -> Vec<(String, String)> {
4677 extract_config_aliases(source, &js_path(), &["resolve", "alias"])
4678 }
4679
4680 #[test]
4681 fn aliases_inline_object_still_extracted() {
4682 let source = r#"
4684 export default defineConfig({
4685 resolve: { alias: { "@": "./src", utils: "../../utils" } }
4686 });
4687 "#;
4688 let mut got = aliases(source);
4689 got.sort();
4690 assert_eq!(
4691 got,
4692 vec![
4693 ("@".to_string(), "./src".to_string()),
4694 ("utils".to_string(), "../../utils".to_string()),
4695 ]
4696 );
4697 }
4698
4699 #[test]
4700 fn aliases_inline_array_still_extracted() {
4701 let source = r#"
4702 export default defineConfig({
4703 resolve: { alias: [{ find: "@", replacement: "./src" }] }
4704 });
4705 "#;
4706 assert_eq!(
4707 aliases(source),
4708 vec![("@".to_string(), "./src".to_string())]
4709 );
4710 }
4711
4712 #[test]
4713 fn aliases_local_const_array_identifier() {
4714 let source = r#"
4715 const sharedAliases = [{ find: "@", replacement: "./src" }];
4716 export default defineConfig({ resolve: { alias: sharedAliases } });
4717 "#;
4718 assert_eq!(
4719 aliases(source),
4720 vec![("@".to_string(), "./src".to_string())]
4721 );
4722 }
4723
4724 #[test]
4725 fn aliases_local_const_object_identifier() {
4726 let source = r#"
4727 const sharedAliases = { "@": "./src" };
4728 export default defineConfig({ resolve: { alias: sharedAliases } });
4729 "#;
4730 assert_eq!(
4731 aliases(source),
4732 vec![("@".to_string(), "./src".to_string())]
4733 );
4734 }
4735
4736 #[test]
4737 fn aliases_array_spread_of_identifiers_and_inline() {
4738 let source = r##"
4739 const a = [{ find: "@", replacement: "./src" }];
4740 const b = [{ find: "~", replacement: "./lib" }];
4741 export default defineConfig({
4742 resolve: { alias: [...a, ...b, { find: "#", replacement: "./test" }] }
4743 });
4744 "##;
4745 let mut got = aliases(source);
4746 got.sort();
4747 assert_eq!(
4748 got,
4749 vec![
4750 ("#".to_string(), "./test".to_string()),
4751 ("@".to_string(), "./src".to_string()),
4752 ("~".to_string(), "./lib".to_string()),
4753 ]
4754 );
4755 }
4756
4757 #[test]
4758 fn aliases_object_spread_of_identifier_and_inline() {
4759 let source = r#"
4760 const base = { "@": "./src" };
4761 export default defineConfig({
4762 resolve: { alias: { ...base, "~": "./lib" } }
4763 });
4764 "#;
4765 let mut got = aliases(source);
4766 got.sort();
4767 assert_eq!(
4768 got,
4769 vec![
4770 ("@".to_string(), "./src".to_string()),
4771 ("~".to_string(), "./lib".to_string()),
4772 ]
4773 );
4774 }
4775
4776 #[test]
4777 fn aliases_local_const_chained_identifier() {
4778 let source = r#"
4780 const real = [{ find: "@", replacement: "./src" }];
4781 const alias2 = real;
4782 export default defineConfig({ resolve: { alias: alias2 } });
4783 "#;
4784 assert_eq!(
4785 aliases(source),
4786 vec![("@".to_string(), "./src".to_string())]
4787 );
4788 }
4789
4790 #[test]
4791 fn aliases_imported_named_identifier_from_sibling() {
4792 let dir = tempfile::tempdir().unwrap();
4793 std::fs::write(
4794 dir.path().join("vite.shared.js"),
4795 r#"export const sharedAliases = [
4796 { find: "@", replacement: new URL("./src", import.meta.url).pathname },
4797 ];"#,
4798 )
4799 .unwrap();
4800 let config = dir.path().join("vite.config.js");
4801 let source = r#"
4802 import { defineConfig } from "vite";
4803 import { sharedAliases } from "./vite.shared.js";
4804 export default defineConfig({ resolve: { alias: sharedAliases } });
4805 "#;
4806 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4807 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4808 }
4809
4810 #[test]
4811 fn aliases_imported_extensionless_specifier_probed() {
4812 let dir = tempfile::tempdir().unwrap();
4813 std::fs::write(
4814 dir.path().join("aliases.mjs"),
4815 r#"export const sharedAliases = { "@": "./src" };"#,
4816 )
4817 .unwrap();
4818 let config = dir.path().join("vite.config.ts");
4819 let source = r#"
4820 import { sharedAliases } from "./aliases";
4821 export default defineConfig({ resolve: { alias: sharedAliases } });
4822 "#;
4823 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4824 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4825 }
4826
4827 #[test]
4828 fn aliases_imported_default_export_from_sibling() {
4829 let dir = tempfile::tempdir().unwrap();
4830 std::fs::write(
4831 dir.path().join("aliases.js"),
4832 r#"export default [{ find: "@", replacement: "./src" }];"#,
4833 )
4834 .unwrap();
4835 let config = dir.path().join("vite.config.js");
4836 let source = r#"
4837 import sharedAliases from "./aliases.js";
4838 export default defineConfig({ resolve: { alias: sharedAliases } });
4839 "#;
4840 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4841 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4842 }
4843
4844 #[test]
4845 fn aliases_imported_spread_from_two_siblings() {
4846 let dir = tempfile::tempdir().unwrap();
4847 std::fs::write(
4848 dir.path().join("a.js"),
4849 r#"export const a = [{ find: "@", replacement: "./src" }];"#,
4850 )
4851 .unwrap();
4852 std::fs::write(
4853 dir.path().join("b.js"),
4854 r#"export const b = [{ find: "~", replacement: "./lib" }];"#,
4855 )
4856 .unwrap();
4857 let config = dir.path().join("vite.config.js");
4858 let source = r#"
4859 import { a } from "./a.js";
4860 import { b } from "./b.js";
4861 export default defineConfig({ resolve: { alias: [...a, ...b] } });
4862 "#;
4863 let mut got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4864 got.sort();
4865 assert_eq!(
4866 got,
4867 vec![
4868 ("@".to_string(), "./src".to_string()),
4869 ("~".to_string(), "./lib".to_string()),
4870 ]
4871 );
4872 }
4873
4874 #[test]
4875 fn aliases_import_cycle_terminates() {
4876 let dir = tempfile::tempdir().unwrap();
4879 std::fs::write(
4880 dir.path().join("a.js"),
4881 r#"import { b } from "./b.js";
4882 export const a = [{ find: "@", replacement: "./src" }, ...b];"#,
4883 )
4884 .unwrap();
4885 std::fs::write(
4886 dir.path().join("b.js"),
4887 r#"import { a } from "./a.js";
4888 export const b = [...a];"#,
4889 )
4890 .unwrap();
4891 let config = dir.path().join("vite.config.js");
4892 let source = r#"
4893 import { a } from "./a.js";
4894 export default defineConfig({ resolve: { alias: a } });
4895 "#;
4896 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4897 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4898 }
4899
4900 #[test]
4901 fn aliases_non_relative_import_not_followed() {
4902 let source = r#"
4905 import { sharedAliases } from "some-pkg";
4906 export default defineConfig({ resolve: { alias: sharedAliases } });
4907 "#;
4908 let dir = tempfile::tempdir().unwrap();
4909 let config = dir.path().join("vite.config.js");
4910 assert!(extract_config_aliases(source, &config, &["resolve", "alias"]).is_empty());
4911 }
4912
4913 #[test]
4914 fn aliases_object_array_value_takes_first_entry() {
4915 let source = r#"
4920 export default {
4921 compilerOptions: { paths: { "@/*": ["./src/*"], "~/*": ["./lib/*", "./vendor/*"] } }
4922 };
4923 "#;
4924 let mut got = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
4925 got.sort();
4926 assert_eq!(
4927 got,
4928 vec![
4929 ("@/*".to_string(), "./src/*".to_string()),
4930 ("~/*".to_string(), "./lib/*".to_string()),
4931 ]
4932 );
4933 }
4934
4935 #[test]
4936 fn aliases_kinded_preserves_is_bare_through_resolution() {
4937 let source = r#"
4940 const a = [{ find: "lodash-es", replacement: "lodash" }];
4941 export default defineConfig({
4942 resolve: { alias: [...a, { find: "@", replacement: "./src" }] }
4943 });
4944 "#;
4945 let mut got = extract_config_aliases_kinded(source, &js_path(), &["resolve", "alias"]);
4946 got.sort();
4947 assert_eq!(
4948 got,
4949 vec![
4950 ("@".to_string(), "./src".to_string(), false),
4951 ("lodash-es".to_string(), "lodash".to_string(), true),
4952 ]
4953 );
4954 }
4955
4956 #[test]
4957 fn aliases_kinded_preserves_is_bare_through_imported_spread() {
4958 let dir = tempfile::tempdir().unwrap();
4959 std::fs::write(
4960 dir.path().join("aliases.js"),
4961 r#"export const packageAliases = [{ find: "lodash-es", replacement: "lodash" }];"#,
4962 )
4963 .unwrap();
4964 let config = dir.path().join("vite.config.js");
4965 let source = r#"
4966 import { packageAliases } from "./aliases.js";
4967 export default defineConfig({
4968 resolve: { alias: [...packageAliases, { find: "@", replacement: "./src" }] }
4969 });
4970 "#;
4971 let mut got = extract_config_aliases_kinded(source, &config, &["resolve", "alias"]);
4972 got.sort();
4973 assert_eq!(
4974 got,
4975 vec![
4976 ("@".to_string(), "./src".to_string(), false),
4977 ("lodash-es".to_string(), "lodash".to_string(), true),
4978 ]
4979 );
4980 }
4981
4982 #[test]
4985 fn extract_command_string_literal() {
4986 let source = r#"export default { start: "node server.js" };"#;
4987 let val = extract_config_command(source, &js_path(), &["start"]);
4988 assert_eq!(val, Some("node server.js".to_string()));
4989 }
4990
4991 #[test]
4992 fn extract_command_nested_path() {
4993 let source = r#"
4994 export default {
4995 scripts: {
4996 dev: "vite dev"
4997 }
4998 };
4999 "#;
5000 let val = extract_config_command(source, &js_path(), &["scripts", "dev"]);
5001 assert_eq!(val, Some("vite dev".to_string()));
5002 }
5003
5004 #[test]
5005 fn extract_command_missing_key_returns_none() {
5006 let source = r#"export default { other: "val" };"#;
5007 let val = extract_config_command(source, &js_path(), &["start"]);
5008 assert!(val.is_none());
5009 }
5010
5011 #[test]
5012 fn extract_command_ts_as_expression() {
5013 let source = r#"export default { start: "node server.js" as string };"#;
5014 let val = extract_config_command(source, &ts_path(), &["start"]);
5015 assert_eq!(val, Some("node server.js".to_string()));
5016 }
5017
5018 #[test]
5019 fn extract_command_ts_satisfies_expression() {
5020 let source = r#"export default { start: "node server.js" satisfies string };"#;
5021 let val = extract_config_command(source, &ts_path(), &["start"]);
5022 assert_eq!(val, Some("node server.js".to_string()));
5023 }
5024
5025 #[test]
5026 fn extract_command_parenthesized_expression() {
5027 let source = r#"export default { start: ("node server.js") };"#;
5028 let val = extract_config_command(source, &js_path(), &["start"]);
5029 assert_eq!(val, Some("node server.js".to_string()));
5030 }
5031
5032 #[test]
5033 fn extract_command_empty_path_returns_none() {
5034 let source = r#"export default { start: "node server.js" };"#;
5035 let val = extract_config_command(source, &js_path(), &[]);
5036 assert!(val.is_none());
5037 }
5038
5039 #[test]
5042 fn truthy_bool_or_object_with_true_value() {
5043 let source = r"export default { typescript: true };";
5044 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5045 assert!(result);
5046 }
5047
5048 #[test]
5049 fn truthy_bool_or_object_with_false_value() {
5050 let source = r"export default { typescript: false };";
5051 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5052 assert!(!result);
5053 }
5054
5055 #[test]
5056 fn truthy_bool_or_object_with_object_value() {
5057 let source = r#"export default { typescript: { reactDocgen: "react-docgen" } };"#;
5058 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5059 assert!(result);
5060 }
5061
5062 #[test]
5063 fn truthy_bool_or_object_missing_key_returns_false() {
5064 let source = r"export default { other: true };";
5065 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5066 assert!(!result);
5067 }
5068
5069 #[test]
5070 fn truthy_bool_or_object_with_string_value_returns_false() {
5071 let source = r#"export default { typescript: "yes" };"#;
5073 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5074 assert!(!result);
5075 }
5076
5077 #[test]
5078 fn truthy_bool_or_object_ts_satisfies_wrapper() {
5079 let source = r"export default { typescript: (true satisfies boolean) };";
5080 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5081 assert!(result);
5082 }
5083
5084 #[test]
5085 fn truthy_bool_or_object_ts_as_wrapper() {
5086 let source = r"export default { typescript: (true as boolean) };";
5087 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5088 assert!(result);
5089 }
5090
5091 #[test]
5092 fn truthy_bool_or_object_parenthesized_wrapper() {
5093 let source = r"export default { typescript: (true) };";
5094 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5095 assert!(result);
5096 }
5097
5098 #[test]
5104 fn static_dir_entries_object_form_exercises_property_string() {
5105 let source = r#"
5108 export default {
5109 staticDirs: [
5110 { from: "./media", to: "/assets" }
5111 ]
5112 };
5113 "#;
5114 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5115 assert_eq!(
5116 entries,
5117 vec![("./media".to_string(), Some("/assets".to_string()))]
5118 );
5119 }
5120
5121 #[test]
5124 fn expression_to_path_values_array_form_via_config_path() {
5125 let source = r#"export default { entries: ["./src/a.ts", "./src/b.ts"] };"#;
5128 let result = extract_config_string_or_array(source, &js_path(), &["entries"]);
5129 assert_eq!(result, vec!["./src/a.ts", "./src/b.ts"]);
5130 }
5131
5132 #[test]
5135 fn array_nested_aliases_object_form() {
5136 let source = r#"
5137 export default {
5138 test: {
5139 projects: [
5140 {
5141 resolve: {
5142 alias: { "@": "./src" }
5143 }
5144 }
5145 ]
5146 }
5147 };
5148 "#;
5149 let aliases = extract_config_array_nested_aliases_kinded(
5150 source,
5151 &ts_path(),
5152 &["test", "projects"],
5153 &["resolve", "alias"],
5154 );
5155 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
5156 }
5157
5158 #[test]
5159 fn array_nested_aliases_empty_when_path_is_not_array() {
5160 let source = r#"export default { test: { projects: "not-an-array" } };"#;
5161 let aliases = extract_config_array_nested_aliases_kinded(
5162 source,
5163 &ts_path(),
5164 &["test", "projects"],
5165 &["resolve", "alias"],
5166 );
5167 assert!(aliases.is_empty());
5168 }
5169
5170 #[test]
5171 fn array_nested_aliases_kinded_tracks_is_bare() {
5172 let source = r#"
5173 export default {
5174 projects: [
5175 {
5176 resolve: {
5177 alias: [
5178 { find: "lodash-es", replacement: "lodash" },
5179 { find: "@", replacement: "./src" }
5180 ]
5181 }
5182 }
5183 ]
5184 };
5185 "#;
5186 let mut aliases = extract_config_array_nested_aliases_kinded(
5187 source,
5188 &ts_path(),
5189 &["projects"],
5190 &["resolve", "alias"],
5191 );
5192 aliases.sort();
5193 assert_eq!(
5194 aliases,
5195 vec![
5196 ("@".to_string(), "./src".to_string(), false),
5197 ("lodash-es".to_string(), "lodash".to_string(), true),
5198 ]
5199 );
5200 }
5201
5202 #[test]
5205 fn default_export_array_aliases_kinded_extracts_from_workspace_config() {
5206 let source = r#"
5207 export default [
5208 {
5209 resolve: {
5210 alias: { "@": "./src" }
5211 }
5212 },
5213 {
5214 resolve: {
5215 alias: [{ find: "~", replacement: "./lib" }]
5216 }
5217 }
5218 ];
5219 "#;
5220 let mut aliases =
5221 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5222 aliases.sort();
5223 assert_eq!(
5224 aliases,
5225 vec![
5226 ("@".to_string(), "./src".to_string(), false),
5227 ("~".to_string(), "./lib".to_string(), false),
5228 ]
5229 );
5230 }
5231
5232 #[test]
5233 fn default_export_array_aliases_kinded_define_workspace_wrapper() {
5234 let source = r#"
5235 export default defineWorkspace([
5236 {
5237 resolve: { alias: { "@": "./src" } }
5238 }
5239 ]);
5240 "#;
5241 let aliases =
5242 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5243 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
5244 }
5245
5246 #[test]
5247 fn default_export_array_aliases_kinded_empty_when_no_alias_path() {
5248 let source = r#"
5249 export default [
5250 { test: { include: ["**/*.test.ts"] } }
5251 ];
5252 "#;
5253 let aliases =
5254 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5255 assert!(aliases.is_empty());
5256 }
5257
5258 #[test]
5261 fn config_default_export_unreachable_when_no_export() {
5262 let source = r"const x = 42;";
5263 assert!(config_default_export_unreachable(source, &js_path()));
5264 }
5265
5266 #[test]
5267 fn config_default_export_unreachable_false_for_object_export() {
5268 let source = r#"export default { key: "value" };"#;
5269 assert!(!config_default_export_unreachable(source, &js_path()));
5270 }
5271
5272 #[test]
5273 fn config_default_export_unreachable_false_for_array_export() {
5274 let source = r#"export default ["a", "b"];"#;
5275 assert!(!config_default_export_unreachable(source, &js_path()));
5276 }
5277
5278 #[test]
5279 fn config_default_export_unreachable_true_for_function_without_return_object() {
5280 let source = r"export default function config() { return 42; }";
5282 assert!(config_default_export_unreachable(source, &js_path()));
5283 }
5284
5285 #[test]
5288 fn static_dir_entries_string_and_object_form() {
5289 let source = r#"
5290 export default {
5291 staticDirs: [
5292 "./public",
5293 { from: "../assets", to: "/static" }
5294 ]
5295 };
5296 "#;
5297 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5298 assert_eq!(
5299 entries,
5300 vec![
5301 ("./public".to_string(), None),
5302 ("../assets".to_string(), Some("/static".to_string())),
5303 ]
5304 );
5305 }
5306
5307 #[test]
5308 fn static_dir_entries_object_without_to() {
5309 let source = r#"
5310 export default {
5311 staticDirs: [
5312 { from: "./media" }
5313 ]
5314 };
5315 "#;
5316 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5317 assert_eq!(entries, vec![("./media".to_string(), None)]);
5318 }
5319
5320 #[test]
5321 fn static_dir_entries_object_missing_from_skipped() {
5322 let source = r#"
5324 export default {
5325 staticDirs: [
5326 { to: "/target" },
5327 "./public"
5328 ]
5329 };
5330 "#;
5331 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5332 assert_eq!(entries, vec![("./public".to_string(), None)]);
5333 }
5334
5335 #[test]
5336 fn static_dir_entries_empty_when_not_array() {
5337 let source = r#"export default { staticDirs: "./public" };"#;
5338 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5339 assert!(entries.is_empty());
5340 }
5341
5342 #[test]
5345 fn aliases_array_form_missing_find_or_replacement_skipped() {
5346 let source = r#"
5348 export default {
5349 resolve: {
5350 alias: [
5351 { replacement: "./src" },
5352 { find: "@" },
5353 { find: "~", replacement: "./lib" }
5354 ]
5355 }
5356 };
5357 "#;
5358 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5359 assert_eq!(aliases, vec![("~".to_string(), "./lib".to_string())]);
5360 }
5361
5362 #[test]
5363 fn aliases_object_form_computed_key_skipped() {
5364 let source = r#"
5366 const k = "@";
5367 export default {
5368 resolve: {
5369 alias: {
5370 [k]: "./src",
5371 "~": "./lib"
5372 }
5373 }
5374 };
5375 "#;
5376 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5377 assert_eq!(aliases, vec![("~".to_string(), "./lib".to_string())]);
5379 }
5380
5381 #[test]
5382 fn aliases_kinded_array_form_path_replacement_is_not_bare() {
5383 let source = r#"
5384 export default {
5385 resolve: {
5386 alias: [{ find: "@", replacement: "./src" }]
5387 }
5388 };
5389 "#;
5390 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5391 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
5392 }
5393
5394 #[test]
5395 fn aliases_kinded_object_form_bare_and_path_discrimination() {
5396 let source = r#"
5397 export default {
5398 resolve: {
5399 alias: {
5400 "lodash-es": "lodash",
5401 "@": "./src"
5402 }
5403 }
5404 };
5405 "#;
5406 let mut aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5407 aliases.sort();
5408 assert_eq!(
5409 aliases,
5410 vec![
5411 ("@".to_string(), "./src".to_string(), false),
5412 ("lodash-es".to_string(), "lodash".to_string(), true),
5413 ]
5414 );
5415 }
5416
5417 #[test]
5418 fn aliases_kinded_parent_relative_replacement_is_not_bare() {
5419 let source = r#"
5420 export default {
5421 resolve: { alias: { "@": "../shared/src" } }
5422 };
5423 "#;
5424 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5425 assert_eq!(
5426 aliases,
5427 vec![("@".to_string(), "../shared/src".to_string(), false)]
5428 );
5429 }
5430
5431 #[test]
5432 fn aliases_kinded_absolute_replacement_is_not_bare() {
5433 let source = r#"
5434 export default {
5435 resolve: { alias: { "@": "/absolute/path" } }
5436 };
5437 "#;
5438 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5439 assert_eq!(
5440 aliases,
5441 vec![("@".to_string(), "/absolute/path".to_string(), false)]
5442 );
5443 }
5444
5445 #[test]
5448 fn default_export_array_ts_as_wrapper() {
5449 let source = r"export default [] as string[];";
5451 assert!(!config_default_export_unreachable(source, &js_path()));
5452 }
5453
5454 #[test]
5455 fn default_export_array_ts_satisfies_wrapper() {
5456 let source = r"export default [] satisfies string[];";
5457 assert!(!config_default_export_unreachable(source, &ts_path()));
5458 }
5459
5460 #[test]
5461 fn default_export_array_define_config_call_wrapper() {
5462 let source = r#"export default defineConfig(["**/*.test.ts"]);"#;
5463 assert!(!config_default_export_unreachable(source, &ts_path()));
5464 }
5465
5466 #[test]
5469 fn shallow_strings_object_with_string_values() {
5470 let source = r#"
5472 export default {
5473 plugins: {
5474 autoprefixer: "autoprefixer",
5475 tailwindcss: "tailwindcss"
5476 }
5477 };
5478 "#;
5479 let vals = extract_config_shallow_strings(source, &js_path(), "plugins");
5480 assert!(vals.contains(&"autoprefixer".to_string()));
5481 assert!(vals.contains(&"tailwindcss".to_string()));
5482 }
5483
5484 #[test]
5485 fn shallow_strings_object_with_sub_array_first_element() {
5486 let source = r#"
5488 export default {
5489 reporters: {
5490 main: ["jest-junit", { outputFile: "report.xml" }],
5491 alt: ["html-reporter"]
5492 }
5493 };
5494 "#;
5495 let vals = extract_config_shallow_strings(source, &js_path(), "reporters");
5496 assert!(vals.contains(&"jest-junit".to_string()));
5497 assert!(vals.contains(&"html-reporter".to_string()));
5498 }
5499
5500 #[test]
5503 fn shallow_strings_or_object_property_non_array_single_string() {
5504 let source = r#"export default { jsPlugins: "eslint-plugin-foo" };"#;
5506 let vals = extract_config_shallow_strings_or_object_property(
5507 source,
5508 &ts_path(),
5509 "jsPlugins",
5510 "specifier",
5511 );
5512 assert_eq!(vals, vec!["eslint-plugin-foo"]);
5513 }
5514
5515 #[test]
5516 fn shallow_strings_or_object_property_ts_satisfies_array_element() {
5517 let source = r#"
5519 export default {
5520 jsPlugins: [
5521 ("eslint-plugin-a" satisfies string)
5522 ]
5523 };
5524 "#;
5525 let vals = extract_config_shallow_strings_or_object_property(
5526 source,
5527 &ts_path(),
5528 "jsPlugins",
5529 "specifier",
5530 );
5531 assert_eq!(vals, vec!["eslint-plugin-a"]);
5532 }
5533
5534 #[test]
5535 fn shallow_strings_or_object_property_ts_as_array_element() {
5536 let source = r#"
5537 export default {
5538 jsPlugins: [
5539 ("eslint-plugin-b" as string)
5540 ]
5541 };
5542 "#;
5543 let vals = extract_config_shallow_strings_or_object_property(
5544 source,
5545 &ts_path(),
5546 "jsPlugins",
5547 "specifier",
5548 );
5549 assert_eq!(vals, vec!["eslint-plugin-b"]);
5550 }
5551
5552 #[test]
5553 fn shallow_strings_or_object_property_sub_array_first_element_string() {
5554 let source = r#"
5556 export default {
5557 jsPlugins: [
5558 ["eslint-plugin-tuple-pkg", { options: true }]
5559 ]
5560 };
5561 "#;
5562 let vals = extract_config_shallow_strings_or_object_property(
5563 source,
5564 &ts_path(),
5565 "jsPlugins",
5566 "specifier",
5567 );
5568 assert_eq!(vals, vec!["eslint-plugin-tuple-pkg"]);
5569 }
5570
5571 #[test]
5574 fn array_object_command_pairs_basic() {
5575 let source = r#"
5576 export default {
5577 webServer: [
5578 { command: "node server.js", cwd: "packages/api" },
5579 { command: "vite dev" }
5580 ]
5581 };
5582 "#;
5583 let pairs = extract_config_array_object_command_pairs(
5584 source,
5585 &ts_path(),
5586 &["webServer"],
5587 "command",
5588 "cwd",
5589 );
5590 assert_eq!(
5591 pairs,
5592 vec![
5593 (
5594 "node server.js".to_string(),
5595 Some("packages/api".to_string())
5596 ),
5597 ("vite dev".to_string(), None),
5598 ]
5599 );
5600 }
5601
5602 #[test]
5603 fn array_object_command_pairs_skips_missing_command() {
5604 let source = r#"
5605 export default {
5606 webServer: [
5607 { cwd: "packages/api" },
5608 { command: "vite dev", cwd: "apps/web" }
5609 ]
5610 };
5611 "#;
5612 let pairs = extract_config_array_object_command_pairs(
5613 source,
5614 &ts_path(),
5615 &["webServer"],
5616 "command",
5617 "cwd",
5618 );
5619 assert_eq!(
5620 pairs,
5621 vec![("vite dev".to_string(), Some("apps/web".to_string()))]
5622 );
5623 }
5624
5625 #[test]
5626 fn array_object_command_pairs_empty_when_not_array() {
5627 let source = r#"export default { webServer: { command: "vite dev" } };"#;
5628 let pairs = extract_config_array_object_command_pairs(
5629 source,
5630 &ts_path(),
5631 &["webServer"],
5632 "command",
5633 "cwd",
5634 );
5635 assert!(pairs.is_empty());
5636 }
5637
5638 #[test]
5641 fn normalize_config_path_empty_string_returns_none() {
5642 let config_path = PathBuf::from("/project/vite.config.ts");
5643 let root = PathBuf::from("/project");
5644 assert_eq!(normalize_config_path("", &config_path, &root), None);
5645 }
5646
5647 #[test]
5648 fn normalize_config_path_escapes_to_above_root_returns_none() {
5649 let config_path = PathBuf::from("/project/vite.config.ts");
5650 let root = PathBuf::from("/project");
5651 assert_eq!(
5653 normalize_config_path("../../etc", &config_path, &root),
5654 None
5655 );
5656 }
5657
5658 #[test]
5659 fn normalize_config_path_dot_slash_resolves_relative_to_config_dir() {
5660 let config_path = PathBuf::from("/project/packages/app/vite.config.ts");
5661 let root = PathBuf::from("/project");
5662 assert_eq!(
5663 normalize_config_path("./src", &config_path, &root),
5664 Some("packages/app/src".to_string())
5665 );
5666 }
5667
5668 #[test]
5671 fn json_config_array_of_arrays_via_shallow_strings() {
5672 let source = r#"{"reporters": ["default", ["jest-junit", {}]]}"#;
5674 let vals = extract_config_shallow_strings(source, &json_path(), "reporters");
5675 assert_eq!(vals, vec!["default", "jest-junit"]);
5676 }
5677
5678 #[test]
5681 fn extract_config_path_string_literal() {
5682 let source = r#"export default { outDir: "./dist" };"#;
5683 let path = extract_config_path(source, &js_path(), &["outDir"]);
5684 assert_eq!(
5685 path.map(|p| p.to_string_lossy().replace('\\', "/")),
5686 Some("./dist".to_string())
5687 );
5688 }
5689
5690 #[test]
5691 fn extract_config_path_with_resolve_call() {
5692 let source = r#"
5693 import { resolve } from "node:path";
5694 export default { outDir: resolve(__dirname, "dist") };
5695 "#;
5696 let path = extract_config_path(source, &js_path(), &["outDir"]);
5697 assert_eq!(
5698 path.map(|p| p.to_string_lossy().replace('\\', "/")),
5699 Some("dist".to_string())
5700 );
5701 }
5702
5703 #[test]
5704 fn extract_config_path_missing_key_returns_none() {
5705 let source = r#"export default { other: "val" };"#;
5706 let path = extract_config_path(source, &js_path(), &["outDir"]);
5707 assert!(path.is_none());
5708 }
5709
5710 #[test]
5713 fn extract_imports_and_requires_both_forms() {
5714 let source = r"
5715 import foo from 'foo-pkg';
5716 require('bar-pkg');
5717 export default {};
5718 ";
5719 let sources = extract_imports_and_requires(source, &js_path());
5720 assert!(sources.contains(&"foo-pkg".to_string()));
5721 assert!(sources.contains(&"bar-pkg".to_string()));
5722 }
5723
5724 #[test]
5725 fn extract_imports_and_requires_skips_non_require_calls() {
5726 let source = r"
5727 import foo from 'foo-pkg';
5728 someOtherCall('bar-pkg');
5729 export default {};
5730 ";
5731 let sources = extract_imports_and_requires(source, &js_path());
5732 assert_eq!(sources, vec!["foo-pkg"]);
5733 }
5734
5735 #[test]
5738 fn nested_shallow_strings_non_object_nested_returns_empty() {
5739 let source = r#"export default { test: "not-an-object" };"#;
5741 let vals =
5742 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
5743 assert!(vals.is_empty());
5744 }
5745
5746 #[test]
5749 fn vite_react_babel_dependencies_namespace_import() {
5750 let source = r#"
5751 import * as react from "@vitejs/plugin-react";
5752
5753 export default defineConfig({
5754 plugins: [
5755 react.default({
5756 babel: {
5757 plugins: ["babel-plugin-ns"],
5758 },
5759 }),
5760 ],
5761 });
5762 "#;
5763 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
5764 assert_eq!(deps, vec!["babel-plugin-ns".to_string()]);
5765 }
5766
5767 #[test]
5770 fn property_strings_deeply_nested_object_values() {
5771 let source = r#"
5773 export default {
5774 settings: {
5775 a: "val-a",
5776 b: {
5777 c: "val-c",
5778 d: ["val-d1", "val-d2"]
5779 }
5780 }
5781 };
5782 "#;
5783 let values = extract_config_property_strings(source, &js_path(), "settings");
5784 assert!(values.contains(&"val-a".to_string()));
5785 assert!(values.contains(&"val-c".to_string()));
5786 assert!(values.contains(&"val-d1".to_string()));
5787 assert!(values.contains(&"val-d2".to_string()));
5788 }
5789
5790 #[test]
5793 fn aliases_exported_const_form_resolves() {
5794 let source = r#"
5796 export const sharedAliases = { "@": "./src" };
5797 export default defineConfig({ resolve: { alias: sharedAliases } });
5798 "#;
5799 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5800 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
5801 }
5802
5803 #[test]
5806 fn aliases_imported_from_sibling_directory_index_file() {
5807 let dir = tempfile::tempdir().unwrap();
5810 let aliases_dir = dir.path().join("aliases");
5811 std::fs::create_dir_all(&aliases_dir).unwrap();
5812 std::fs::write(
5813 aliases_dir.join("index.js"),
5814 r#"export const aliases = [{ find: "@", replacement: "./src" }];"#,
5815 )
5816 .unwrap();
5817 let config = dir.path().join("vite.config.js");
5818 let source = r#"
5819 import { aliases } from "./aliases";
5820 export default defineConfig({ resolve: { alias: aliases } });
5821 "#;
5822 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
5823 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
5824 }
5825
5826 #[test]
5829 fn aliases_depth_limit_terminates_deep_chain() {
5830 let source = r#"
5833 const a9 = [{ find: "@", replacement: "./src" }];
5834 const a8 = a9;
5835 const a7 = a8;
5836 const a6 = a7;
5837 const a5 = a6;
5838 const a4 = a5;
5839 const a3 = a4;
5840 const a2 = a3;
5841 const a1 = a2;
5842 export default defineConfig({ resolve: { alias: a1 } });
5843 "#;
5844 let got = extract_config_aliases(source, &js_path(), &["resolve", "alias"]);
5846 let _ = got; }
5848
5849 #[test]
5852 fn extract_aliases_file_url_to_path_new_url() {
5853 let source = r#"
5855 import { fileURLToPath, URL } from 'node:url';
5856 export default {
5857 resolve: {
5858 alias: {
5859 "@": fileURLToPath(new URL("./src", import.meta.url))
5860 }
5861 }
5862 };
5863 "#;
5864 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5865 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
5866 }
5867
5868 #[test]
5869 fn extract_path_via_new_url_pathname_member() {
5870 let source = r#"
5872 export default {
5873 resolve: {
5874 alias: {
5875 "@": new URL("./src", import.meta.url).pathname
5876 }
5877 }
5878 };
5879 "#;
5880 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5881 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
5882 }
5883
5884 #[test]
5887 fn truthy_bool_or_object_null_literal_returns_false() {
5888 let source = r"export default { typescript: null };";
5890 let result = extract_config_truthy_bool_or_object(source, &js_path(), &["typescript"]);
5891 assert!(!result);
5892 }
5893
5894 #[test]
5897 fn string_array_non_array_value_returns_empty() {
5898 let source = r#"export default { items: "not-an-array" };"#;
5899 let result = extract_config_string_array(source, &js_path(), &["items"]);
5900 assert!(result.is_empty());
5901 }
5902
5903 #[test]
5906 fn object_nested_empty_when_inner_value_is_not_object() {
5907 let source = r#"export default { targets: { build: "not-an-object" } };"#;
5909 let results =
5910 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
5911 assert!(results.is_empty());
5912 }
5913
5914 #[test]
5917 fn array_nested_string_or_array_missing_inner_path_returns_empty() {
5918 let source = r#"
5919 export default {
5920 test: {
5921 projects: [
5922 { test: { include: ["**/*.test.ts"] } }
5923 ]
5924 }
5925 };
5926 "#;
5927 let results = extract_config_array_nested_string_or_array(
5928 source,
5929 &ts_path(),
5930 &["test", "projects"],
5931 &["test", "setupFiles"],
5932 );
5933 assert!(results.is_empty());
5934 }
5935
5936 #[test]
5937 fn wrapped_named_const_default_export_resolves() {
5938 let source = r#"
5941 import createMDX from "@next/mdx";
5942 const nextConfig = { pageExtensions: ["ts", "tsx", "md", "mdx"] };
5943 const withMDX = createMDX({});
5944 export default withMDX(nextConfig);
5945 "#;
5946 let exts = extract_config_string_array(source, &ts_path(), &["pageExtensions"]);
5947 assert_eq!(exts, vec!["ts", "tsx", "md", "mdx"]);
5948 }
5949
5950 #[test]
5951 fn wrapped_named_const_module_exports_resolves() {
5952 let source = r#"
5954 const nextJest = require("next/jest");
5955 const createJestConfig = nextJest();
5956 const customConfig = { testMatch: ["**/*.test.ts"] };
5957 module.exports = createJestConfig(customConfig);
5958 "#;
5959 let matches = extract_config_string_array(source, &js_path(), &["testMatch"]);
5960 assert_eq!(matches, vec!["**/*.test.ts"]);
5961 }
5962
5963 #[test]
5964 fn wrapped_named_const_nested_and_curried_resolve() {
5965 let nested = r#"
5966 const nextConfig = { pageExtensions: ["mdx"] };
5967 const withMDX = (c) => c;
5968 const withFoo = (c) => c;
5969 export default withMDX(withFoo(nextConfig));
5970 "#;
5971 assert_eq!(
5972 extract_config_string_array(nested, &js_path(), &["pageExtensions"]),
5973 vec!["mdx"]
5974 );
5975
5976 let curried = r#"
5977 const nextConfig = { pageExtensions: ["md"] };
5978 const compose = (..._p) => (c) => c;
5979 export default compose(a, b)(nextConfig);
5980 "#;
5981 assert_eq!(
5982 extract_config_string_array(curried, &js_path(), &["pageExtensions"]),
5983 vec!["md"]
5984 );
5985 }
5986
5987 #[test]
5988 fn wrapped_inline_object_still_resolves() {
5989 let source = r#"
5991 const withMDX = createMDX({});
5992 export default withMDX({ pageExtensions: ["mdx"] });
5993 "#;
5994 assert_eq!(
5995 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
5996 vec!["mdx"]
5997 );
5998 }
5999}