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
937struct VitePluginOptionReader {
939 package: &'static str,
940 read: fn(&ObjectExpression<'_>, &mut Vec<String>),
941}
942
943const VITE_PLUGIN_OPTION_READERS: &[VitePluginOptionReader] = &[
947 VitePluginOptionReader {
948 package: "@vitejs/plugin-react",
949 read: collect_vite_react_babel_dependencies,
950 },
951 VitePluginOptionReader {
952 package: "@vitejs/plugin-react-swc",
953 read: collect_vite_react_swc_dependencies,
954 },
955 VitePluginOptionReader {
956 package: "@wyw-in-js/vite",
957 read: collect_wyw_babel_options_dependencies,
958 },
959];
960
961const MAX_VITE_PLUGIN_WRAPPER_DEPTH: usize = 3;
964
965#[must_use]
970pub(crate) fn extract_vite_plugin_option_dependencies(source: &str, path: &Path) -> Vec<String> {
971 extract_from_source(source, path, |program| {
972 let bindings: Vec<(PluginImportBindings, &VitePluginOptionReader)> =
973 VITE_PLUGIN_OPTION_READERS
974 .iter()
975 .map(|reader| {
976 (
977 collect_plugin_import_bindings(program, reader.package),
978 reader,
979 )
980 })
981 .filter(|(imports, _)| !imports.is_empty())
982 .collect();
983 if bindings.is_empty() {
984 return None;
985 }
986
987 let obj = find_config_object(program)?;
988 let plugins = get_nested_expression(obj, &["plugins"])?;
989 let Expression::ArrayExpression(plugin_array) = plugins else {
990 return None;
991 };
992
993 let mut deps = Vec::new();
994 for element in &plugin_array.elements {
995 if let Some(Expression::CallExpression(call)) = element.as_expression() {
996 read_vite_plugin_call(call, &bindings, 0, &mut deps);
997 }
998 }
999
1000 (!deps.is_empty()).then_some(deps)
1001 })
1002 .unwrap_or_default()
1003}
1004
1005fn read_vite_plugin_call(
1006 call: &CallExpression<'_>,
1007 bindings: &[(PluginImportBindings, &VitePluginOptionReader)],
1008 depth: usize,
1009 deps: &mut Vec<String>,
1010) {
1011 if let Some((_, reader)) = bindings
1012 .iter()
1013 .find(|(imports, _)| is_plugin_binding_call(call, imports))
1014 {
1015 if let Some(Expression::ObjectExpression(options)) =
1016 call.arguments.first().and_then(Argument::as_expression)
1017 {
1018 (reader.read)(options, deps);
1019 }
1020 return;
1021 }
1022 if depth >= MAX_VITE_PLUGIN_WRAPPER_DEPTH {
1023 return;
1024 }
1025 for argument in &call.arguments {
1026 if let Some(Expression::CallExpression(inner)) = argument.as_expression() {
1027 read_vite_plugin_call(inner, bindings, depth + 1, deps);
1028 }
1029 }
1030}
1031
1032#[must_use]
1036pub(crate) fn config_string_package_name(raw: &str) -> Option<String> {
1037 let specifier = raw.trim();
1038 is_package_specifier(specifier).then(|| crate::resolve::extract_package_name(specifier))
1039}
1040
1041#[must_use]
1044pub(crate) fn extract_config_swc_plugin_dependencies(
1045 source: &str,
1046 path: &Path,
1047 array_path: &[&str],
1048) -> Vec<String> {
1049 extract_config_array_tuple_heads(source, path, array_path)
1050 .iter()
1051 .filter_map(|raw| config_string_package_name(raw))
1052 .fold(Vec::new(), |mut deps, dep| {
1053 push_unique_string(&mut deps, dep);
1054 deps
1055 })
1056}
1057
1058#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1060enum LeadingSlash {
1061 RootRelative,
1063 Filesystem,
1065}
1066
1067#[must_use]
1076pub(crate) fn normalize_config_path_buf(
1077 raw: impl AsRef<Path>,
1078 config_path: &Path,
1079 root: &Path,
1080) -> Option<PathBuf> {
1081 normalize_path_value(raw.as_ref(), config_path, root, LeadingSlash::RootRelative)
1082}
1083
1084#[must_use]
1088pub(crate) fn normalize_filesystem_config_path_buf(
1089 raw: impl AsRef<Path>,
1090 config_path: &Path,
1091 root: &Path,
1092) -> Option<PathBuf> {
1093 normalize_path_value(raw.as_ref(), config_path, root, LeadingSlash::Filesystem)
1094}
1095
1096fn normalize_path_value(
1097 raw: &Path,
1098 config_path: &Path,
1099 root: &Path,
1100 leading_slash: LeadingSlash,
1101) -> Option<PathBuf> {
1102 if raw.as_os_str().is_empty() {
1103 return None;
1104 }
1105
1106 let raw_string = path_to_config_string(raw);
1107 let raw_path = Path::new(&raw_string);
1108 let root_relative_slash =
1109 leading_slash == LeadingSlash::RootRelative && raw_string.starts_with('/');
1110 let absolute = raw_path
1111 .is_absolute()
1112 .then(|| lexical_normalize(raw_path))
1113 .filter(|absolute| absolute.starts_with(root))
1114 .filter(|absolute| !(root_relative_slash && (absolute == root || root_in_root(root))));
1115 let candidate = if let Some(absolute) = absolute {
1116 absolute
1117 } else if let Some(stripped) = raw_string.strip_prefix('/') {
1118 match leading_slash {
1119 LeadingSlash::RootRelative => lexical_normalize(&root.join(stripped)),
1120 LeadingSlash::Filesystem => return None,
1121 }
1122 } else if raw_path.is_absolute() {
1123 return None;
1124 } else {
1125 let base = config_path.parent().unwrap_or(root);
1126 lexical_normalize(&base.join(raw_path))
1127 };
1128
1129 let relative = candidate.strip_prefix(root).ok()?;
1130 (!relative.as_os_str().is_empty()).then(|| relative.to_path_buf())
1131}
1132
1133fn root_in_root(root: &Path) -> bool {
1137 let root_string = path_to_config_string(root);
1138 let Some(stripped) = root_string.strip_prefix('/') else {
1139 return false;
1140 };
1141 !stripped.is_empty() && root.join(stripped).is_dir()
1142}
1143
1144#[must_use]
1147pub(crate) fn normalize_filesystem_config_path(
1148 raw: impl AsRef<Path>,
1149 config_path: &Path,
1150 root: &Path,
1151) -> Option<String> {
1152 normalize_filesystem_config_path_buf(raw, config_path, root)
1153 .map(|path| path_to_config_string(&path))
1154}
1155
1156#[must_use]
1158pub(crate) fn normalize_config_path(
1159 raw: impl AsRef<Path>,
1160 config_path: &Path,
1161 root: &Path,
1162) -> Option<String> {
1163 normalize_config_path_buf(raw, config_path, root).map(|path| path_to_config_string(&path))
1164}
1165
1166pub(crate) fn parent_relative_config_path(
1176 target: &str,
1177 config_path: &Path,
1178 root: &Path,
1179) -> Option<String> {
1180 if !target.starts_with("../") {
1181 return None;
1182 }
1183 let directory = config_path.parent().unwrap_or(root);
1184 let candidate = lexical_normalize(&directory.join(target));
1185 let root = lexical_normalize(root);
1186 let mut ancestor = root.as_path();
1187 let mut climbs = 0;
1188 while !candidate.starts_with(ancestor) {
1189 ancestor = ancestor.parent()?;
1190 climbs += 1;
1191 }
1192 let rest = candidate.strip_prefix(ancestor).ok()?;
1193 (climbs > 0 && !rest.as_os_str().is_empty())
1194 .then(|| format!("{}{}", "../".repeat(climbs), path_to_config_string(rest)))
1195}
1196
1197pub(crate) fn extract_from_source<T>(
1204 source: &str,
1205 path: &Path,
1206 extractor: impl FnOnce(&Program) -> Option<T>,
1207) -> Option<T> {
1208 let source_type = SourceType::from_path(path).unwrap_or_default();
1209 let alloc = Allocator::default();
1210
1211 let is_json = path
1212 .extension()
1213 .is_some_and(|ext| ext == "json" || ext == "jsonc");
1214 if is_json {
1215 let wrapped = format!("({source})");
1216 let parsed = Parser::new(&alloc, &wrapped, SourceType::mjs()).parse();
1217 return extractor(&parsed.program);
1218 }
1219
1220 let parsed = Parser::new(&alloc, source, source_type).parse();
1221 extractor(&parsed.program)
1222}
1223
1224#[derive(Default)]
1227struct PluginImportBindings {
1228 callables: Vec<String>,
1229 namespaces: Vec<String>,
1230}
1231
1232impl PluginImportBindings {
1233 fn is_empty(&self) -> bool {
1234 self.callables.is_empty() && self.namespaces.is_empty()
1235 }
1236}
1237
1238fn collect_plugin_import_bindings(program: &Program<'_>, package: &str) -> PluginImportBindings {
1239 let mut imports = PluginImportBindings::default();
1240
1241 for stmt in &program.body {
1242 let Statement::ImportDeclaration(decl) = stmt else {
1243 continue;
1244 };
1245 if decl.source.value != package {
1246 continue;
1247 }
1248 let Some(specifiers) = &decl.specifiers else {
1249 continue;
1250 };
1251 for specifier in specifiers {
1252 match specifier {
1253 ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
1254 push_unique_string(&mut imports.callables, specifier.local.name.to_string());
1255 }
1256 ImportDeclarationSpecifier::ImportSpecifier(specifier)
1257 if specifier.imported.name().as_ref() == "default" =>
1258 {
1259 push_unique_string(&mut imports.callables, specifier.local.name.to_string());
1260 }
1261 ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
1262 push_unique_string(&mut imports.namespaces, specifier.local.name.to_string());
1263 }
1264 ImportDeclarationSpecifier::ImportSpecifier(_) => {}
1265 }
1266 }
1267 }
1268
1269 imports
1270}
1271
1272fn is_plugin_binding_call(call: &CallExpression<'_>, imports: &PluginImportBindings) -> bool {
1273 match &call.callee {
1274 Expression::Identifier(identifier) => imports
1275 .callables
1276 .iter()
1277 .any(|name| name == identifier.name.as_str()),
1278 Expression::StaticMemberExpression(member) if matches!(&member.object, Expression::Identifier(object) if imports.namespaces.iter().any(|name| name == object.name.as_str())) => {
1279 member.property.name == "default"
1280 }
1281 _ => false,
1282 }
1283}
1284
1285fn collect_vite_react_babel_dependencies(options: &ObjectExpression<'_>, deps: &mut Vec<String>) {
1286 if let Some(babel) = property_object(options, "babel") {
1287 collect_babel_plugin_and_preset_dependencies(babel, deps);
1288 }
1289}
1290
1291fn collect_wyw_babel_options_dependencies(options: &ObjectExpression<'_>, deps: &mut Vec<String>) {
1294 if let Some(babel) = property_object(options, "babelOptions") {
1295 collect_babel_plugin_and_preset_dependencies(babel, deps);
1296 }
1297}
1298
1299fn collect_vite_react_swc_dependencies(options: &ObjectExpression<'_>, deps: &mut Vec<String>) {
1302 let Some(prop) = find_property(options, "plugins") else {
1303 return;
1304 };
1305 let Expression::ArrayExpression(plugins) = &prop.value else {
1306 return;
1307 };
1308 for entry in &plugins.elements {
1309 let Some(Expression::ArrayExpression(tuple)) = entry.as_expression() else {
1310 continue;
1311 };
1312 if let Some(dep) = tuple
1313 .elements
1314 .first()
1315 .and_then(ArrayExpressionElement::as_expression)
1316 .and_then(expression_to_string)
1317 .and_then(|raw| config_string_package_name(&raw))
1318 {
1319 push_unique_string(deps, dep);
1320 }
1321 }
1322}
1323
1324fn collect_babel_plugin_and_preset_dependencies(
1325 babel: &ObjectExpression<'_>,
1326 deps: &mut Vec<String>,
1327) {
1328 for key in ["plugins", "presets"] {
1329 let Some(prop) = find_property(babel, key) else {
1330 continue;
1331 };
1332 for raw in collect_shallow_string_values(&prop.value) {
1333 if let Some(dep) = vite_react_babel_dependency_name(&raw) {
1334 push_unique_string(deps, dep);
1335 }
1336 }
1337 }
1338}
1339
1340fn vite_react_babel_dependency_name(raw: &str) -> Option<String> {
1341 let raw = raw.trim();
1342 let specifier = raw.strip_prefix("module:").unwrap_or(raw).trim();
1343 if specifier.is_empty()
1344 || specifier.starts_with('.')
1345 || specifier.starts_with('/')
1346 || specifier.contains(':')
1347 || specifier.contains('\\')
1348 {
1349 return None;
1350 }
1351 Some(crate::resolve::extract_package_name(specifier))
1352}
1353
1354fn push_unique_string(items: &mut Vec<String>, value: String) {
1355 if !items.contains(&value) {
1356 items.push(value);
1357 }
1358}
1359
1360pub(crate) fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
1372 for stmt in &program.body {
1373 match stmt {
1374 Statement::ExportDefaultDeclaration(decl) => {
1375 let expr: Option<&Expression> = match &decl.declaration {
1376 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
1377 return Some(obj);
1378 }
1379 ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
1380 return extract_object_from_function(func);
1381 }
1382 _ => decl.declaration.as_expression(),
1383 };
1384 if let Some(expr) = expr {
1385 if let Some(obj) =
1386 resolve_call_config_object(program, expr, MAX_CONFIG_WRAPPER_DEPTH)
1387 {
1388 return Some(obj);
1389 }
1390 if let Some(obj) = extract_object_from_expression(expr) {
1391 return Some(obj);
1392 }
1393 if let Some(name) = unwrap_to_identifier_name(expr) {
1394 return find_variable_init_object(program, name);
1395 }
1396 if let Some(obj) = resolve_wrapped_config_object(program, expr) {
1397 return Some(obj);
1398 }
1399 }
1400 }
1401 Statement::ExpressionStatement(expr_stmt) => {
1402 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
1403 && is_module_exports_target(&assign.left)
1404 {
1405 if let Some(obj) =
1406 resolve_call_config_object(program, &assign.right, MAX_CONFIG_WRAPPER_DEPTH)
1407 {
1408 return Some(obj);
1409 }
1410 if let Some(obj) = extract_object_from_expression(&assign.right) {
1411 return Some(obj);
1412 }
1413 if let Some(name) = unwrap_to_identifier_name(&assign.right) {
1414 return find_variable_init_object(program, name);
1415 }
1416 return resolve_wrapped_config_object(program, &assign.right);
1417 }
1418 }
1419 _ => {}
1420 }
1421 }
1422
1423 if program.body.len() == 1
1424 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
1425 {
1426 match &expr_stmt.expression {
1427 Expression::ObjectExpression(obj) => return Some(obj),
1428 Expression::ParenthesizedExpression(paren) => {
1429 if let Expression::ObjectExpression(obj) = &paren.expression {
1430 return Some(obj);
1431 }
1432 }
1433 _ => {}
1434 }
1435 }
1436
1437 None
1438}
1439
1440pub(crate) fn extract_object_from_expression<'a>(
1442 expr: &'a Expression<'a>,
1443) -> Option<&'a ObjectExpression<'a>> {
1444 match expr {
1445 Expression::ObjectExpression(obj) => Some(obj),
1446 Expression::CallExpression(call) => {
1447 for arg in &call.arguments {
1448 match arg {
1449 Argument::ObjectExpression(obj) => return Some(obj),
1450 Argument::ArrowFunctionExpression(arrow) => {
1457 if let Some(obj) = extract_object_from_arrow_function(arrow) {
1458 return Some(obj);
1459 }
1460 }
1461 Argument::FunctionExpression(func) => {
1462 if let Some(obj) = extract_object_from_function(func) {
1463 return Some(obj);
1464 }
1465 }
1466 _ => {}
1467 }
1468 }
1469 None
1470 }
1471 Expression::ParenthesizedExpression(paren) => {
1472 extract_object_from_expression(&paren.expression)
1473 }
1474 Expression::TSSatisfiesExpression(ts_sat) => {
1475 extract_object_from_expression(&ts_sat.expression)
1476 }
1477 Expression::TSAsExpression(ts_as) => extract_object_from_expression(&ts_as.expression),
1478 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
1479 Expression::FunctionExpression(func) => extract_object_from_function(func),
1480 _ => None,
1481 }
1482}
1483
1484fn extract_object_from_arrow_function<'a>(
1485 arrow: &'a ArrowFunctionExpression<'a>,
1486) -> Option<&'a ObjectExpression<'a>> {
1487 match &arrow.body {
1488 ArrowFunctionBody::FunctionBody(body) => extract_object_from_function_body(body),
1489 body => body
1490 .as_expression()
1491 .and_then(extract_object_from_expression),
1492 }
1493}
1494
1495fn extract_object_from_function<'a>(func: &'a Function<'a>) -> Option<&'a ObjectExpression<'a>> {
1496 func.body
1497 .as_ref()
1498 .and_then(|body| extract_object_from_function_body(body))
1499}
1500
1501fn extract_object_from_function_body<'a>(
1510 body: &'a FunctionBody<'a>,
1511) -> Option<&'a ObjectExpression<'a>> {
1512 find_top_level_returned_object(&body.statements)
1513 .or_else(|| find_returned_object(&body.statements, MAX_CONFIG_BRANCH_DEPTH))
1514}
1515
1516const MAX_CONFIG_BRANCH_DEPTH: u8 = 4;
1518
1519fn find_top_level_returned_object<'a>(
1521 statements: &'a [Statement<'a>],
1522) -> Option<&'a ObjectExpression<'a>> {
1523 statements.iter().find_map(|stmt| match stmt {
1524 Statement::ReturnStatement(ret) => ret
1525 .argument
1526 .as_ref()
1527 .and_then(|argument| extract_object_from_expression(argument)),
1528 _ => None,
1529 })
1530}
1531
1532fn find_returned_object<'a>(
1541 statements: &'a [Statement<'a>],
1542 depth: u8,
1543) -> Option<&'a ObjectExpression<'a>> {
1544 if depth == 0 {
1545 return None;
1546 }
1547 for stmt in statements {
1548 let found = match stmt {
1549 Statement::ReturnStatement(ret) => ret
1550 .argument
1551 .as_ref()
1552 .and_then(|argument| extract_object_from_expression(argument)),
1553 Statement::BlockStatement(block) => find_returned_object(&block.body, depth - 1),
1554 Statement::IfStatement(if_stmt) => {
1555 find_returned_object_in_statement(&if_stmt.consequent, depth - 1).or_else(|| {
1556 if_stmt
1557 .alternate
1558 .as_ref()
1559 .and_then(|alt| find_returned_object_in_statement(alt, depth - 1))
1560 })
1561 }
1562 Statement::TryStatement(try_stmt) => {
1563 find_returned_object(&try_stmt.block.body, depth - 1)
1564 .or_else(|| {
1565 try_stmt
1566 .handler
1567 .as_ref()
1568 .and_then(|handler| find_returned_object(&handler.body.body, depth - 1))
1569 })
1570 .or_else(|| {
1571 try_stmt
1572 .finalizer
1573 .as_ref()
1574 .and_then(|finalizer| find_returned_object(&finalizer.body, depth - 1))
1575 })
1576 }
1577 Statement::SwitchStatement(switch) => switch
1578 .cases
1579 .iter()
1580 .find_map(|case| find_returned_object(&case.consequent, depth - 1)),
1581 _ => None,
1582 };
1583 if found.is_some() {
1584 return found;
1585 }
1586 }
1587 None
1588}
1589
1590fn find_returned_object_in_statement<'a>(
1591 stmt: &'a Statement<'a>,
1592 depth: u8,
1593) -> Option<&'a ObjectExpression<'a>> {
1594 match stmt {
1595 Statement::BlockStatement(block) => find_returned_object(&block.body, depth),
1596 other => find_returned_object(std::slice::from_ref(other), depth),
1597 }
1598}
1599
1600fn is_module_exports_target(target: &AssignmentTarget) -> bool {
1602 if let AssignmentTarget::StaticMemberExpression(member) = target
1603 && let Expression::Identifier(obj) = &member.object
1604 {
1605 return obj.name == "module" && member.property.name == "exports";
1606 }
1607 false
1608}
1609
1610fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
1614 match expr {
1615 Expression::Identifier(id) => Some(&id.name),
1616 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
1617 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
1618 _ => None,
1619 }
1620}
1621
1622pub(crate) fn find_variable_init_object<'a>(
1627 program: &'a Program,
1628 name: &str,
1629) -> Option<&'a ObjectExpression<'a>> {
1630 for decl in top_level_variable_declarations(program) {
1631 for declarator in &decl.declarations {
1632 if let BindingPattern::BindingIdentifier(id) = &declarator.id
1633 && id.name == name
1634 && let Some(init) = &declarator.init
1635 {
1636 return extract_object_from_expression(init);
1637 }
1638 }
1639 }
1640 None
1641}
1642
1643fn find_variable_init<'a>(program: &'a Program<'a>, name: &str) -> Option<&'a Expression<'a>> {
1645 top_level_variable_declarations(program)
1646 .flat_map(|decl| &decl.declarations)
1647 .find(|declarator| {
1648 matches!(&declarator.id, BindingPattern::BindingIdentifier(id) if id.name == name)
1649 })
1650 .and_then(|declarator| declarator.init.as_ref())
1651}
1652
1653fn exported_config_expression<'a>(program: &'a Program<'a>) -> Option<&'a Expression<'a>> {
1656 program.body.iter().find_map(|stmt| match stmt {
1657 Statement::ExportDefaultDeclaration(decl) => decl.declaration.as_expression(),
1658 Statement::ExpressionStatement(expr_stmt) => match &expr_stmt.expression {
1659 Expression::AssignmentExpression(assign) if is_module_exports_target(&assign.left) => {
1660 Some(&assign.right)
1661 }
1662 _ => None,
1663 },
1664 _ => None,
1665 })
1666}
1667
1668fn config_call<'a>(
1671 program: &'a Program<'a>,
1672 expr: &'a Expression<'a>,
1673 follow_binding: bool,
1674) -> Option<&'a CallExpression<'a>> {
1675 match expr {
1676 Expression::CallExpression(call) => Some(call),
1677 Expression::ParenthesizedExpression(paren) => {
1678 config_call(program, &paren.expression, follow_binding)
1679 }
1680 Expression::TSSatisfiesExpression(ts_sat) => {
1681 config_call(program, &ts_sat.expression, follow_binding)
1682 }
1683 Expression::TSAsExpression(ts_as) => {
1684 config_call(program, &ts_as.expression, follow_binding)
1685 }
1686 Expression::Identifier(id) if follow_binding => {
1687 config_call(program, find_variable_init(program, &id.name)?, false)
1688 }
1689 _ => None,
1690 }
1691}
1692
1693pub(crate) fn find_config_merge_arguments<'a>(
1702 program: &'a Program<'a>,
1703) -> Option<Vec<Option<&'a ObjectExpression<'a>>>> {
1704 let call = config_call(program, exported_config_expression(program)?, true)?;
1705 if call.arguments.len() < 2 {
1706 return None;
1707 }
1708 let resolve = |arg: &'a Argument<'a>| {
1709 let expr = arg.as_expression()?;
1710 if config_call(program, expr, true).is_some_and(|nested| nested.arguments.len() > 1) {
1711 return None;
1712 }
1713 resolve_config_argument(program, expr, MAX_CONFIG_WRAPPER_DEPTH)
1714 };
1715 Some(call.arguments.iter().map(resolve).collect())
1716}
1717
1718fn top_level_variable_declarations<'a>(
1721 program: &'a Program<'a>,
1722) -> impl Iterator<Item = &'a VariableDeclaration<'a>> {
1723 program.body.iter().filter_map(|stmt| match stmt {
1724 Statement::VariableDeclaration(decl) => Some(&**decl),
1725 Statement::ExportDeclaration(export) => match &export.declaration {
1726 Declaration::VariableDeclaration(decl) => Some(&**decl),
1727 _ => None,
1728 },
1729 _ => None,
1730 })
1731}
1732
1733pub(crate) fn find_stable_binding_init<'a>(
1745 program: &'a Program<'a>,
1746 name: &str,
1747) -> Option<&'a Expression<'a>> {
1748 if !holds_one_stable_object(program, name) {
1749 return None;
1750 }
1751 find_variable_init_expression(program, name)
1752}
1753
1754fn holds_one_stable_object(program: &Program<'_>, name: &str) -> bool {
1757 if !declares_top_level_const_or_let(program, name) {
1758 return false;
1759 }
1760 let mut usage = NameUsage::new(name);
1761 usage.visit_program(program);
1762 usage.bindings == 1 && usage.writes == 0
1763}
1764
1765fn declares_top_level_const_or_let(program: &Program<'_>, name: &str) -> bool {
1768 top_level_variable_declarations(program).any(|decl| {
1769 matches!(
1770 decl.kind,
1771 VariableDeclarationKind::Const | VariableDeclarationKind::Let
1772 ) && decl.declarations.iter().any(|declarator| {
1773 matches!(&declarator.id, BindingPattern::BindingIdentifier(id) if id.name == name)
1774 })
1775 })
1776}
1777
1778struct NameUsage<'n> {
1781 name: &'n str,
1782 bindings: usize,
1783 writes: usize,
1784 write_depth: usize,
1785}
1786
1787impl<'n> NameUsage<'n> {
1788 const fn new(name: &'n str) -> Self {
1789 Self {
1790 name,
1791 bindings: 0,
1792 writes: 0,
1793 write_depth: 0,
1794 }
1795 }
1796}
1797
1798impl<'a> Visit<'a> for NameUsage<'_> {
1799 fn visit_binding_identifier(&mut self, identifier: &BindingIdentifier<'a>) {
1800 if identifier.name == self.name {
1801 self.bindings += 1;
1802 }
1803 }
1804
1805 fn visit_identifier_reference(&mut self, identifier: &IdentifierReference<'a>) {
1806 if self.write_depth > 0 && identifier.name == self.name {
1807 self.writes += 1;
1808 }
1809 }
1810
1811 fn visit_assignment_target(&mut self, target: &AssignmentTarget<'a>) {
1812 self.write_depth += 1;
1813 walk::walk_assignment_target(self, target);
1814 self.write_depth -= 1;
1815 }
1816
1817 fn visit_update_expression(&mut self, expression: &UpdateExpression<'a>) {
1818 self.write_depth += 1;
1819 walk::walk_update_expression(self, expression);
1820 self.write_depth -= 1;
1821 }
1822
1823 fn visit_unary_expression(&mut self, expression: &UnaryExpression<'a>) {
1824 if expression.operator != UnaryOperator::Delete {
1825 walk::walk_unary_expression(self, expression);
1826 return;
1827 }
1828 self.write_depth += 1;
1829 walk::walk_unary_expression(self, expression);
1830 self.write_depth -= 1;
1831 }
1832}
1833
1834fn resolve_wrapped_config_object<'a>(
1847 program: &'a Program,
1848 expr: &'a Expression<'a>,
1849) -> Option<&'a ObjectExpression<'a>> {
1850 let call = match expr {
1851 Expression::CallExpression(call) => call,
1852 Expression::ParenthesizedExpression(paren) => {
1853 return resolve_wrapped_config_object(program, &paren.expression);
1854 }
1855 Expression::TSSatisfiesExpression(ts_sat) => {
1856 return resolve_wrapped_config_object(program, &ts_sat.expression);
1857 }
1858 Expression::TSAsExpression(ts_as) => {
1859 return resolve_wrapped_config_object(program, &ts_as.expression);
1860 }
1861 _ => return None,
1862 };
1863 for arg in &call.arguments {
1864 let Some(arg_expr) = arg.as_expression() else {
1865 continue;
1866 };
1867 if let Some(name) = unwrap_to_identifier_name(arg_expr)
1868 && let Some(obj) = find_variable_init_object(program, name)
1869 {
1870 return Some(obj);
1871 }
1872 if let Some(obj) = resolve_wrapped_config_object(program, arg_expr) {
1873 return Some(obj);
1874 }
1875 }
1876 None
1877}
1878
1879const MAX_CONFIG_WRAPPER_DEPTH: u8 = 3;
1884
1885fn resolve_call_config_object<'a>(
1896 program: &'a Program,
1897 expr: &'a Expression<'a>,
1898 depth: u8,
1899) -> Option<&'a ObjectExpression<'a>> {
1900 if depth == 0 {
1901 return None;
1902 }
1903 let call = match expr {
1904 Expression::CallExpression(call) => call,
1905 Expression::ParenthesizedExpression(paren) => {
1906 return resolve_call_config_object(program, &paren.expression, depth);
1907 }
1908 Expression::TSSatisfiesExpression(ts_sat) => {
1909 return resolve_call_config_object(program, &ts_sat.expression, depth);
1910 }
1911 Expression::TSAsExpression(ts_as) => {
1912 return resolve_call_config_object(program, &ts_as.expression, depth);
1913 }
1914 _ => return None,
1915 };
1916
1917 call.arguments
1918 .iter()
1919 .filter_map(oxc_ast::ast::Argument::as_expression)
1920 .find_map(|arg| resolve_config_argument(program, arg, depth))
1921}
1922
1923fn resolve_config_argument<'a>(
1925 program: &'a Program,
1926 expr: &'a Expression<'a>,
1927 depth: u8,
1928) -> Option<&'a ObjectExpression<'a>> {
1929 match expr {
1930 Expression::ObjectExpression(obj) => Some(obj),
1931 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
1932 Expression::FunctionExpression(func) => extract_object_from_function(func),
1933 Expression::ParenthesizedExpression(paren) => {
1934 resolve_config_argument(program, &paren.expression, depth)
1935 }
1936 Expression::TSSatisfiesExpression(ts_sat) => {
1937 resolve_config_argument(program, &ts_sat.expression, depth)
1938 }
1939 Expression::TSAsExpression(ts_as) => {
1940 resolve_config_argument(program, &ts_as.expression, depth)
1941 }
1942 Expression::CallExpression(_) => resolve_call_config_object(program, expr, depth - 1),
1943 _ => unwrap_to_identifier_name(expr)
1944 .and_then(|name| find_variable_init_object(program, name)),
1945 }
1946}
1947
1948pub(crate) fn find_property<'a>(
1950 obj: &'a ObjectExpression<'a>,
1951 key: &str,
1952) -> Option<&'a ObjectProperty<'a>> {
1953 for prop in &obj.properties {
1954 if let ObjectPropertyKind::ObjectProperty(p) = prop
1955 && property_key_matches(&p.key, key)
1956 {
1957 return Some(p);
1958 }
1959 }
1960 None
1961}
1962
1963fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
1965 match key {
1966 PropertyKey::StaticIdentifier(id) => id.name == name,
1967 PropertyKey::StringLiteral(s) => s.value == name,
1968 _ => false,
1969 }
1970}
1971
1972fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
1974 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
1975}
1976
1977fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
1979 find_property(obj, key)
1980 .map(|p| expression_to_string_array(&p.value))
1981 .unwrap_or_default()
1982}
1983
1984fn get_nested_string_array_from_object(
1986 obj: &ObjectExpression,
1987 path: &[&str],
1988) -> Option<Vec<String>> {
1989 if path.is_empty() {
1990 return None;
1991 }
1992 if path.len() == 1 {
1993 return Some(get_object_string_array_property(obj, path[0]));
1994 }
1995 let prop = find_property(obj, path[0])?;
1996 if let Expression::ObjectExpression(nested) = &prop.value {
1997 get_nested_string_array_from_object(nested, &path[1..])
1998 } else {
1999 None
2000 }
2001}
2002
2003fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
2005 if path.is_empty() {
2006 return None;
2007 }
2008 if path.len() == 1 {
2009 return get_object_string_property(obj, path[0]);
2010 }
2011 let prop = find_property(obj, path[0])?;
2012 if let Expression::ObjectExpression(nested) = &prop.value {
2013 get_nested_string_from_object(nested, &path[1..])
2014 } else {
2015 None
2016 }
2017}
2018
2019fn get_nested_command_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
2021 if path.is_empty() {
2022 return None;
2023 }
2024 if path.len() == 1 {
2025 return find_property(obj, path[0]).and_then(|prop| expression_to_command(&prop.value));
2026 }
2027 let prop = find_property(obj, path[0])?;
2028 if let Expression::ObjectExpression(nested) = &prop.value {
2029 get_nested_command_from_object(nested, &path[1..])
2030 } else {
2031 None
2032 }
2033}
2034
2035pub(crate) fn expression_to_string(expr: &Expression) -> Option<String> {
2037 match expr {
2038 Expression::StringLiteral(s) => Some(s.value.to_string()),
2039 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
2040 t.quasis.first().map(|q| q.value.raw.to_string())
2041 }
2042 _ => None,
2043 }
2044}
2045
2046fn expression_to_command(expr: &Expression) -> Option<String> {
2048 match expr {
2049 Expression::StringLiteral(s) => Some(s.value.to_string()),
2050 Expression::TemplateLiteral(template) => template_literal_to_command(template),
2051 Expression::ParenthesizedExpression(paren) => expression_to_command(&paren.expression),
2052 Expression::TSAsExpression(ts_as) => expression_to_command(&ts_as.expression),
2053 Expression::TSSatisfiesExpression(ts_sat) => expression_to_command(&ts_sat.expression),
2054 _ => None,
2055 }
2056}
2057
2058fn template_literal_to_command(template: &TemplateLiteral<'_>) -> Option<String> {
2059 let first = template.quasis.first()?.value.raw.as_str();
2060 if first.trim_start().is_empty() {
2061 return None;
2062 }
2063
2064 let mut command = String::new();
2065 for (idx, quasi) in template.quasis.iter().enumerate() {
2066 command.push_str(quasi.value.raw.as_str());
2067 if idx < template.expressions.len() {
2068 let next = template
2069 .quasis
2070 .get(idx + 1)
2071 .map_or("", |next| next.value.raw.as_str());
2072 if dynamic_template_boundary_splits_static_token(quasi.value.raw.as_str(), next) {
2073 return None;
2074 }
2075 command.push(' ');
2076 }
2077 }
2078
2079 Some(command)
2080}
2081
2082fn dynamic_template_boundary_splits_static_token(before: &str, after: &str) -> bool {
2083 before
2084 .chars()
2085 .next_back()
2086 .is_some_and(is_command_token_char)
2087 && after.chars().next().is_some_and(is_command_token_char)
2088}
2089
2090fn is_command_token_char(ch: char) -> bool {
2091 !ch.is_whitespace() && !matches!(ch, '&' | '|' | ';' | '"' | '\'')
2092}
2093
2094#[derive(Clone, Copy)]
2096enum PathScope<'p> {
2097 Bare,
2099 Program(&'p Program<'p>),
2101 Param { name: &'p str, value: &'p str },
2103}
2104
2105pub(crate) fn expression_to_path_string(expr: &Expression) -> Option<String> {
2107 path_string_in(expr, PathScope::Bare)
2108}
2109
2110fn path_string_in(expr: &Expression, scope: PathScope<'_>) -> Option<String> {
2111 match expr {
2112 Expression::ParenthesizedExpression(paren) => path_string_in(&paren.expression, scope),
2113 Expression::TSAsExpression(ts_as) => path_string_in(&ts_as.expression, scope),
2114 Expression::TSSatisfiesExpression(ts_sat) => path_string_in(&ts_sat.expression, scope),
2115 Expression::StaticMemberExpression(member) if member.property.name == "pathname" => {
2116 path_string_in(&member.object, scope)
2117 }
2118 Expression::CallExpression(call) => call_expression_to_path_string(call, scope),
2119 Expression::NewExpression(new_expr) => new_expression_to_path_string(new_expr, scope),
2120 _ => scoped_string(expr, scope),
2121 }
2122}
2123
2124fn scoped_string(expr: &Expression, scope: PathScope<'_>) -> Option<String> {
2126 match (expr, scope) {
2127 (Expression::Identifier(id), PathScope::Param { name, value }) if id.name == name => {
2128 Some(value.to_string())
2129 }
2130 _ => expression_to_string(expr),
2131 }
2132}
2133
2134fn call_expression_to_path_string(call: &CallExpression, scope: PathScope<'_>) -> Option<String> {
2135 if let PathScope::Program(program) = scope
2136 && let Expression::Identifier(id) = &call.callee
2137 && let Some(path) = local_helper_call_path(program, &id.name, call)
2138 {
2139 return Some(path);
2140 }
2141
2142 if matches!(&call.callee, Expression::Identifier(id) if id.name == "fileURLToPath") {
2143 return call
2144 .arguments
2145 .first()
2146 .and_then(Argument::as_expression)
2147 .and_then(|arg| path_string_in(arg, scope));
2148 }
2149
2150 let callee_name = match &call.callee {
2151 Expression::Identifier(id) => Some(id.name.as_str()),
2152 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
2153 _ => None,
2154 }?;
2155
2156 if !matches!(callee_name, "resolve" | "join") {
2157 return None;
2158 }
2159
2160 let mut segments = Vec::new();
2161 for (index, arg) in call.arguments.iter().enumerate() {
2162 let expr = arg.as_expression()?;
2163
2164 if is_dirname_anchor(expr) {
2165 if index == 0 {
2166 continue;
2167 }
2168 return None;
2169 }
2170
2171 segments.push(scoped_string(expr, scope)?);
2172 }
2173
2174 (!segments.is_empty()).then(|| join_path_segments(&segments))
2175}
2176
2177fn local_helper_call_path(program: &Program, name: &str, call: &CallExpression) -> Option<String> {
2184 let [argument] = call.arguments.as_slice() else {
2185 return None;
2186 };
2187 let value = expression_to_string(argument.as_expression()?)?;
2188 let (param, body) = find_local_path_helper(program, name)?;
2189 path_string_in(
2190 body,
2191 PathScope::Param {
2192 name: param,
2193 value: &value,
2194 },
2195 )
2196}
2197
2198fn find_local_path_helper<'a>(
2202 program: &'a Program<'a>,
2203 name: &str,
2204) -> Option<(&'a str, &'a Expression<'a>)> {
2205 let declared = program.body.iter().find_map(|stmt| {
2206 let func = match stmt {
2207 Statement::FunctionDeclaration(func) => func,
2208 Statement::ExportDeclaration(export) => match &export.declaration {
2209 Declaration::FunctionDeclaration(func) => func,
2210 _ => return None,
2211 },
2212 _ => return None,
2213 };
2214 func.id
2215 .as_ref()
2216 .is_some_and(|id| id.name == name)
2217 .then_some(&**func)
2218 });
2219 if let Some(func) = declared {
2220 return function_helper_body(func);
2221 }
2222 match find_variable_init_expression(program, name)?.without_parentheses() {
2223 Expression::ArrowFunctionExpression(arrow) => {
2224 let param = single_simple_param(&arrow.params)?;
2225 let body = match &arrow.body {
2226 ArrowFunctionBody::FunctionBody(body) => single_returned_expression(body)?,
2227 body => body.as_expression()?,
2228 };
2229 Some((param, body))
2230 }
2231 Expression::FunctionExpression(func) => function_helper_body(func),
2232 _ => None,
2233 }
2234}
2235
2236fn function_helper_body<'a>(func: &'a Function<'a>) -> Option<(&'a str, &'a Expression<'a>)> {
2237 let param = single_simple_param(&func.params)?;
2238 let body = single_returned_expression(func.body.as_ref()?)?;
2239 Some((param, body))
2240}
2241
2242fn single_simple_param<'a>(params: &'a FormalParameters<'a>) -> Option<&'a str> {
2243 let [param] = params.items.as_slice() else {
2244 return None;
2245 };
2246 if params.rest.is_some() || param.initializer.is_some() {
2247 return None;
2248 }
2249 match ¶m.pattern {
2250 BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
2251 _ => None,
2252 }
2253}
2254
2255fn single_returned_expression<'a>(body: &'a FunctionBody<'a>) -> Option<&'a Expression<'a>> {
2256 match body.statements.as_slice() {
2257 [Statement::ReturnStatement(ret)] => ret.argument.as_ref(),
2258 _ => None,
2259 }
2260}
2261
2262fn is_dirname_anchor(expr: &Expression) -> bool {
2267 match expr {
2268 Expression::Identifier(id) => id.name == "__dirname",
2269 Expression::StaticMemberExpression(member) => {
2270 member.property.name == "dirname" && is_import_meta_expression(&member.object)
2271 }
2272 _ => false,
2273 }
2274}
2275
2276fn is_import_meta_expression(expr: &Expression) -> bool {
2278 matches!(expr, Expression::ImportMeta(_))
2279}
2280
2281fn new_expression_to_path_string(new_expr: &NewExpression, scope: PathScope<'_>) -> Option<String> {
2282 if !matches!(&new_expr.callee, Expression::Identifier(id) if id.name == "URL") {
2283 return None;
2284 }
2285
2286 let source = new_expr
2287 .arguments
2288 .first()
2289 .and_then(Argument::as_expression)
2290 .and_then(|arg| scoped_string(arg, scope))?;
2291
2292 let base = new_expr
2293 .arguments
2294 .get(1)
2295 .and_then(Argument::as_expression)?;
2296 is_import_meta_url_expression(base).then_some(source)
2297}
2298
2299fn is_import_meta_url_expression(expr: &Expression) -> bool {
2300 if let Expression::StaticMemberExpression(member) = expr {
2301 member.property.name == "url"
2302 && matches!(
2303 member.object,
2304 Expression::ImportMeta(_) | Expression::NewTarget(_)
2305 )
2306 } else {
2307 false
2308 }
2309}
2310
2311fn join_path_segments(segments: &[String]) -> String {
2312 let mut joined = PathBuf::new();
2313 for segment in segments {
2314 joined.push(segment);
2315 }
2316 joined.to_string_lossy().replace('\\', "/")
2317}
2318
2319fn expression_to_alias_pairs_kinded(expr: &Expression) -> Vec<(String, String, bool)> {
2324 match expr {
2325 Expression::ObjectExpression(obj) => obj
2326 .properties
2327 .iter()
2328 .filter_map(|prop| {
2329 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
2330 return None;
2331 };
2332 let find = property_key_to_string(&prop.key)?;
2333 let (replacement, is_bare) =
2334 alias_replacement_kinded(&prop.value, PathScope::Bare)?;
2335 Some((find, replacement, is_bare))
2336 })
2337 .collect(),
2338 Expression::ArrayExpression(arr) => arr
2339 .elements
2340 .iter()
2341 .filter_map(|element| {
2342 let Expression::ObjectExpression(obj) = element.as_expression()? else {
2343 return None;
2344 };
2345 let find = find_property(obj, "find")
2346 .and_then(|prop| expression_to_string(&prop.value))?;
2347 let (replacement, is_bare) = find_property(obj, "replacement")
2348 .and_then(|prop| alias_replacement_kinded(&prop.value, PathScope::Bare))?;
2349 Some((find, replacement, is_bare))
2350 })
2351 .collect(),
2352 _ => Vec::new(),
2353 }
2354}
2355
2356fn alias_replacement_kinded(expr: &Expression, scope: PathScope<'_>) -> Option<(String, bool)> {
2363 match expr {
2364 Expression::ParenthesizedExpression(paren) => {
2365 alias_replacement_kinded(&paren.expression, scope)
2366 }
2367 Expression::TSAsExpression(ts_as) => alias_replacement_kinded(&ts_as.expression, scope),
2368 Expression::TSSatisfiesExpression(ts_sat) => {
2369 alias_replacement_kinded(&ts_sat.expression, scope)
2370 }
2371 Expression::StringLiteral(s) => {
2372 let value = s.value.to_string();
2373 let is_bare =
2374 !value.starts_with("./") && !value.starts_with("../") && !value.starts_with('/');
2375 Some((value, is_bare))
2376 }
2377 Expression::ArrayExpression(arr) => arr
2381 .elements
2382 .iter()
2383 .find_map(ArrayExpressionElement::as_expression)
2384 .and_then(|element| alias_replacement_kinded(element, scope)),
2385 _ => path_string_in(expr, scope).map(|value| (value, false)),
2386 }
2387}
2388
2389const MAX_ALIAS_RESOLVE_DEPTH: usize = 8;
2395
2396const ALIAS_SIBLING_EXTS: [&str; 6] = ["js", "mjs", "cjs", "ts", "mts", "cts"];
2403
2404fn resolve_alias_pairs_kinded(
2421 program: &Program,
2422 config_path: &Path,
2423 expr: &Expression,
2424 visited: &mut FxHashSet<PathBuf>,
2425 depth: usize,
2426) -> Vec<(String, String, bool)> {
2427 match expr {
2428 Expression::ParenthesizedExpression(paren) => {
2429 resolve_alias_pairs_kinded(program, config_path, &paren.expression, visited, depth)
2430 }
2431 Expression::TSAsExpression(ts_as) => {
2432 resolve_alias_pairs_kinded(program, config_path, &ts_as.expression, visited, depth)
2433 }
2434 Expression::TSSatisfiesExpression(ts_sat) => {
2435 resolve_alias_pairs_kinded(program, config_path, &ts_sat.expression, visited, depth)
2436 }
2437 Expression::ObjectExpression(obj) => {
2438 resolve_object_alias_pairs_kinded(program, config_path, obj, visited, depth)
2439 }
2440 Expression::ArrayExpression(arr) => {
2441 resolve_array_alias_pairs_kinded(program, config_path, arr, visited, depth)
2442 }
2443 Expression::Identifier(id) => {
2444 resolve_identifier_alias_pairs(program, config_path, id.name.as_str(), visited, depth)
2445 }
2446 _ => Vec::new(),
2447 }
2448}
2449
2450fn resolve_object_alias_pairs_kinded(
2453 program: &Program,
2454 config_path: &Path,
2455 obj: &ObjectExpression,
2456 visited: &mut FxHashSet<PathBuf>,
2457 depth: usize,
2458) -> Vec<(String, String, bool)> {
2459 let mut pairs = Vec::new();
2460 for prop in &obj.properties {
2461 match prop {
2462 ObjectPropertyKind::ObjectProperty(prop) => {
2463 if let Some(find) = property_key_to_string(&prop.key)
2464 && let Some((replacement, is_bare)) =
2465 alias_replacement_kinded(&prop.value, PathScope::Program(program))
2466 {
2467 pairs.push((find, replacement, is_bare));
2468 }
2469 }
2470 ObjectPropertyKind::SpreadProperty(spread) => {
2472 pairs.extend(resolve_alias_pairs_kinded(
2473 program,
2474 config_path,
2475 &spread.argument,
2476 visited,
2477 depth,
2478 ));
2479 }
2480 }
2481 }
2482 pairs
2483}
2484
2485fn resolve_array_alias_pairs_kinded(
2488 program: &Program,
2489 config_path: &Path,
2490 arr: &ArrayExpression,
2491 visited: &mut FxHashSet<PathBuf>,
2492 depth: usize,
2493) -> Vec<(String, String, bool)> {
2494 let mut pairs = Vec::new();
2495 for element in &arr.elements {
2496 match element {
2497 ArrayExpressionElement::SpreadElement(spread) => {
2499 pairs.extend(resolve_alias_pairs_kinded(
2500 program,
2501 config_path,
2502 &spread.argument,
2503 visited,
2504 depth,
2505 ));
2506 }
2507 _ => {
2508 if let Some(Expression::ObjectExpression(obj)) = element.as_expression()
2509 && let Some(find) = find_property(obj, "find")
2510 .and_then(|prop| expression_to_string(&prop.value))
2511 && let Some((replacement, is_bare)) = find_property(obj, "replacement")
2512 .and_then(|prop| {
2513 alias_replacement_kinded(&prop.value, PathScope::Program(program))
2514 })
2515 {
2516 pairs.push((find, replacement, is_bare));
2517 }
2518 }
2519 }
2520 }
2521 pairs
2522}
2523
2524fn resolve_identifier_alias_pairs(
2527 program: &Program,
2528 config_path: &Path,
2529 name: &str,
2530 visited: &mut FxHashSet<PathBuf>,
2531 depth: usize,
2532) -> Vec<(String, String, bool)> {
2533 if depth >= MAX_ALIAS_RESOLVE_DEPTH {
2534 return Vec::new();
2535 }
2536 if let Some(init) = find_variable_init_expression(program, name) {
2538 return resolve_alias_pairs_kinded(program, config_path, init, visited, depth + 1);
2539 }
2540 let Some((specifier, imported_name)) = find_relative_import_binding(program, name) else {
2542 return Vec::new();
2543 };
2544 resolve_imported_alias_pairs(
2545 config_path,
2546 &specifier,
2547 imported_name.as_deref(),
2548 visited,
2549 depth + 1,
2550 )
2551}
2552
2553fn resolve_imported_alias_pairs(
2556 config_path: &Path,
2557 specifier: &str,
2558 imported_name: Option<&str>,
2559 visited: &mut FxHashSet<PathBuf>,
2560 depth: usize,
2561) -> Vec<(String, String, bool)> {
2562 let Some((sibling_path, sibling_source)) = resolve_sibling_module(config_path, specifier)
2563 else {
2564 return Vec::new();
2565 };
2566 if !visited.insert(sibling_path.clone()) {
2567 return Vec::new();
2568 }
2569 extract_from_source(&sibling_source, &sibling_path, |program| {
2570 let init = find_exported_init(program, imported_name)?;
2571 let pairs = resolve_alias_pairs_kinded(program, &sibling_path, init, visited, depth);
2572 (!pairs.is_empty()).then_some(pairs)
2573 })
2574 .unwrap_or_default()
2575}
2576
2577fn find_variable_init_expression<'a>(
2582 program: &'a Program<'a>,
2583 name: &str,
2584) -> Option<&'a Expression<'a>> {
2585 for decl in top_level_variable_declarations(program) {
2586 for declarator in &decl.declarations {
2587 if let BindingPattern::BindingIdentifier(id) = &declarator.id
2588 && id.name == name
2589 && let Some(init) = &declarator.init
2590 {
2591 return Some(init);
2592 }
2593 }
2594 }
2595 None
2596}
2597
2598pub(crate) fn find_module_export_expression<'a>(
2601 program: &'a Program<'a>,
2602) -> Option<&'a Expression<'a>> {
2603 program.body.iter().find_map(|stmt| match stmt {
2604 Statement::ExportDefaultDeclaration(decl) => decl.declaration.as_expression(),
2605 Statement::ExpressionStatement(expr_stmt) => match &expr_stmt.expression {
2606 Expression::AssignmentExpression(assign) if is_module_exports_target(&assign.left) => {
2607 Some(&assign.right)
2608 }
2609 _ => None,
2610 },
2611 _ => None,
2612 })
2613}
2614
2615pub(crate) fn find_exported_init<'a>(
2620 program: &'a Program<'a>,
2621 name: Option<&str>,
2622) -> Option<&'a Expression<'a>> {
2623 match name {
2624 Some(name) => find_variable_init_expression(program, name),
2625 None => program.body.iter().find_map(|stmt| {
2626 if let Statement::ExportDefaultDeclaration(decl) = stmt {
2627 decl.declaration.as_expression()
2628 } else {
2629 None
2630 }
2631 }),
2632 }
2633}
2634
2635pub(crate) fn find_relative_import_binding(
2640 program: &Program,
2641 name: &str,
2642) -> Option<(String, Option<String>)> {
2643 for stmt in &program.body {
2644 let Statement::ImportDeclaration(decl) = stmt else {
2645 continue;
2646 };
2647 let specifier = decl.source.value.as_str();
2648 if !is_relative_specifier(specifier) {
2649 continue;
2650 }
2651 let Some(specifiers) = &decl.specifiers else {
2652 continue;
2653 };
2654 for spec in specifiers {
2655 match spec {
2656 ImportDeclarationSpecifier::ImportSpecifier(spec) if spec.local.name == name => {
2657 return Some((
2658 specifier.to_string(),
2659 Some(spec.imported.name().to_string()),
2660 ));
2661 }
2662 ImportDeclarationSpecifier::ImportDefaultSpecifier(spec)
2663 if spec.local.name == name =>
2664 {
2665 return Some((specifier.to_string(), None));
2666 }
2667 _ => {}
2668 }
2669 }
2670 }
2671 None
2672}
2673
2674pub(crate) fn is_relative_specifier(specifier: &str) -> bool {
2677 specifier.starts_with("./") || specifier.starts_with("../") || specifier.starts_with('/')
2678}
2679
2680pub(crate) fn resolve_sibling_module(
2686 config_path: &Path,
2687 specifier: &str,
2688) -> Option<(PathBuf, String)> {
2689 let parent = config_path.parent().unwrap_or(config_path);
2690 let direct = parent.join(specifier);
2691 if let Ok(source) = std::fs::read_to_string(&direct) {
2692 return Some((direct, source));
2693 }
2694 for ext in ALIAS_SIBLING_EXTS {
2695 let candidate = parent.join(format!("{specifier}.{ext}"));
2696 if let Ok(source) = std::fs::read_to_string(&candidate) {
2697 return Some((candidate, source));
2698 }
2699 }
2700 for ext in ALIAS_SIBLING_EXTS {
2701 let candidate = direct.join(format!("index.{ext}"));
2702 if let Ok(source) = std::fs::read_to_string(&candidate) {
2703 return Some((candidate, source));
2704 }
2705 }
2706 None
2707}
2708
2709fn find_default_export_array<'a>(program: &'a Program<'a>) -> Option<&'a ArrayExpression<'a>> {
2714 for stmt in &program.body {
2715 if let Statement::ExportDefaultDeclaration(decl) = stmt
2716 && let Some(expr) = decl.declaration.as_expression()
2717 {
2718 return array_from_expression(expr);
2719 }
2720 }
2721 None
2722}
2723
2724fn array_from_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
2725 match expr {
2726 Expression::ArrayExpression(arr) => Some(arr),
2727 Expression::ParenthesizedExpression(paren) => array_from_expression(&paren.expression),
2728 Expression::TSAsExpression(ts_as) => array_from_expression(&ts_as.expression),
2729 Expression::TSSatisfiesExpression(ts_sat) => array_from_expression(&ts_sat.expression),
2730 Expression::CallExpression(call) => call
2731 .arguments
2732 .first()
2733 .and_then(Argument::as_expression)
2734 .and_then(array_from_expression),
2735 _ => None,
2736 }
2737}
2738
2739pub(crate) fn lexical_normalize(path: &Path) -> PathBuf {
2740 let mut normalized = PathBuf::new();
2741
2742 for component in path.components() {
2743 match component {
2744 std::path::Component::CurDir => {}
2745 std::path::Component::ParentDir => {
2746 normalized.pop();
2747 }
2748 _ => normalized.push(component.as_os_str()),
2749 }
2750 }
2751
2752 normalized
2753}
2754
2755pub(crate) fn is_package_specifier(specifier: &str) -> bool {
2759 !specifier.is_empty()
2760 && specifier != "."
2761 && specifier != ".."
2762 && !specifier.starts_with("./")
2763 && !specifier.starts_with("../")
2764 && !specifier.starts_with('/')
2765 && !specifier.contains(':')
2766 && !specifier.contains('\\')
2767 && !specifier.chars().any(char::is_whitespace)
2768}
2769
2770fn expression_to_string_array(expr: &Expression) -> Vec<String> {
2772 match expr {
2773 Expression::ArrayExpression(arr) => arr
2774 .elements
2775 .iter()
2776 .filter_map(|el| match el {
2777 ArrayExpressionElement::SpreadElement(_) => None,
2778 _ => el.as_expression().and_then(expression_to_string),
2779 })
2780 .collect(),
2781 _ => vec![],
2782 }
2783}
2784
2785fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
2790 let mut values = Vec::new();
2791 match expr {
2792 Expression::StringLiteral(s) => {
2793 values.push(s.value.to_string());
2794 }
2795 Expression::ArrayExpression(arr) => {
2796 for el in &arr.elements {
2797 if let Some(inner) = el.as_expression() {
2798 match inner {
2799 Expression::StringLiteral(s) => {
2800 values.push(s.value.to_string());
2801 }
2802 Expression::ArrayExpression(sub_arr) => {
2803 if let Some(first) = sub_arr.elements.first()
2804 && let Some(first_expr) = first.as_expression()
2805 && let Some(s) = expression_to_string(first_expr)
2806 {
2807 values.push(s);
2808 }
2809 }
2810 _ => {}
2811 }
2812 }
2813 }
2814 }
2815 Expression::ObjectExpression(obj) => {
2816 for prop in &obj.properties {
2817 if let ObjectPropertyKind::ObjectProperty(p) = prop {
2818 match &p.value {
2819 Expression::StringLiteral(s) => {
2820 values.push(s.value.to_string());
2821 }
2822 Expression::ArrayExpression(sub_arr) => {
2823 if let Some(first) = sub_arr.elements.first()
2824 && let Some(first_expr) = first.as_expression()
2825 && let Some(s) = expression_to_string(first_expr)
2826 {
2827 values.push(s);
2828 }
2829 }
2830 _ => {}
2831 }
2832 }
2833 }
2834 }
2835 _ => {}
2836 }
2837 values
2838}
2839
2840fn collect_shallow_string_or_object_property_values(
2842 expr: &Expression,
2843 object_property: &str,
2844) -> Vec<String> {
2845 match expr {
2846 Expression::ArrayExpression(arr) => arr
2847 .elements
2848 .iter()
2849 .filter_map(|element| {
2850 element
2851 .as_expression()
2852 .and_then(|expr| shallow_string_or_object_property(expr, object_property))
2853 })
2854 .collect(),
2855 _ => shallow_string_or_object_property(expr, object_property)
2856 .into_iter()
2857 .collect(),
2858 }
2859}
2860
2861fn shallow_string_or_object_property(expr: &Expression, object_property: &str) -> Option<String> {
2862 match expr {
2863 Expression::ParenthesizedExpression(paren) => {
2864 shallow_string_or_object_property(&paren.expression, object_property)
2865 }
2866 Expression::TSSatisfiesExpression(ts_sat) => {
2867 shallow_string_or_object_property(&ts_sat.expression, object_property)
2868 }
2869 Expression::TSAsExpression(ts_as) => {
2870 shallow_string_or_object_property(&ts_as.expression, object_property)
2871 }
2872 Expression::ArrayExpression(sub_arr) => sub_arr
2873 .elements
2874 .first()
2875 .and_then(ArrayExpressionElement::as_expression)
2876 .and_then(expression_to_string),
2877 Expression::ObjectExpression(obj) => {
2878 find_property(obj, object_property).and_then(|prop| expression_to_string(&prop.value))
2879 }
2880 _ => expression_to_string(expr),
2881 }
2882}
2883
2884fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
2886 match expr {
2887 Expression::StringLiteral(s) => {
2888 values.push(s.value.to_string());
2889 }
2890 Expression::ArrayExpression(arr) => {
2891 for el in &arr.elements {
2892 if let Some(expr) = el.as_expression() {
2893 collect_all_string_values(expr, values);
2894 }
2895 }
2896 }
2897 Expression::ObjectExpression(obj) => {
2898 for prop in &obj.properties {
2899 if let ObjectPropertyKind::ObjectProperty(p) = prop {
2900 collect_all_string_values(&p.value, values);
2901 }
2902 }
2903 }
2904 _ => {}
2905 }
2906}
2907
2908fn property_key_to_string(key: &PropertyKey) -> Option<String> {
2910 match key {
2911 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
2912 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
2913 _ => None,
2914 }
2915}
2916
2917fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
2919 if path.is_empty() {
2920 return None;
2921 }
2922 let prop = find_property(obj, path[0])?;
2923 if path.len() == 1 {
2924 if let Expression::ObjectExpression(nested) = &prop.value {
2925 let keys = nested
2926 .properties
2927 .iter()
2928 .filter_map(|p| {
2929 if let ObjectPropertyKind::ObjectProperty(p) = p {
2930 property_key_to_string(&p.key)
2931 } else {
2932 None
2933 }
2934 })
2935 .collect();
2936 return Some(keys);
2937 }
2938 return None;
2939 }
2940 if let Expression::ObjectExpression(nested) = &prop.value {
2941 get_nested_object_keys(nested, &path[1..])
2942 } else {
2943 None
2944 }
2945}
2946
2947fn get_nested_expression<'a>(
2949 obj: &'a ObjectExpression<'a>,
2950 path: &[&str],
2951) -> Option<&'a Expression<'a>> {
2952 if path.is_empty() {
2953 return None;
2954 }
2955 let prop = find_property(obj, path[0])?;
2956 if path.len() == 1 {
2957 return Some(&prop.value);
2958 }
2959 if let Expression::ObjectExpression(nested) = &prop.value {
2960 get_nested_expression(nested, &path[1..])
2961 } else {
2962 None
2963 }
2964}
2965
2966fn get_nested_string_or_array(
2968 obj: &ObjectExpression,
2969 path: &[&str],
2970 scope: PathScope<'_>,
2971) -> Option<Vec<String>> {
2972 if path.is_empty() {
2973 return None;
2974 }
2975 if path.len() == 1 {
2976 let prop = find_property(obj, path[0])?;
2977 return Some(string_or_array_in(&prop.value, scope));
2978 }
2979 let prop = find_property(obj, path[0])?;
2980 if let Expression::ObjectExpression(nested) = &prop.value {
2981 get_nested_string_or_array(nested, &path[1..], scope)
2982 } else {
2983 None
2984 }
2985}
2986
2987pub(crate) fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
2995 string_or_array_in(expr, PathScope::Bare)
2996}
2997
2998fn string_or_array_in(expr: &Expression, scope: PathScope<'_>) -> Vec<String> {
2999 match expr {
3000 Expression::StringLiteral(s) => vec![s.value.to_string()],
3001 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
3002 .quasis
3003 .first()
3004 .map(|q| vec![q.value.raw.to_string()])
3005 .unwrap_or_default(),
3006 Expression::ArrayExpression(arr) => arr
3007 .elements
3008 .iter()
3009 .filter_map(|el| el.as_expression())
3010 .flat_map(|e| match e {
3011 Expression::ObjectExpression(obj) => find_property(obj, "input")
3012 .map(|p| string_or_array_in(&p.value, scope))
3013 .unwrap_or_default(),
3014 _ => path_string_in(e, scope).into_iter().collect(),
3015 })
3016 .collect(),
3017 Expression::ObjectExpression(obj) => obj
3018 .properties
3019 .iter()
3020 .flat_map(|p| {
3021 if let ObjectPropertyKind::ObjectProperty(p) = p {
3022 match &p.value {
3023 Expression::ArrayExpression(_) => string_or_array_in(&p.value, scope),
3024 Expression::ObjectExpression(value_obj) => {
3025 find_property(value_obj, "import")
3026 .map(|import_prop| string_or_array_in(&import_prop.value, scope))
3027 .unwrap_or_default()
3028 }
3029 _ => path_string_in(&p.value, scope).into_iter().collect(),
3030 }
3031 } else {
3032 Vec::new()
3033 }
3034 })
3035 .collect(),
3036 _ => path_string_in(expr, scope).into_iter().collect(),
3037 }
3038}
3039
3040fn collect_require_sources(expr: &Expression) -> Vec<String> {
3042 let mut sources = Vec::new();
3043 match expr {
3044 Expression::CallExpression(call) if is_require_call(call) => {
3045 if let Some(s) = get_require_source(call) {
3046 sources.push(s);
3047 }
3048 }
3049 Expression::ArrayExpression(arr) => {
3050 for el in &arr.elements {
3051 if let Some(inner) = el.as_expression() {
3052 match inner {
3053 Expression::CallExpression(call) if is_require_call(call) => {
3054 if let Some(s) = get_require_source(call) {
3055 sources.push(s);
3056 }
3057 }
3058 Expression::ArrayExpression(sub_arr) => {
3059 if let Some(first) = sub_arr.elements.first()
3060 && let Some(Expression::CallExpression(call)) =
3061 first.as_expression()
3062 && is_require_call(call)
3063 && let Some(s) = get_require_source(call)
3064 {
3065 sources.push(s);
3066 }
3067 }
3068 _ => {}
3069 }
3070 }
3071 }
3072 }
3073 _ => {}
3074 }
3075 sources
3076}
3077
3078pub(crate) fn is_require_call(call: &CallExpression) -> bool {
3080 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
3081}
3082
3083pub(crate) fn get_require_source(call: &CallExpression) -> Option<String> {
3085 call.arguments.first().and_then(|arg| {
3086 if let Argument::StringLiteral(s) = arg {
3087 Some(s.value.to_string())
3088 } else {
3089 None
3090 }
3091 })
3092}
3093
3094#[cfg(test)]
3095mod tests {
3096 use super::*;
3097 use std::path::PathBuf;
3098
3099 fn js_path() -> PathBuf {
3100 PathBuf::from("config.js")
3101 }
3102
3103 fn ts_path() -> PathBuf {
3104 PathBuf::from("config.ts")
3105 }
3106
3107 #[test]
3108 fn extract_config_array_tuple_heads_reads_first_string_of_each_tuple() {
3109 let source = r#"
3110 export default defineNuxtConfig({
3111 modules: ["@nuxt/image", ["@nuxt/content", { watch: false }], [someModule, {}], []]
3112 });
3113 "#;
3114 assert_eq!(
3115 extract_config_array_tuple_heads(source, &ts_path(), &["modules"]),
3116 vec!["@nuxt/content".to_string()]
3117 );
3118 }
3119
3120 #[test]
3121 fn extract_lazy_imports_bare_arrows() {
3122 let source = r"
3123 import { defineConfig } from '@adonisjs/core/app'
3124 export default defineConfig({
3125 preloads: [
3126 () => import('#start/routes'),
3127 () => import('#start/kernel'),
3128 ],
3129 })
3130 ";
3131 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["preloads"]);
3132 assert_eq!(specs, vec!["#start/routes", "#start/kernel"]);
3133 }
3134
3135 #[test]
3136 fn extract_lazy_imports_object_form_with_file_key() {
3137 let source = r"
3138 export default defineConfig({
3139 providers: [
3140 () => import('@adonisjs/core/providers/app_provider'),
3141 {
3142 file: () => import('@adonisjs/core/providers/repl_provider'),
3143 environment: ['repl', 'test'],
3144 },
3145 ],
3146 })
3147 ";
3148 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
3149 assert_eq!(
3150 specs,
3151 vec![
3152 "@adonisjs/core/providers/app_provider",
3153 "@adonisjs/core/providers/repl_provider",
3154 ]
3155 );
3156 }
3157
3158 #[test]
3159 fn extract_lazy_imports_block_body_with_return() {
3160 let source = r"
3161 export default defineConfig({
3162 commands: [
3163 () => { return import('@adonisjs/core/commands') },
3164 ],
3165 })
3166 ";
3167 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
3168 assert_eq!(specs, vec!["@adonisjs/core/commands"]);
3169 }
3170
3171 #[test]
3172 fn extract_lazy_imports_skips_unknown_element_shapes() {
3173 let source = r"
3174 export default defineConfig({
3175 commands: [
3176 'string-entry',
3177 42,
3178 { other: 'value' },
3179 () => import('@adonisjs/lucid/commands'),
3180 ],
3181 })
3182 ";
3183 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
3184 assert_eq!(specs, vec!["@adonisjs/lucid/commands"]);
3185 }
3186
3187 #[test]
3188 fn extract_lazy_imports_missing_property_returns_empty() {
3189 let source = r"
3190 export default defineConfig({
3191 preloads: [() => import('#start/routes')],
3192 })
3193 ";
3194 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
3195 assert!(specs.is_empty());
3196 }
3197
3198 #[test]
3199 fn extract_imports_basic() {
3200 let source = r"
3201 import foo from 'foo-pkg';
3202 import { bar } from '@scope/bar';
3203 export default {};
3204 ";
3205 let imports = extract_imports(source, &js_path());
3206 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
3207 }
3208
3209 #[test]
3210 fn extract_default_export_object_property() {
3211 let source = r#"export default { testDir: "./tests" };"#;
3212 let val = extract_config_string(source, &js_path(), &["testDir"]);
3213 assert_eq!(val, Some("./tests".to_string()));
3214 }
3215
3216 #[test]
3217 fn extract_define_config_property() {
3218 let source = r#"
3219 import { defineConfig } from 'vitest/config';
3220 export default defineConfig({
3221 test: {
3222 include: ["**/*.test.ts", "**/*.spec.ts"],
3223 setupFiles: ["./test/setup.ts"]
3224 }
3225 });
3226 "#;
3227 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
3228 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
3229
3230 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
3231 assert_eq!(setup, vec!["./test/setup.ts"]);
3232 }
3233
3234 #[test]
3235 fn extract_module_exports_property() {
3236 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
3237 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
3238 assert_eq!(val, Some("jsdom".to_string()));
3239 }
3240
3241 #[test]
3242 fn extract_nested_string_array() {
3243 let source = r#"
3244 export default {
3245 resolve: {
3246 alias: {
3247 "@": "./src"
3248 }
3249 },
3250 test: {
3251 include: ["src/**/*.test.ts"]
3252 }
3253 };
3254 "#;
3255 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
3256 assert_eq!(include, vec!["src/**/*.test.ts"]);
3257 }
3258
3259 #[test]
3260 fn extract_addons_array() {
3261 let source = r#"
3262 export default {
3263 addons: [
3264 "@storybook/addon-a11y",
3265 "@storybook/addon-docs",
3266 "@storybook/addon-links"
3267 ]
3268 };
3269 "#;
3270 let addons = extract_config_property_strings(source, &ts_path(), "addons");
3271 assert_eq!(
3272 addons,
3273 vec![
3274 "@storybook/addon-a11y",
3275 "@storybook/addon-docs",
3276 "@storybook/addon-links"
3277 ]
3278 );
3279 }
3280
3281 #[test]
3282 fn handle_empty_config() {
3283 let source = "";
3284 let result = extract_config_string(source, &js_path(), &["key"]);
3285 assert_eq!(result, None);
3286 }
3287
3288 #[test]
3289 fn object_keys_postcss_plugins() {
3290 let source = r"
3291 module.exports = {
3292 plugins: {
3293 autoprefixer: {},
3294 tailwindcss: {},
3295 'postcss-import': {}
3296 }
3297 };
3298 ";
3299 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3300 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
3301 }
3302
3303 #[test]
3304 fn object_keys_nested_path() {
3305 let source = r"
3306 export default {
3307 build: {
3308 plugins: {
3309 minify: {},
3310 compress: {}
3311 }
3312 }
3313 };
3314 ";
3315 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
3316 assert_eq!(keys, vec!["minify", "compress"]);
3317 }
3318
3319 #[test]
3320 fn object_keys_empty_object() {
3321 let source = r"export default { plugins: {} };";
3322 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3323 assert!(keys.is_empty());
3324 }
3325
3326 #[test]
3327 fn object_keys_non_object_returns_empty() {
3328 let source = r#"export default { plugins: ["a", "b"] };"#;
3329 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3330 assert!(keys.is_empty());
3331 }
3332
3333 #[test]
3334 fn string_or_array_single_string() {
3335 let source = r#"export default { entry: "./src/index.js" };"#;
3336 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3337 assert_eq!(result, vec!["./src/index.js"]);
3338 }
3339
3340 #[test]
3341 fn string_or_array_array() {
3342 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
3343 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3344 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
3345 }
3346
3347 #[test]
3348 fn string_or_array_object_values() {
3349 let source =
3350 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
3351 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3352 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
3353 }
3354
3355 #[test]
3356 fn array_or_object_string_or_array_reads_each_array_element() {
3357 let source = r#"
3358 const shared = { format: ["esm"] };
3359 export default defineConfig([
3360 { entry: "./src/a.ts" },
3361 ({ ...shared, entry: { b: "./src/b.ts" } }),
3362 { entry: ["./src/c.ts"] } satisfies Options,
3363 { format: ["cjs"] },
3364 shared,
3365 ] as Options[]);
3366 "#;
3367 let result = extract_config_array_or_object_string_or_array(source, &ts_path(), &["entry"]);
3368 assert_eq!(result, vec!["./src/a.ts", "./src/b.ts", "./src/c.ts"]);
3369 }
3370
3371 #[test]
3372 fn array_or_object_string_or_array_falls_back_to_config_object() {
3373 let source = r#"export default defineConfig({ entry: { main: "./src/main.ts" } });"#;
3374 let result = extract_config_array_or_object_string_or_array(source, &ts_path(), &["entry"]);
3375 assert_eq!(result, vec!["./src/main.ts"]);
3376 }
3377
3378 #[test]
3379 fn string_or_array_object_array_values() {
3380 let source = r#"export default { entry: { app: ["./src/polyfill.js", "./src/app.js"] } };"#;
3381 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3382 assert_eq!(result, vec!["./src/polyfill.js", "./src/app.js"]);
3383 }
3384
3385 #[test]
3386 fn string_or_array_webpack_entry_descriptors() {
3387 let source = r#"
3388 export default {
3389 entry: {
3390 app: {
3391 import: "./src/app.js",
3392 filename: "pages/app.js",
3393 dependOn: "shared",
3394 },
3395 admin: {
3396 import: ["./src/admin-polyfill.js", "./src/admin.js"],
3397 runtime: "runtime",
3398 },
3399 shared: ["react", "react-dom"],
3400 },
3401 };
3402 "#;
3403 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3404 assert_eq!(
3405 result,
3406 vec![
3407 "./src/app.js",
3408 "./src/admin-polyfill.js",
3409 "./src/admin.js",
3410 "react",
3411 "react-dom"
3412 ]
3413 );
3414 }
3415
3416 #[test]
3417 fn string_or_array_nested_path() {
3418 let source = r#"
3419 export default {
3420 build: {
3421 rollupOptions: {
3422 input: ["./index.html", "./about.html"]
3423 }
3424 }
3425 };
3426 "#;
3427 let result = extract_config_string_or_array(
3428 source,
3429 &js_path(),
3430 &["build", "rollupOptions", "input"],
3431 );
3432 assert_eq!(result, vec!["./index.html", "./about.html"]);
3433 }
3434
3435 #[test]
3436 fn string_or_array_template_literal() {
3437 let source = r"export default { entry: `./src/index.js` };";
3438 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3439 assert_eq!(result, vec!["./src/index.js"]);
3440 }
3441
3442 #[test]
3443 fn string_or_array_object_path_helper_values() {
3444 let source = r#"
3445 import { resolve, join } from "node:path";
3446 import path from "node:path";
3447 export default {
3448 build: {
3449 rollupOptions: {
3450 input: {
3451 app: resolve(__dirname, "src/app.ts"),
3452 modal: path.resolve(__dirname, "src/modal.ts"),
3453 tabs: join(__dirname, "src/tabs.ts"),
3454 styles: resolve(__dirname, "src/index.css"),
3455 },
3456 },
3457 },
3458 };
3459 "#;
3460 let result = extract_config_string_or_array(
3461 source,
3462 &js_path(),
3463 &["build", "rollupOptions", "input"],
3464 );
3465 assert_eq!(
3466 result,
3467 vec!["src/app.ts", "src/modal.ts", "src/tabs.ts", "src/index.css"]
3468 );
3469 }
3470
3471 #[test]
3472 fn string_or_array_array_path_helper_values() {
3473 let source = r#"
3474 import { resolve } from "node:path";
3475 export default {
3476 build: {
3477 rollupOptions: {
3478 input: [resolve(__dirname, "src/a.ts"), "./src/b.ts"],
3479 },
3480 },
3481 };
3482 "#;
3483 let result = extract_config_string_or_array(
3484 source,
3485 &js_path(),
3486 &["build", "rollupOptions", "input"],
3487 );
3488 assert_eq!(result, vec!["src/a.ts", "./src/b.ts"]);
3489 }
3490
3491 #[test]
3492 fn string_or_array_top_level_path_helper_call() {
3493 let source = r#"
3494 import { resolve } from "node:path";
3495 export default { build: { lib: { entry: resolve(__dirname, "src/index.ts") } } };
3496 "#;
3497 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
3498 assert_eq!(result, vec!["src/index.ts"]);
3499 }
3500
3501 #[test]
3502 fn string_or_array_import_meta_dirname_anchor() {
3503 let source = r#"
3504 import { resolve } from "node:path";
3505 export default {
3506 build: { lib: { entry: resolve(import.meta.dirname, "src/index.ts") } },
3507 };
3508 "#;
3509 let result = extract_config_string_or_array(source, &ts_path(), &["build", "lib", "entry"]);
3510 assert_eq!(result, vec!["src/index.ts"]);
3511 }
3512
3513 #[test]
3514 fn string_or_array_non_literal_path_helper_args_dropped() {
3515 let source = r#"
3516 import { resolve } from "node:path";
3517 export default { build: { lib: { entry: resolve(baseDir, "src/index.ts") } } };
3518 "#;
3519 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
3520 assert!(
3521 result.is_empty(),
3522 "non-literal path-helper args must be dropped: {result:?}"
3523 );
3524 }
3525
3526 #[test]
3527 fn require_strings_array() {
3528 let source = r"
3529 module.exports = {
3530 plugins: [
3531 require('autoprefixer'),
3532 require('postcss-import')
3533 ]
3534 };
3535 ";
3536 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3537 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
3538 }
3539
3540 #[test]
3541 fn require_strings_with_tuples() {
3542 let source = r"
3543 module.exports = {
3544 plugins: [
3545 require('autoprefixer'),
3546 [require('postcss-preset-env'), { stage: 3 }]
3547 ]
3548 };
3549 ";
3550 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3551 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
3552 }
3553
3554 #[test]
3555 fn require_strings_empty_array() {
3556 let source = r"module.exports = { plugins: [] };";
3557 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3558 assert!(deps.is_empty());
3559 }
3560
3561 #[test]
3562 fn require_strings_no_require_calls() {
3563 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
3564 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3565 assert!(deps.is_empty());
3566 }
3567
3568 #[test]
3569 fn extract_aliases_from_object_with_file_url_to_path() {
3570 let source = r#"
3571 import { defineConfig } from 'vite';
3572 import { fileURLToPath, URL } from 'node:url';
3573
3574 export default defineConfig({
3575 resolve: {
3576 alias: {
3577 "@": fileURLToPath(new URL("./src", import.meta.url))
3578 }
3579 }
3580 });
3581 "#;
3582
3583 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
3584 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
3585 }
3586
3587 #[test]
3588 fn extract_aliases_from_array_form() {
3589 let source = r#"
3590 export default {
3591 resolve: {
3592 alias: [
3593 { find: "@", replacement: "./src" },
3594 { find: "$utils", replacement: "src/lib/utils" }
3595 ]
3596 }
3597 };
3598 "#;
3599
3600 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
3601 assert_eq!(
3602 aliases,
3603 vec![
3604 ("@".to_string(), "./src".to_string()),
3605 ("$utils".to_string(), "src/lib/utils".to_string())
3606 ]
3607 );
3608 }
3609
3610 #[test]
3611 fn extract_aliases_from_object_with_array_values() {
3612 let source = r#"
3613 ({
3614 compilerOptions: {
3615 paths: {
3616 "@/*": ["./src/*"],
3617 "@shared/*": ["./shared/*", "./fallback/*"]
3618 }
3619 }
3620 })
3621 "#;
3622
3623 let aliases = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
3624 assert_eq!(
3625 aliases,
3626 vec![
3627 ("@/*".to_string(), "./src/*".to_string()),
3628 ("@shared/*".to_string(), "./shared/*".to_string())
3629 ]
3630 );
3631 }
3632
3633 #[test]
3634 fn extract_array_object_strings_mixed_forms() {
3635 let source = r#"
3636 export default {
3637 components: [
3638 "~/components",
3639 { path: "@/feature-components" }
3640 ]
3641 };
3642 "#;
3643
3644 let values =
3645 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
3646 assert_eq!(
3647 values,
3648 vec![
3649 "~/components".to_string(),
3650 "@/feature-components".to_string()
3651 ]
3652 );
3653 }
3654
3655 #[test]
3656 fn extract_config_plugin_option_string_from_json() {
3657 let source = r#"{
3658 "expo": {
3659 "plugins": [
3660 ["expo-router", { "root": "src/app" }]
3661 ]
3662 }
3663 }"#;
3664
3665 let value = extract_config_plugin_option_string(
3666 source,
3667 &json_path(),
3668 &["expo", "plugins"],
3669 "expo-router",
3670 "root",
3671 );
3672
3673 assert_eq!(value, Some("src/app".to_string()));
3674 }
3675
3676 #[test]
3677 fn extract_config_plugin_option_string_from_top_level_plugins() {
3678 let source = r#"{
3679 "plugins": [
3680 ["expo-router", { "root": "./src/routes" }]
3681 ]
3682 }"#;
3683
3684 let value = extract_config_plugin_option_string_from_paths(
3685 source,
3686 &json_path(),
3687 &[&["plugins"], &["expo", "plugins"]],
3688 "expo-router",
3689 "root",
3690 );
3691
3692 assert_eq!(value, Some("./src/routes".to_string()));
3693 }
3694
3695 #[test]
3696 fn extract_config_plugin_option_string_from_ts_config() {
3697 let source = r"
3698 export default {
3699 expo: {
3700 plugins: [
3701 ['expo-router', { root: './src/app' }]
3702 ]
3703 }
3704 };
3705 ";
3706
3707 let value = extract_config_plugin_option_string(
3708 source,
3709 &ts_path(),
3710 &["expo", "plugins"],
3711 "expo-router",
3712 "root",
3713 );
3714
3715 assert_eq!(value, Some("./src/app".to_string()));
3716 }
3717
3718 #[test]
3719 fn extract_config_plugin_option_string_returns_none_when_plugin_missing() {
3720 let source = r#"{
3721 "expo": {
3722 "plugins": [
3723 ["expo-font", {}]
3724 ]
3725 }
3726 }"#;
3727
3728 let value = extract_config_plugin_option_string(
3729 source,
3730 &json_path(),
3731 &["expo", "plugins"],
3732 "expo-router",
3733 "root",
3734 );
3735
3736 assert_eq!(value, None);
3737 }
3738
3739 #[test]
3740 fn vite_react_babel_dependencies_extract_plain_tuple_and_prefixed_entries() {
3741 let source = r#"
3742 import react from "@vitejs/plugin-react";
3743
3744 export default defineConfig({
3745 plugins: [
3746 react({
3747 babel: {
3748 plugins: [
3749 "babel-plugin-plain",
3750 ["module:@preact/signals-react-transform", { mode: "auto" }],
3751 ],
3752 presets: [["@babel/preset-react", { runtime: "automatic" }]],
3753 },
3754 }),
3755 ],
3756 });
3757 "#;
3758
3759 let deps = extract_vite_plugin_option_dependencies(source, &ts_path());
3760
3761 assert_eq!(
3762 deps,
3763 vec![
3764 "babel-plugin-plain".to_string(),
3765 "@preact/signals-react-transform".to_string(),
3766 "@babel/preset-react".to_string(),
3767 ]
3768 );
3769 }
3770
3771 #[test]
3772 fn vite_react_babel_dependencies_support_default_alias_import() {
3773 let source = r#"
3774 import { default as viteReact } from "@vitejs/plugin-react";
3775
3776 export default {
3777 plugins: [
3778 viteReact({
3779 babel: {
3780 plugins: [["module:@scope/pkg/plugin", {}]],
3781 },
3782 }),
3783 ],
3784 };
3785 "#;
3786
3787 let deps = extract_vite_plugin_option_dependencies(source, &ts_path());
3788
3789 assert_eq!(deps, vec!["@scope/pkg".to_string()]);
3790 }
3791
3792 #[test]
3793 fn vite_react_babel_dependencies_ignore_unrelated_plugin_calls() {
3794 let source = r#"
3795 import vue from "@vitejs/plugin-vue";
3796
3797 export default {
3798 plugins: [
3799 vue({
3800 babel: {
3801 plugins: ["@preact/signals-react-transform"],
3802 },
3803 }),
3804 ],
3805 };
3806 "#;
3807
3808 let deps = extract_vite_plugin_option_dependencies(source, &ts_path());
3809
3810 assert!(deps.is_empty());
3811 }
3812
3813 #[test]
3814 fn vite_react_babel_dependencies_skip_relative_and_protocol_entries() {
3815 let source = r#"
3816 import react from "@vitejs/plugin-react";
3817
3818 export default {
3819 plugins: [
3820 react({
3821 babel: {
3822 plugins: ["./local-plugin", "module:./local-prefixed", "http://example.com/plugin"],
3823 },
3824 }),
3825 ],
3826 };
3827 "#;
3828
3829 let deps = extract_vite_plugin_option_dependencies(source, &ts_path());
3830
3831 assert!(deps.is_empty());
3832 }
3833
3834 #[test]
3835 fn normalize_config_path_relative_to_root() {
3836 let config_path = PathBuf::from("/project/vite.config.ts");
3837 let root = PathBuf::from("/project");
3838
3839 assert_eq!(
3840 normalize_config_path("./src/lib", &config_path, &root),
3841 Some("src/lib".to_string())
3842 );
3843 assert_eq!(
3844 normalize_config_path("/src/lib", &config_path, &root),
3845 Some("src/lib".to_string())
3846 );
3847 }
3848
3849 #[test]
3850 fn normalize_config_path_mixed_separators_and_parent_dirs() {
3851 let config_path = PathBuf::from("/project/config/vite.config.ts");
3852 let root = PathBuf::from("/project");
3853
3854 assert_eq!(
3855 normalize_config_path(".\\src\\..\\app\\lib", &config_path, &root),
3856 Some("config/app/lib".to_string())
3857 );
3858 }
3859
3860 #[test]
3863 fn normalize_config_path_reads_an_absolute_path_under_root_as_absolute() {
3864 let root = std::env::temp_dir().join("fallow-2806-project");
3865 let config_path = root.join("vite.config.ts");
3866 let absolute = path_to_config_string(&root.join("src").join("lib"));
3867
3868 assert_eq!(
3869 normalize_config_path(&absolute, &config_path, &root),
3870 Some("src/lib".to_string())
3871 );
3872 assert_eq!(
3873 normalize_filesystem_config_path(&absolute, &config_path, &root),
3874 Some("src/lib".to_string())
3875 );
3876 }
3877
3878 #[cfg(unix)]
3883 #[test]
3884 fn normalize_config_path_keeps_the_vite_root_in_root_reading() {
3885 let temp = tempfile::tempdir().expect("temp dir");
3886 let root = temp.path().join("src");
3887 std::fs::create_dir_all(&root).expect("root dir");
3888 let config_path = root.join("vite.config.ts");
3889 let root_string = path_to_config_string(&root);
3890 let nested = root_string.trim_start_matches('/').to_string();
3891 let under = format!("{root_string}/lib");
3892
3893 assert_eq!(
3894 normalize_config_path(&root_string, &config_path, &root),
3895 Some(nested.clone())
3896 );
3897 assert_eq!(
3898 normalize_config_path(&under, &config_path, &root),
3899 Some("lib".to_string())
3900 );
3901
3902 std::fs::create_dir_all(root.join(&nested)).expect("root-in-root dir");
3903 assert_eq!(
3904 normalize_config_path(&under, &config_path, &root),
3905 Some(format!("{nested}/lib"))
3906 );
3907 assert_eq!(
3908 normalize_filesystem_config_path(&under, &config_path, &root),
3909 Some("lib".to_string())
3910 );
3911 }
3912
3913 #[test]
3916 fn normalize_filesystem_config_path_does_not_read_a_leading_slash_as_root_relative() {
3917 let config_path = PathBuf::from("/project/config/webpack.config.js");
3918 let root = PathBuf::from("/project");
3919
3920 assert_eq!(
3921 normalize_filesystem_config_path("/src/lib", &config_path, &root),
3922 None
3923 );
3924 assert_eq!(
3925 normalize_filesystem_config_path("./src/lib", &config_path, &root),
3926 Some("config/src/lib".to_string())
3927 );
3928 assert_eq!(
3929 normalize_config_path("/src/lib", &config_path, &root),
3930 Some("src/lib".to_string())
3931 );
3932 }
3933
3934 #[test]
3935 fn normalize_config_path_leading_slash_stays_project_relative() {
3936 let config_path = PathBuf::from("/project/vite.config.ts");
3937 let root = PathBuf::from("/project");
3938
3939 assert_eq!(
3940 normalize_config_path("/src\\lib", &config_path, &root),
3941 Some("src/lib".to_string())
3942 );
3943 }
3944
3945 #[test]
3946 fn json_wrapped_in_parens_string() {
3947 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
3948 let val = extract_config_string(source, &js_path(), &["extends"]);
3949 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
3950 }
3951
3952 #[test]
3953 fn json_wrapped_in_parens_nested_array() {
3954 let source =
3955 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
3956 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
3957 assert_eq!(types, vec!["node", "jest"]);
3958
3959 let include = extract_config_string_array(source, &js_path(), &["include"]);
3960 assert_eq!(include, vec!["src/**/*"]);
3961 }
3962
3963 #[test]
3964 fn json_wrapped_in_parens_object_keys() {
3965 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
3966 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3967 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
3968 }
3969
3970 fn json_path() -> PathBuf {
3971 PathBuf::from("config.json")
3972 }
3973
3974 #[test]
3975 fn json_file_parsed_correctly() {
3976 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
3977 let val = extract_config_string(source, &json_path(), &["key"]);
3978 assert_eq!(val, Some("value".to_string()));
3979
3980 let list = extract_config_string_array(source, &json_path(), &["list"]);
3981 assert_eq!(list, vec!["a", "b"]);
3982 }
3983
3984 #[test]
3985 fn jsonc_file_parsed_correctly() {
3986 let source = r#"{"key": "value"}"#;
3987 let path = PathBuf::from("tsconfig.jsonc");
3988 let val = extract_config_string(source, &path, &["key"]);
3989 assert_eq!(val, Some("value".to_string()));
3990 }
3991
3992 #[test]
3993 fn extract_define_config_arrow_function() {
3994 let source = r#"
3995 import { defineConfig } from 'vite';
3996 export default defineConfig(() => ({
3997 test: {
3998 include: ["**/*.test.ts"]
3999 }
4000 }));
4001 "#;
4002 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
4003 assert_eq!(include, vec!["**/*.test.ts"]);
4004 }
4005
4006 #[test]
4010 fn extract_define_config_block_body_arrow_function() {
4011 let source = r#"
4012 import { defineConfig } from 'vite';
4013 export default defineConfig(({ mode }) => {
4014 const isProduction = mode === 'production';
4015 return {
4016 test: {
4017 environment: "jsdom",
4018 setupFiles: "./tests/setup.ts"
4019 },
4020 base: isProduction ? '/app/' : '/'
4021 };
4022 });
4023 "#;
4024 assert_eq!(
4025 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4026 Some("jsdom")
4027 );
4028 assert_eq!(
4029 extract_config_string(source, &ts_path(), &["test", "setupFiles"]).as_deref(),
4030 Some("./tests/setup.ts")
4031 );
4032 }
4033
4034 #[test]
4035 fn extract_define_config_function_expression() {
4036 let source = r#"
4037 import { defineConfig } from 'vite';
4038 export default defineConfig(function () {
4039 return { test: { environment: "happy-dom" } };
4040 });
4041 "#;
4042 assert_eq!(
4043 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4044 Some("happy-dom")
4045 );
4046 }
4047
4048 #[test]
4052 fn wrapper_options_object_does_not_shadow_named_config_arg() {
4053 let source = r#"
4054 const nextConfig = { pageExtensions: ["page.tsx"] };
4055 module.exports = withSentryConfig(nextConfig, { org: "o", project: "p", silent: true });
4056 "#;
4057 assert_eq!(
4058 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
4059 vec!["page.tsx"]
4060 );
4061 }
4062
4063 #[test]
4064 fn wrapper_options_object_does_not_shadow_named_config_arg_esm() {
4065 let source = r#"
4066 const nextConfig = { pageExtensions: ["page.tsx"] };
4067 export default withSentryConfig(nextConfig, { org: "o", project: "p" });
4068 "#;
4069 assert_eq!(
4070 extract_config_string_array(source, &ts_path(), &["pageExtensions"]),
4071 vec!["page.tsx"]
4072 );
4073 }
4074
4075 #[test]
4077 fn merge_config_extracts_nested_define_config_object() {
4078 let source = r#"
4079 import { defineConfig, mergeConfig } from 'vitest/config';
4080 import viteConfig from './vite.config';
4081 export default mergeConfig(viteConfig, defineConfig({
4082 test: { environment: "jsdom" }
4083 }));
4084 "#;
4085 assert_eq!(
4086 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4087 Some("jsdom")
4088 );
4089 }
4090
4091 #[test]
4092 fn define_config_wrapping_merge_config_extracts_object() {
4093 let source = r#"
4094 import { defineConfig, mergeConfig } from 'vitest/config';
4095 import base from './base';
4096 export default defineConfig(mergeConfig(base, { test: { environment: "jsdom" } }));
4097 "#;
4098 assert_eq!(
4099 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4100 Some("jsdom")
4101 );
4102 }
4103
4104 #[test]
4107 fn inline_object_at_argument_zero_still_wins() {
4108 let source = r#"
4109 const unrelated = { pageExtensions: ["wrong.tsx"] };
4110 module.exports = withSentryConfig({ pageExtensions: ["right.tsx"] }, { org: "o" });
4111 "#;
4112 assert_eq!(
4113 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
4114 vec!["right.tsx"]
4115 );
4116 }
4117
4118 #[test]
4122 fn conditional_config_callback_extracts_branch_return() {
4123 let source = r#"
4124 import { defineConfig } from 'vite';
4125 export default defineConfig(({ command }) => {
4126 if (command === "serve") {
4127 return { test: { environment: "jsdom" } };
4128 } else {
4129 return { test: { environment: "node" } };
4130 }
4131 });
4132 "#;
4133 assert_eq!(
4134 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4135 Some("jsdom"),
4136 "with no top-level return, the first branch in source order wins"
4137 );
4138 }
4139
4140 #[test]
4141 fn try_block_return_is_extracted() {
4142 let source = r#"
4143 export default (() => {
4144 try {
4145 return { test: { environment: "jsdom" } };
4146 } catch (e) {
4147 return { test: { environment: "node" } };
4148 }
4149 });
4150 "#;
4151 assert_eq!(
4152 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4153 Some("jsdom")
4154 );
4155 }
4156
4157 #[test]
4158 fn switch_case_return_is_extracted() {
4159 let source = r#"
4160 import { defineConfig } from 'vite';
4161 export default defineConfig(({ mode }) => {
4162 switch (mode) {
4163 case "test":
4164 return { test: { environment: "jsdom" } };
4165 default:
4166 return { test: { environment: "node" } };
4167 }
4168 });
4169 "#;
4170 assert_eq!(
4171 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4172 Some("jsdom")
4173 );
4174 }
4175
4176 #[test]
4180 fn guard_clause_does_not_shadow_the_top_level_return() {
4181 let source = r#"
4182 import { defineConfig } from 'vite';
4183 export default defineConfig(({ mode }) => {
4184 if (!mode) {
4185 return {};
4186 }
4187 return { test: { environment: "jsdom" } };
4188 });
4189 "#;
4190 assert_eq!(
4191 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4192 Some("jsdom"),
4193 "a return at the callback's own level is the main config"
4194 );
4195 }
4196
4197 #[test]
4200 fn else_if_chain_does_not_shadow_the_top_level_return() {
4201 let source = r#"
4202 import { defineConfig } from 'vite';
4203 export default defineConfig(({ mode }) => {
4204 if (mode === "a") {
4205 return { base: "/a/" };
4206 } else if (mode === "b") {
4207 return { base: "/b/" };
4208 }
4209 return { test: { environment: "jsdom" } };
4210 });
4211 "#;
4212 assert_eq!(
4213 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4214 Some("jsdom")
4215 );
4216 }
4217
4218 #[test]
4219 fn extract_config_from_default_export_function_declaration() {
4220 let source = r#"
4221 export default function createConfig() {
4222 return {
4223 clientModules: ["./src/client/global.js"]
4224 };
4225 }
4226 "#;
4227
4228 let client_modules = extract_config_string_array(source, &ts_path(), &["clientModules"]);
4229 assert_eq!(client_modules, vec!["./src/client/global.js"]);
4230 }
4231
4232 #[test]
4233 fn extract_config_from_default_export_async_function_declaration() {
4234 let source = r#"
4235 export default async function createConfigAsync() {
4236 return {
4237 docs: {
4238 path: "knowledge"
4239 }
4240 };
4241 }
4242 "#;
4243
4244 let docs_path = extract_config_string(source, &ts_path(), &["docs", "path"]);
4245 assert_eq!(docs_path, Some("knowledge".to_string()));
4246 }
4247
4248 #[test]
4249 fn extract_config_from_exported_arrow_function_identifier() {
4250 let source = r#"
4251 const config = async () => {
4252 return {
4253 themes: ["classic"]
4254 };
4255 };
4256
4257 export default config;
4258 "#;
4259
4260 let themes = extract_config_shallow_strings(source, &ts_path(), "themes");
4261 assert_eq!(themes, vec!["classic"]);
4262 }
4263
4264 #[test]
4265 fn module_exports_nested_string() {
4266 let source = r#"
4267 module.exports = {
4268 resolve: {
4269 alias: {
4270 "@": "./src"
4271 }
4272 }
4273 };
4274 "#;
4275 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
4276 assert_eq!(val, Some("./src".to_string()));
4277 }
4278
4279 #[test]
4280 fn property_strings_nested_objects() {
4281 let source = r#"
4282 export default {
4283 plugins: {
4284 group1: { a: "val-a" },
4285 group2: { b: "val-b" }
4286 }
4287 };
4288 "#;
4289 let values = extract_config_property_strings(source, &js_path(), "plugins");
4290 assert!(values.contains(&"val-a".to_string()));
4291 assert!(values.contains(&"val-b".to_string()));
4292 }
4293
4294 #[test]
4295 fn property_strings_missing_key_returns_empty() {
4296 let source = r#"export default { other: "value" };"#;
4297 let values = extract_config_property_strings(source, &js_path(), "missing");
4298 assert!(values.is_empty());
4299 }
4300
4301 #[test]
4302 fn shallow_strings_tuple_array() {
4303 let source = r#"
4304 module.exports = {
4305 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
4306 };
4307 "#;
4308 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
4309 assert_eq!(values, vec!["default", "jest-junit"]);
4310 assert!(!values.contains(&"reports".to_string()));
4311 }
4312
4313 #[test]
4314 fn shallow_strings_single_string() {
4315 let source = r#"export default { preset: "ts-jest" };"#;
4316 let values = extract_config_shallow_strings(source, &js_path(), "preset");
4317 assert_eq!(values, vec!["ts-jest"]);
4318 }
4319
4320 #[test]
4321 fn shallow_strings_missing_key() {
4322 let source = r#"export default { other: "val" };"#;
4323 let values = extract_config_shallow_strings(source, &js_path(), "missing");
4324 assert!(values.is_empty());
4325 }
4326
4327 #[test]
4328 fn shallow_strings_or_object_property_alias_objects() {
4329 let source = r#"
4330 export default {
4331 jsPlugins: [
4332 "eslint-plugin-playwright",
4333 ["eslint-plugin-regexp", { rules: {} }],
4334 { name: "short", specifier: "eslint-plugin-with-long-name" }
4335 ]
4336 };
4337 "#;
4338 let values = extract_config_shallow_strings_or_object_property(
4339 source,
4340 &ts_path(),
4341 "jsPlugins",
4342 "specifier",
4343 );
4344 assert_eq!(
4345 values,
4346 vec![
4347 "eslint-plugin-playwright",
4348 "eslint-plugin-regexp",
4349 "eslint-plugin-with-long-name"
4350 ]
4351 );
4352 }
4353
4354 #[test]
4355 fn nested_shallow_strings_vitest_reporters() {
4356 let source = r#"
4357 export default {
4358 test: {
4359 reporters: ["default", "vitest-sonar-reporter"]
4360 }
4361 };
4362 "#;
4363 let values =
4364 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4365 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
4366 }
4367
4368 #[test]
4369 fn nested_shallow_strings_tuple_format() {
4370 let source = r#"
4371 export default {
4372 test: {
4373 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
4374 }
4375 };
4376 "#;
4377 let values =
4378 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4379 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
4380 }
4381
4382 #[test]
4383 fn nested_shallow_strings_missing_outer() {
4384 let source = r"export default { other: {} };";
4385 let values =
4386 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4387 assert!(values.is_empty());
4388 }
4389
4390 #[test]
4391 fn nested_shallow_strings_missing_inner() {
4392 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
4393 let values =
4394 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4395 assert!(values.is_empty());
4396 }
4397
4398 #[test]
4399 fn string_or_array_missing_path() {
4400 let source = r"export default {};";
4401 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
4402 assert!(result.is_empty());
4403 }
4404
4405 #[test]
4406 fn string_or_array_non_string_values() {
4407 let source = r"export default { entry: [42, true] };";
4408 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
4409 assert!(result.is_empty());
4410 }
4411
4412 #[test]
4413 fn array_nested_extraction() {
4414 let source = r#"
4415 export default defineConfig({
4416 test: {
4417 projects: [
4418 {
4419 test: {
4420 setupFiles: ["./test/setup-a.ts"]
4421 }
4422 },
4423 {
4424 test: {
4425 setupFiles: "./test/setup-b.ts"
4426 }
4427 }
4428 ]
4429 }
4430 });
4431 "#;
4432 let results = extract_config_array_nested_string_or_array(
4433 source,
4434 &ts_path(),
4435 &["test", "projects"],
4436 &["test", "setupFiles"],
4437 );
4438 assert!(results.contains(&"./test/setup-a.ts".to_string()));
4439 assert!(results.contains(&"./test/setup-b.ts".to_string()));
4440 }
4441
4442 #[test]
4443 fn array_nested_empty_when_no_array() {
4444 let source = r#"export default { test: { projects: "not-an-array" } };"#;
4445 let results = extract_config_array_nested_string_or_array(
4446 source,
4447 &js_path(),
4448 &["test", "projects"],
4449 &["test", "setupFiles"],
4450 );
4451 assert!(results.is_empty());
4452 }
4453
4454 #[test]
4455 fn object_nested_extraction() {
4456 let source = r#"{
4457 "projects": {
4458 "app-one": {
4459 "architect": {
4460 "build": {
4461 "options": {
4462 "styles": ["src/styles.css"]
4463 }
4464 }
4465 }
4466 }
4467 }
4468 }"#;
4469 let results = extract_config_object_nested_string_or_array(
4470 source,
4471 &json_path(),
4472 &["projects"],
4473 &["architect", "build", "options", "styles"],
4474 );
4475 assert_eq!(results, vec!["src/styles.css"]);
4476 }
4477
4478 #[test]
4479 fn array_with_object_input_form_extracted() {
4480 let source = r#"{
4481 "projects": {
4482 "app": {
4483 "architect": {
4484 "build": {
4485 "options": {
4486 "styles": [
4487 "src/styles.scss",
4488 { "input": "src/theme.scss", "bundleName": "theme", "inject": false },
4489 { "bundleName": "lazy-only" }
4490 ]
4491 }
4492 }
4493 }
4494 }
4495 }
4496 }"#;
4497 let results = extract_config_object_nested_string_or_array(
4498 source,
4499 &json_path(),
4500 &["projects"],
4501 &["architect", "build", "options", "styles"],
4502 );
4503 assert!(
4504 results.contains(&"src/styles.scss".to_string()),
4505 "string form must still work: {results:?}"
4506 );
4507 assert!(
4508 results.contains(&"src/theme.scss".to_string()),
4509 "object form with `input` must be extracted: {results:?}"
4510 );
4511 assert!(
4512 !results.contains(&"lazy-only".to_string()),
4513 "bundleName must not be misinterpreted as a path: {results:?}"
4514 );
4515 assert!(
4516 !results.contains(&"theme".to_string()),
4517 "bundleName from full object must not leak: {results:?}"
4518 );
4519 }
4520
4521 #[test]
4522 fn object_nested_strings_extraction() {
4523 let source = r#"{
4524 "targets": {
4525 "build": {
4526 "executor": "@angular/build:application"
4527 },
4528 "test": {
4529 "executor": "@nx/vite:test"
4530 }
4531 }
4532 }"#;
4533 let results =
4534 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
4535 assert!(results.contains(&"@angular/build:application".to_string()));
4536 assert!(results.contains(&"@nx/vite:test".to_string()));
4537 }
4538
4539 #[test]
4540 fn require_strings_direct_call() {
4541 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
4542 let deps = extract_config_require_strings(source, &js_path(), "adapter");
4543 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
4544 }
4545
4546 #[test]
4547 fn require_strings_no_matching_key() {
4548 let source = r"module.exports = { other: require('something') };";
4549 let deps = extract_config_require_strings(source, &js_path(), "plugins");
4550 assert!(deps.is_empty());
4551 }
4552
4553 #[test]
4554 fn extract_imports_no_imports() {
4555 let source = r"export default {};";
4556 let imports = extract_imports(source, &js_path());
4557 assert!(imports.is_empty());
4558 }
4559
4560 #[test]
4561 fn extract_imports_side_effect_import() {
4562 let source = r"
4563 import 'polyfill';
4564 import './local-setup';
4565 export default {};
4566 ";
4567 let imports = extract_imports(source, &js_path());
4568 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
4569 }
4570
4571 #[test]
4572 fn extract_imports_mixed_specifiers() {
4573 let source = r"
4574 import defaultExport from 'module-a';
4575 import { named } from 'module-b';
4576 import * as ns from 'module-c';
4577 export default {};
4578 ";
4579 let imports = extract_imports(source, &js_path());
4580 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
4581 }
4582
4583 #[test]
4584 fn template_literal_in_string_or_array() {
4585 let source = r"export default { entry: `./src/index.ts` };";
4586 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
4587 assert_eq!(result, vec!["./src/index.ts"]);
4588 }
4589
4590 #[test]
4591 fn template_literal_in_config_string() {
4592 let source = r"export default { testDir: `./tests` };";
4593 let val = extract_config_string(source, &js_path(), &["testDir"]);
4594 assert_eq!(val, Some("./tests".to_string()));
4595 }
4596
4597 #[test]
4598 fn template_literal_command_recovers_static_command_tokens() {
4599 let source = r"
4600 const PORT = 3000;
4601 export default {
4602 webServer: {
4603 command: `pnpm exec srvx --port ${PORT} --hostname 127.0.0.1`
4604 }
4605 };
4606 ";
4607 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
4608 assert_eq!(
4609 val,
4610 Some("pnpm exec srvx --port --hostname 127.0.0.1".to_string())
4611 );
4612 }
4613
4614 #[test]
4615 fn template_literal_command_skips_dynamic_prefix() {
4616 let source = r"
4617 export default {
4618 webServer: { command: `${serverCommand} && pnpm exec srvx` }
4619 };
4620 ";
4621 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
4622 assert!(val.is_none());
4623 }
4624
4625 #[test]
4626 fn template_literal_command_skips_split_static_token() {
4627 let source = r"
4628 export default {
4629 webServer: { command: `pnpm exec sr${part}vx --port 3000` }
4630 };
4631 ";
4632 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
4633 assert!(val.is_none());
4634 }
4635
4636 #[test]
4637 fn array_object_command_pairs_recover_template_command() {
4638 let source = r"
4639 const PORT = 3000;
4640 export default {
4641 webServer: [
4642 {
4643 command: `pnpm exec srvx --port ${PORT}`,
4644 cwd: 'apps/web'
4645 }
4646 ]
4647 };
4648 ";
4649 let pairs = extract_config_array_object_command_pairs(
4650 source,
4651 &ts_path(),
4652 &["webServer"],
4653 "command",
4654 "cwd",
4655 );
4656 assert_eq!(
4657 pairs,
4658 vec![(
4659 "pnpm exec srvx --port ".to_string(),
4660 Some("apps/web".to_string())
4661 )]
4662 );
4663 }
4664
4665 #[test]
4666 fn nested_string_array_empty_path() {
4667 let source = r#"export default { items: ["a", "b"] };"#;
4668 let result = extract_config_string_array(source, &js_path(), &[]);
4669 assert!(result.is_empty());
4670 }
4671
4672 #[test]
4673 fn nested_string_empty_path() {
4674 let source = r#"export default { key: "val" };"#;
4675 let result = extract_config_string(source, &js_path(), &[]);
4676 assert!(result.is_none());
4677 }
4678
4679 #[test]
4680 fn object_keys_empty_path() {
4681 let source = r"export default { plugins: {} };";
4682 let result = extract_config_object_keys(source, &js_path(), &[]);
4683 assert!(result.is_empty());
4684 }
4685
4686 #[test]
4687 fn no_config_object_returns_empty() {
4688 let source = r"const x = 42;";
4689 let result = extract_config_string(source, &js_path(), &["key"]);
4690 assert!(result.is_none());
4691
4692 let arr = extract_config_string_array(source, &js_path(), &["items"]);
4693 assert!(arr.is_empty());
4694
4695 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
4696 assert!(keys.is_empty());
4697 }
4698
4699 #[test]
4700 fn property_with_string_key() {
4701 let source = r#"export default { "string-key": "value" };"#;
4702 let val = extract_config_string(source, &js_path(), &["string-key"]);
4703 assert_eq!(val, Some("value".to_string()));
4704 }
4705
4706 #[test]
4707 fn nested_navigation_through_non_object() {
4708 let source = r#"export default { level1: "not-an-object" };"#;
4709 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
4710 assert!(val.is_none());
4711 }
4712
4713 #[test]
4714 fn variable_reference_untyped() {
4715 let source = r#"
4716 const config = {
4717 testDir: "./tests"
4718 };
4719 export default config;
4720 "#;
4721 let val = extract_config_string(source, &js_path(), &["testDir"]);
4722 assert_eq!(val, Some("./tests".to_string()));
4723 }
4724
4725 #[test]
4726 fn variable_reference_to_an_exported_const() {
4727 for source in [
4728 r#"
4729 export const config = { testDir: "./tests" };
4730 export default config;
4731 "#,
4732 r#"
4733 export const config = { testDir: "./tests" };
4734 module.exports = config;
4735 "#,
4736 ] {
4737 let val = extract_config_string(source, &js_path(), &["testDir"]);
4738 assert_eq!(val, Some("./tests".to_string()), "source: {source}");
4739 }
4740 }
4741
4742 #[test]
4743 fn variable_reference_with_type_annotation() {
4744 let source = r#"
4745 import type { StorybookConfig } from '@storybook/react-vite';
4746 const config: StorybookConfig = {
4747 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
4748 framework: "@storybook/react-vite"
4749 };
4750 export default config;
4751 "#;
4752 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
4753 assert_eq!(
4754 addons,
4755 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
4756 );
4757
4758 let framework = extract_config_string(source, &ts_path(), &["framework"]);
4759 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
4760 }
4761
4762 #[test]
4763 fn variable_reference_with_define_config() {
4764 let source = r#"
4765 import { defineConfig } from 'vitest/config';
4766 const config = defineConfig({
4767 test: {
4768 include: ["**/*.test.ts"]
4769 }
4770 });
4771 export default config;
4772 "#;
4773 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
4774 assert_eq!(include, vec!["**/*.test.ts"]);
4775 }
4776
4777 #[test]
4778 fn ts_satisfies_direct_export() {
4779 let source = r#"
4780 export default {
4781 testDir: "./tests"
4782 } satisfies PlaywrightTestConfig;
4783 "#;
4784 let val = extract_config_string(source, &ts_path(), &["testDir"]);
4785 assert_eq!(val, Some("./tests".to_string()));
4786 }
4787
4788 #[test]
4789 fn ts_as_direct_export() {
4790 let source = r#"
4791 export default {
4792 testDir: "./tests"
4793 } as const;
4794 "#;
4795 let val = extract_config_string(source, &ts_path(), &["testDir"]);
4796 assert_eq!(val, Some("./tests".to_string()));
4797 }
4798
4799 fn aliases(source: &str) -> Vec<(String, String)> {
4802 extract_config_aliases(source, &js_path(), &["resolve", "alias"])
4803 }
4804
4805 #[test]
4806 fn aliases_inline_object_still_extracted() {
4807 let source = r#"
4809 export default defineConfig({
4810 resolve: { alias: { "@": "./src", utils: "../../utils" } }
4811 });
4812 "#;
4813 let mut got = aliases(source);
4814 got.sort();
4815 assert_eq!(
4816 got,
4817 vec![
4818 ("@".to_string(), "./src".to_string()),
4819 ("utils".to_string(), "../../utils".to_string()),
4820 ]
4821 );
4822 }
4823
4824 #[test]
4825 fn aliases_inline_array_still_extracted() {
4826 let source = r#"
4827 export default defineConfig({
4828 resolve: { alias: [{ find: "@", replacement: "./src" }] }
4829 });
4830 "#;
4831 assert_eq!(
4832 aliases(source),
4833 vec![("@".to_string(), "./src".to_string())]
4834 );
4835 }
4836
4837 #[test]
4838 fn aliases_local_const_array_identifier() {
4839 let source = r#"
4840 const sharedAliases = [{ find: "@", replacement: "./src" }];
4841 export default defineConfig({ resolve: { alias: sharedAliases } });
4842 "#;
4843 assert_eq!(
4844 aliases(source),
4845 vec![("@".to_string(), "./src".to_string())]
4846 );
4847 }
4848
4849 #[test]
4850 fn aliases_local_const_object_identifier() {
4851 let source = r#"
4852 const sharedAliases = { "@": "./src" };
4853 export default defineConfig({ resolve: { alias: sharedAliases } });
4854 "#;
4855 assert_eq!(
4856 aliases(source),
4857 vec![("@".to_string(), "./src".to_string())]
4858 );
4859 }
4860
4861 #[test]
4862 fn aliases_array_spread_of_identifiers_and_inline() {
4863 let source = r##"
4864 const a = [{ find: "@", replacement: "./src" }];
4865 const b = [{ find: "~", replacement: "./lib" }];
4866 export default defineConfig({
4867 resolve: { alias: [...a, ...b, { find: "#", replacement: "./test" }] }
4868 });
4869 "##;
4870 let mut got = aliases(source);
4871 got.sort();
4872 assert_eq!(
4873 got,
4874 vec![
4875 ("#".to_string(), "./test".to_string()),
4876 ("@".to_string(), "./src".to_string()),
4877 ("~".to_string(), "./lib".to_string()),
4878 ]
4879 );
4880 }
4881
4882 #[test]
4883 fn aliases_object_spread_of_identifier_and_inline() {
4884 let source = r#"
4885 const base = { "@": "./src" };
4886 export default defineConfig({
4887 resolve: { alias: { ...base, "~": "./lib" } }
4888 });
4889 "#;
4890 let mut got = aliases(source);
4891 got.sort();
4892 assert_eq!(
4893 got,
4894 vec![
4895 ("@".to_string(), "./src".to_string()),
4896 ("~".to_string(), "./lib".to_string()),
4897 ]
4898 );
4899 }
4900
4901 #[test]
4902 fn aliases_local_const_chained_identifier() {
4903 let source = r#"
4905 const real = [{ find: "@", replacement: "./src" }];
4906 const alias2 = real;
4907 export default defineConfig({ resolve: { alias: alias2 } });
4908 "#;
4909 assert_eq!(
4910 aliases(source),
4911 vec![("@".to_string(), "./src".to_string())]
4912 );
4913 }
4914
4915 #[test]
4916 fn aliases_imported_named_identifier_from_sibling() {
4917 let dir = tempfile::tempdir().unwrap();
4918 std::fs::write(
4919 dir.path().join("vite.shared.js"),
4920 r#"export const sharedAliases = [
4921 { find: "@", replacement: new URL("./src", import.meta.url).pathname },
4922 ];"#,
4923 )
4924 .unwrap();
4925 let config = dir.path().join("vite.config.js");
4926 let source = r#"
4927 import { defineConfig } from "vite";
4928 import { sharedAliases } from "./vite.shared.js";
4929 export default defineConfig({ resolve: { alias: sharedAliases } });
4930 "#;
4931 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4932 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4933 }
4934
4935 #[test]
4936 fn aliases_imported_extensionless_specifier_probed() {
4937 let dir = tempfile::tempdir().unwrap();
4938 std::fs::write(
4939 dir.path().join("aliases.mjs"),
4940 r#"export const sharedAliases = { "@": "./src" };"#,
4941 )
4942 .unwrap();
4943 let config = dir.path().join("vite.config.ts");
4944 let source = r#"
4945 import { sharedAliases } from "./aliases";
4946 export default defineConfig({ resolve: { alias: sharedAliases } });
4947 "#;
4948 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4949 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4950 }
4951
4952 #[test]
4953 fn aliases_imported_default_export_from_sibling() {
4954 let dir = tempfile::tempdir().unwrap();
4955 std::fs::write(
4956 dir.path().join("aliases.js"),
4957 r#"export default [{ find: "@", replacement: "./src" }];"#,
4958 )
4959 .unwrap();
4960 let config = dir.path().join("vite.config.js");
4961 let source = r#"
4962 import sharedAliases from "./aliases.js";
4963 export default defineConfig({ resolve: { alias: sharedAliases } });
4964 "#;
4965 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4966 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4967 }
4968
4969 #[test]
4970 fn aliases_imported_spread_from_two_siblings() {
4971 let dir = tempfile::tempdir().unwrap();
4972 std::fs::write(
4973 dir.path().join("a.js"),
4974 r#"export const a = [{ find: "@", replacement: "./src" }];"#,
4975 )
4976 .unwrap();
4977 std::fs::write(
4978 dir.path().join("b.js"),
4979 r#"export const b = [{ find: "~", replacement: "./lib" }];"#,
4980 )
4981 .unwrap();
4982 let config = dir.path().join("vite.config.js");
4983 let source = r#"
4984 import { a } from "./a.js";
4985 import { b } from "./b.js";
4986 export default defineConfig({ resolve: { alias: [...a, ...b] } });
4987 "#;
4988 let mut got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4989 got.sort();
4990 assert_eq!(
4991 got,
4992 vec![
4993 ("@".to_string(), "./src".to_string()),
4994 ("~".to_string(), "./lib".to_string()),
4995 ]
4996 );
4997 }
4998
4999 #[test]
5000 fn aliases_import_cycle_terminates() {
5001 let dir = tempfile::tempdir().unwrap();
5004 std::fs::write(
5005 dir.path().join("a.js"),
5006 r#"import { b } from "./b.js";
5007 export const a = [{ find: "@", replacement: "./src" }, ...b];"#,
5008 )
5009 .unwrap();
5010 std::fs::write(
5011 dir.path().join("b.js"),
5012 r#"import { a } from "./a.js";
5013 export const b = [...a];"#,
5014 )
5015 .unwrap();
5016 let config = dir.path().join("vite.config.js");
5017 let source = r#"
5018 import { a } from "./a.js";
5019 export default defineConfig({ resolve: { alias: a } });
5020 "#;
5021 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
5022 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
5023 }
5024
5025 #[test]
5026 fn aliases_non_relative_import_not_followed() {
5027 let source = r#"
5030 import { sharedAliases } from "some-pkg";
5031 export default defineConfig({ resolve: { alias: sharedAliases } });
5032 "#;
5033 let dir = tempfile::tempdir().unwrap();
5034 let config = dir.path().join("vite.config.js");
5035 assert!(extract_config_aliases(source, &config, &["resolve", "alias"]).is_empty());
5036 }
5037
5038 #[test]
5039 fn aliases_object_array_value_takes_first_entry() {
5040 let source = r#"
5045 export default {
5046 compilerOptions: { paths: { "@/*": ["./src/*"], "~/*": ["./lib/*", "./vendor/*"] } }
5047 };
5048 "#;
5049 let mut got = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
5050 got.sort();
5051 assert_eq!(
5052 got,
5053 vec![
5054 ("@/*".to_string(), "./src/*".to_string()),
5055 ("~/*".to_string(), "./lib/*".to_string()),
5056 ]
5057 );
5058 }
5059
5060 #[test]
5061 fn aliases_kinded_preserves_is_bare_through_resolution() {
5062 let source = r#"
5065 const a = [{ find: "lodash-es", replacement: "lodash" }];
5066 export default defineConfig({
5067 resolve: { alias: [...a, { find: "@", replacement: "./src" }] }
5068 });
5069 "#;
5070 let mut got = extract_config_aliases_kinded(source, &js_path(), &["resolve", "alias"]);
5071 got.sort();
5072 assert_eq!(
5073 got,
5074 vec![
5075 ("@".to_string(), "./src".to_string(), false),
5076 ("lodash-es".to_string(), "lodash".to_string(), true),
5077 ]
5078 );
5079 }
5080
5081 #[test]
5082 fn aliases_kinded_preserves_is_bare_through_imported_spread() {
5083 let dir = tempfile::tempdir().unwrap();
5084 std::fs::write(
5085 dir.path().join("aliases.js"),
5086 r#"export const packageAliases = [{ find: "lodash-es", replacement: "lodash" }];"#,
5087 )
5088 .unwrap();
5089 let config = dir.path().join("vite.config.js");
5090 let source = r#"
5091 import { packageAliases } from "./aliases.js";
5092 export default defineConfig({
5093 resolve: { alias: [...packageAliases, { find: "@", replacement: "./src" }] }
5094 });
5095 "#;
5096 let mut got = extract_config_aliases_kinded(source, &config, &["resolve", "alias"]);
5097 got.sort();
5098 assert_eq!(
5099 got,
5100 vec![
5101 ("@".to_string(), "./src".to_string(), false),
5102 ("lodash-es".to_string(), "lodash".to_string(), true),
5103 ]
5104 );
5105 }
5106
5107 #[test]
5110 fn extract_command_string_literal() {
5111 let source = r#"export default { start: "node server.js" };"#;
5112 let val = extract_config_command(source, &js_path(), &["start"]);
5113 assert_eq!(val, Some("node server.js".to_string()));
5114 }
5115
5116 #[test]
5117 fn extract_command_nested_path() {
5118 let source = r#"
5119 export default {
5120 scripts: {
5121 dev: "vite dev"
5122 }
5123 };
5124 "#;
5125 let val = extract_config_command(source, &js_path(), &["scripts", "dev"]);
5126 assert_eq!(val, Some("vite dev".to_string()));
5127 }
5128
5129 #[test]
5130 fn extract_command_missing_key_returns_none() {
5131 let source = r#"export default { other: "val" };"#;
5132 let val = extract_config_command(source, &js_path(), &["start"]);
5133 assert!(val.is_none());
5134 }
5135
5136 #[test]
5137 fn extract_command_ts_as_expression() {
5138 let source = r#"export default { start: "node server.js" as string };"#;
5139 let val = extract_config_command(source, &ts_path(), &["start"]);
5140 assert_eq!(val, Some("node server.js".to_string()));
5141 }
5142
5143 #[test]
5144 fn extract_command_ts_satisfies_expression() {
5145 let source = r#"export default { start: "node server.js" satisfies string };"#;
5146 let val = extract_config_command(source, &ts_path(), &["start"]);
5147 assert_eq!(val, Some("node server.js".to_string()));
5148 }
5149
5150 #[test]
5151 fn extract_command_parenthesized_expression() {
5152 let source = r#"export default { start: ("node server.js") };"#;
5153 let val = extract_config_command(source, &js_path(), &["start"]);
5154 assert_eq!(val, Some("node server.js".to_string()));
5155 }
5156
5157 #[test]
5158 fn extract_command_empty_path_returns_none() {
5159 let source = r#"export default { start: "node server.js" };"#;
5160 let val = extract_config_command(source, &js_path(), &[]);
5161 assert!(val.is_none());
5162 }
5163
5164 #[test]
5167 fn truthy_bool_or_object_with_true_value() {
5168 let source = r"export default { typescript: true };";
5169 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5170 assert!(result);
5171 }
5172
5173 #[test]
5174 fn truthy_bool_or_object_with_false_value() {
5175 let source = r"export default { typescript: false };";
5176 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5177 assert!(!result);
5178 }
5179
5180 #[test]
5181 fn truthy_bool_or_object_with_object_value() {
5182 let source = r#"export default { typescript: { reactDocgen: "react-docgen" } };"#;
5183 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5184 assert!(result);
5185 }
5186
5187 #[test]
5188 fn truthy_bool_or_object_missing_key_returns_false() {
5189 let source = r"export default { other: true };";
5190 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5191 assert!(!result);
5192 }
5193
5194 #[test]
5195 fn truthy_bool_or_object_with_string_value_returns_false() {
5196 let source = r#"export default { typescript: "yes" };"#;
5198 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5199 assert!(!result);
5200 }
5201
5202 #[test]
5203 fn truthy_bool_or_object_ts_satisfies_wrapper() {
5204 let source = r"export default { typescript: (true satisfies boolean) };";
5205 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5206 assert!(result);
5207 }
5208
5209 #[test]
5210 fn truthy_bool_or_object_ts_as_wrapper() {
5211 let source = r"export default { typescript: (true as boolean) };";
5212 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5213 assert!(result);
5214 }
5215
5216 #[test]
5217 fn truthy_bool_or_object_parenthesized_wrapper() {
5218 let source = r"export default { typescript: (true) };";
5219 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5220 assert!(result);
5221 }
5222
5223 #[test]
5229 fn static_dir_entries_object_form_exercises_property_string() {
5230 let source = r#"
5233 export default {
5234 staticDirs: [
5235 { from: "./media", to: "/assets" }
5236 ]
5237 };
5238 "#;
5239 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5240 assert_eq!(
5241 entries,
5242 vec![("./media".to_string(), Some("/assets".to_string()))]
5243 );
5244 }
5245
5246 #[test]
5249 fn expression_to_path_values_array_form_via_config_path() {
5250 let source = r#"export default { entries: ["./src/a.ts", "./src/b.ts"] };"#;
5253 let result = extract_config_string_or_array(source, &js_path(), &["entries"]);
5254 assert_eq!(result, vec!["./src/a.ts", "./src/b.ts"]);
5255 }
5256
5257 #[test]
5260 fn array_nested_aliases_object_form() {
5261 let source = r#"
5262 export default {
5263 test: {
5264 projects: [
5265 {
5266 resolve: {
5267 alias: { "@": "./src" }
5268 }
5269 }
5270 ]
5271 }
5272 };
5273 "#;
5274 let aliases = extract_config_array_nested_aliases_kinded(
5275 source,
5276 &ts_path(),
5277 &["test", "projects"],
5278 &["resolve", "alias"],
5279 );
5280 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
5281 }
5282
5283 #[test]
5284 fn array_nested_aliases_empty_when_path_is_not_array() {
5285 let source = r#"export default { test: { projects: "not-an-array" } };"#;
5286 let aliases = extract_config_array_nested_aliases_kinded(
5287 source,
5288 &ts_path(),
5289 &["test", "projects"],
5290 &["resolve", "alias"],
5291 );
5292 assert!(aliases.is_empty());
5293 }
5294
5295 #[test]
5296 fn array_nested_aliases_kinded_tracks_is_bare() {
5297 let source = r#"
5298 export default {
5299 projects: [
5300 {
5301 resolve: {
5302 alias: [
5303 { find: "lodash-es", replacement: "lodash" },
5304 { find: "@", replacement: "./src" }
5305 ]
5306 }
5307 }
5308 ]
5309 };
5310 "#;
5311 let mut aliases = extract_config_array_nested_aliases_kinded(
5312 source,
5313 &ts_path(),
5314 &["projects"],
5315 &["resolve", "alias"],
5316 );
5317 aliases.sort();
5318 assert_eq!(
5319 aliases,
5320 vec![
5321 ("@".to_string(), "./src".to_string(), false),
5322 ("lodash-es".to_string(), "lodash".to_string(), true),
5323 ]
5324 );
5325 }
5326
5327 #[test]
5330 fn default_export_array_aliases_kinded_extracts_from_workspace_config() {
5331 let source = r#"
5332 export default [
5333 {
5334 resolve: {
5335 alias: { "@": "./src" }
5336 }
5337 },
5338 {
5339 resolve: {
5340 alias: [{ find: "~", replacement: "./lib" }]
5341 }
5342 }
5343 ];
5344 "#;
5345 let mut aliases =
5346 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5347 aliases.sort();
5348 assert_eq!(
5349 aliases,
5350 vec![
5351 ("@".to_string(), "./src".to_string(), false),
5352 ("~".to_string(), "./lib".to_string(), false),
5353 ]
5354 );
5355 }
5356
5357 #[test]
5358 fn default_export_array_aliases_kinded_define_workspace_wrapper() {
5359 let source = r#"
5360 export default defineWorkspace([
5361 {
5362 resolve: { alias: { "@": "./src" } }
5363 }
5364 ]);
5365 "#;
5366 let aliases =
5367 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5368 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
5369 }
5370
5371 #[test]
5372 fn default_export_array_aliases_kinded_empty_when_no_alias_path() {
5373 let source = r#"
5374 export default [
5375 { test: { include: ["**/*.test.ts"] } }
5376 ];
5377 "#;
5378 let aliases =
5379 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5380 assert!(aliases.is_empty());
5381 }
5382
5383 #[test]
5386 fn config_default_export_unreachable_when_no_export() {
5387 let source = r"const x = 42;";
5388 assert!(config_default_export_unreachable(source, &js_path()));
5389 }
5390
5391 #[test]
5392 fn config_default_export_unreachable_false_for_object_export() {
5393 let source = r#"export default { key: "value" };"#;
5394 assert!(!config_default_export_unreachable(source, &js_path()));
5395 }
5396
5397 #[test]
5398 fn config_default_export_unreachable_false_for_array_export() {
5399 let source = r#"export default ["a", "b"];"#;
5400 assert!(!config_default_export_unreachable(source, &js_path()));
5401 }
5402
5403 #[test]
5404 fn config_default_export_unreachable_true_for_function_without_return_object() {
5405 let source = r"export default function config() { return 42; }";
5407 assert!(config_default_export_unreachable(source, &js_path()));
5408 }
5409
5410 #[test]
5413 fn static_dir_entries_string_and_object_form() {
5414 let source = r#"
5415 export default {
5416 staticDirs: [
5417 "./public",
5418 { from: "../assets", to: "/static" }
5419 ]
5420 };
5421 "#;
5422 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5423 assert_eq!(
5424 entries,
5425 vec![
5426 ("./public".to_string(), None),
5427 ("../assets".to_string(), Some("/static".to_string())),
5428 ]
5429 );
5430 }
5431
5432 #[test]
5433 fn static_dir_entries_object_without_to() {
5434 let source = r#"
5435 export default {
5436 staticDirs: [
5437 { from: "./media" }
5438 ]
5439 };
5440 "#;
5441 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5442 assert_eq!(entries, vec![("./media".to_string(), None)]);
5443 }
5444
5445 #[test]
5446 fn static_dir_entries_object_missing_from_skipped() {
5447 let source = r#"
5449 export default {
5450 staticDirs: [
5451 { to: "/target" },
5452 "./public"
5453 ]
5454 };
5455 "#;
5456 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5457 assert_eq!(entries, vec![("./public".to_string(), None)]);
5458 }
5459
5460 #[test]
5461 fn static_dir_entries_empty_when_not_array() {
5462 let source = r#"export default { staticDirs: "./public" };"#;
5463 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5464 assert!(entries.is_empty());
5465 }
5466
5467 #[test]
5470 fn aliases_array_form_missing_find_or_replacement_skipped() {
5471 let source = r#"
5473 export default {
5474 resolve: {
5475 alias: [
5476 { replacement: "./src" },
5477 { find: "@" },
5478 { find: "~", replacement: "./lib" }
5479 ]
5480 }
5481 };
5482 "#;
5483 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5484 assert_eq!(aliases, vec![("~".to_string(), "./lib".to_string())]);
5485 }
5486
5487 #[test]
5488 fn aliases_object_form_computed_key_skipped() {
5489 let source = r#"
5491 const k = "@";
5492 export default {
5493 resolve: {
5494 alias: {
5495 [k]: "./src",
5496 "~": "./lib"
5497 }
5498 }
5499 };
5500 "#;
5501 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5502 assert_eq!(aliases, vec![("~".to_string(), "./lib".to_string())]);
5504 }
5505
5506 #[test]
5507 fn aliases_kinded_array_form_path_replacement_is_not_bare() {
5508 let source = r#"
5509 export default {
5510 resolve: {
5511 alias: [{ find: "@", replacement: "./src" }]
5512 }
5513 };
5514 "#;
5515 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5516 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
5517 }
5518
5519 #[test]
5520 fn aliases_kinded_object_form_bare_and_path_discrimination() {
5521 let source = r#"
5522 export default {
5523 resolve: {
5524 alias: {
5525 "lodash-es": "lodash",
5526 "@": "./src"
5527 }
5528 }
5529 };
5530 "#;
5531 let mut aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5532 aliases.sort();
5533 assert_eq!(
5534 aliases,
5535 vec![
5536 ("@".to_string(), "./src".to_string(), false),
5537 ("lodash-es".to_string(), "lodash".to_string(), true),
5538 ]
5539 );
5540 }
5541
5542 #[test]
5543 fn aliases_kinded_parent_relative_replacement_is_not_bare() {
5544 let source = r#"
5545 export default {
5546 resolve: { alias: { "@": "../shared/src" } }
5547 };
5548 "#;
5549 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5550 assert_eq!(
5551 aliases,
5552 vec![("@".to_string(), "../shared/src".to_string(), false)]
5553 );
5554 }
5555
5556 #[test]
5557 fn aliases_kinded_absolute_replacement_is_not_bare() {
5558 let source = r#"
5559 export default {
5560 resolve: { alias: { "@": "/absolute/path" } }
5561 };
5562 "#;
5563 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5564 assert_eq!(
5565 aliases,
5566 vec![("@".to_string(), "/absolute/path".to_string(), false)]
5567 );
5568 }
5569
5570 #[test]
5573 fn default_export_array_ts_as_wrapper() {
5574 let source = r"export default [] as string[];";
5576 assert!(!config_default_export_unreachable(source, &js_path()));
5577 }
5578
5579 #[test]
5580 fn default_export_array_ts_satisfies_wrapper() {
5581 let source = r"export default [] satisfies string[];";
5582 assert!(!config_default_export_unreachable(source, &ts_path()));
5583 }
5584
5585 #[test]
5586 fn default_export_array_define_config_call_wrapper() {
5587 let source = r#"export default defineConfig(["**/*.test.ts"]);"#;
5588 assert!(!config_default_export_unreachable(source, &ts_path()));
5589 }
5590
5591 #[test]
5594 fn shallow_strings_object_with_string_values() {
5595 let source = r#"
5597 export default {
5598 plugins: {
5599 autoprefixer: "autoprefixer",
5600 tailwindcss: "tailwindcss"
5601 }
5602 };
5603 "#;
5604 let vals = extract_config_shallow_strings(source, &js_path(), "plugins");
5605 assert!(vals.contains(&"autoprefixer".to_string()));
5606 assert!(vals.contains(&"tailwindcss".to_string()));
5607 }
5608
5609 #[test]
5610 fn shallow_strings_object_with_sub_array_first_element() {
5611 let source = r#"
5613 export default {
5614 reporters: {
5615 main: ["jest-junit", { outputFile: "report.xml" }],
5616 alt: ["html-reporter"]
5617 }
5618 };
5619 "#;
5620 let vals = extract_config_shallow_strings(source, &js_path(), "reporters");
5621 assert!(vals.contains(&"jest-junit".to_string()));
5622 assert!(vals.contains(&"html-reporter".to_string()));
5623 }
5624
5625 #[test]
5628 fn shallow_strings_or_object_property_non_array_single_string() {
5629 let source = r#"export default { jsPlugins: "eslint-plugin-foo" };"#;
5631 let vals = extract_config_shallow_strings_or_object_property(
5632 source,
5633 &ts_path(),
5634 "jsPlugins",
5635 "specifier",
5636 );
5637 assert_eq!(vals, vec!["eslint-plugin-foo"]);
5638 }
5639
5640 #[test]
5641 fn shallow_strings_or_object_property_ts_satisfies_array_element() {
5642 let source = r#"
5644 export default {
5645 jsPlugins: [
5646 ("eslint-plugin-a" satisfies string)
5647 ]
5648 };
5649 "#;
5650 let vals = extract_config_shallow_strings_or_object_property(
5651 source,
5652 &ts_path(),
5653 "jsPlugins",
5654 "specifier",
5655 );
5656 assert_eq!(vals, vec!["eslint-plugin-a"]);
5657 }
5658
5659 #[test]
5660 fn shallow_strings_or_object_property_ts_as_array_element() {
5661 let source = r#"
5662 export default {
5663 jsPlugins: [
5664 ("eslint-plugin-b" as string)
5665 ]
5666 };
5667 "#;
5668 let vals = extract_config_shallow_strings_or_object_property(
5669 source,
5670 &ts_path(),
5671 "jsPlugins",
5672 "specifier",
5673 );
5674 assert_eq!(vals, vec!["eslint-plugin-b"]);
5675 }
5676
5677 #[test]
5678 fn shallow_strings_or_object_property_sub_array_first_element_string() {
5679 let source = r#"
5681 export default {
5682 jsPlugins: [
5683 ["eslint-plugin-tuple-pkg", { options: true }]
5684 ]
5685 };
5686 "#;
5687 let vals = extract_config_shallow_strings_or_object_property(
5688 source,
5689 &ts_path(),
5690 "jsPlugins",
5691 "specifier",
5692 );
5693 assert_eq!(vals, vec!["eslint-plugin-tuple-pkg"]);
5694 }
5695
5696 #[test]
5699 fn array_object_command_pairs_basic() {
5700 let source = r#"
5701 export default {
5702 webServer: [
5703 { command: "node server.js", cwd: "packages/api" },
5704 { command: "vite dev" }
5705 ]
5706 };
5707 "#;
5708 let pairs = extract_config_array_object_command_pairs(
5709 source,
5710 &ts_path(),
5711 &["webServer"],
5712 "command",
5713 "cwd",
5714 );
5715 assert_eq!(
5716 pairs,
5717 vec![
5718 (
5719 "node server.js".to_string(),
5720 Some("packages/api".to_string())
5721 ),
5722 ("vite dev".to_string(), None),
5723 ]
5724 );
5725 }
5726
5727 #[test]
5728 fn array_object_command_pairs_skips_missing_command() {
5729 let source = r#"
5730 export default {
5731 webServer: [
5732 { cwd: "packages/api" },
5733 { command: "vite dev", cwd: "apps/web" }
5734 ]
5735 };
5736 "#;
5737 let pairs = extract_config_array_object_command_pairs(
5738 source,
5739 &ts_path(),
5740 &["webServer"],
5741 "command",
5742 "cwd",
5743 );
5744 assert_eq!(
5745 pairs,
5746 vec![("vite dev".to_string(), Some("apps/web".to_string()))]
5747 );
5748 }
5749
5750 #[test]
5751 fn array_object_command_pairs_empty_when_not_array() {
5752 let source = r#"export default { webServer: { command: "vite dev" } };"#;
5753 let pairs = extract_config_array_object_command_pairs(
5754 source,
5755 &ts_path(),
5756 &["webServer"],
5757 "command",
5758 "cwd",
5759 );
5760 assert!(pairs.is_empty());
5761 }
5762
5763 #[test]
5766 fn normalize_config_path_empty_string_returns_none() {
5767 let config_path = PathBuf::from("/project/vite.config.ts");
5768 let root = PathBuf::from("/project");
5769 assert_eq!(normalize_config_path("", &config_path, &root), None);
5770 }
5771
5772 #[test]
5773 fn normalize_config_path_escapes_to_above_root_returns_none() {
5774 let config_path = PathBuf::from("/project/vite.config.ts");
5775 let root = PathBuf::from("/project");
5776 assert_eq!(
5778 normalize_config_path("../../etc", &config_path, &root),
5779 None
5780 );
5781 }
5782
5783 #[test]
5784 fn normalize_config_path_dot_slash_resolves_relative_to_config_dir() {
5785 let config_path = PathBuf::from("/project/packages/app/vite.config.ts");
5786 let root = PathBuf::from("/project");
5787 assert_eq!(
5788 normalize_config_path("./src", &config_path, &root),
5789 Some("packages/app/src".to_string())
5790 );
5791 }
5792
5793 #[test]
5796 fn json_config_array_of_arrays_via_shallow_strings() {
5797 let source = r#"{"reporters": ["default", ["jest-junit", {}]]}"#;
5799 let vals = extract_config_shallow_strings(source, &json_path(), "reporters");
5800 assert_eq!(vals, vec!["default", "jest-junit"]);
5801 }
5802
5803 #[test]
5806 fn extract_config_path_string_literal() {
5807 let source = r#"export default { outDir: "./dist" };"#;
5808 let path = extract_config_path(source, &js_path(), &["outDir"]);
5809 assert_eq!(
5810 path.map(|p| p.to_string_lossy().replace('\\', "/")),
5811 Some("./dist".to_string())
5812 );
5813 }
5814
5815 #[test]
5816 fn extract_config_path_with_resolve_call() {
5817 let source = r#"
5818 import { resolve } from "node:path";
5819 export default { outDir: resolve(__dirname, "dist") };
5820 "#;
5821 let path = extract_config_path(source, &js_path(), &["outDir"]);
5822 assert_eq!(
5823 path.map(|p| p.to_string_lossy().replace('\\', "/")),
5824 Some("dist".to_string())
5825 );
5826 }
5827
5828 #[test]
5829 fn extract_config_path_missing_key_returns_none() {
5830 let source = r#"export default { other: "val" };"#;
5831 let path = extract_config_path(source, &js_path(), &["outDir"]);
5832 assert!(path.is_none());
5833 }
5834
5835 #[test]
5838 fn extract_imports_and_requires_both_forms() {
5839 let source = r"
5840 import foo from 'foo-pkg';
5841 require('bar-pkg');
5842 export default {};
5843 ";
5844 let sources = extract_imports_and_requires(source, &js_path());
5845 assert!(sources.contains(&"foo-pkg".to_string()));
5846 assert!(sources.contains(&"bar-pkg".to_string()));
5847 }
5848
5849 #[test]
5850 fn extract_imports_and_requires_skips_non_require_calls() {
5851 let source = r"
5852 import foo from 'foo-pkg';
5853 someOtherCall('bar-pkg');
5854 export default {};
5855 ";
5856 let sources = extract_imports_and_requires(source, &js_path());
5857 assert_eq!(sources, vec!["foo-pkg"]);
5858 }
5859
5860 #[test]
5863 fn nested_shallow_strings_non_object_nested_returns_empty() {
5864 let source = r#"export default { test: "not-an-object" };"#;
5866 let vals =
5867 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
5868 assert!(vals.is_empty());
5869 }
5870
5871 #[test]
5874 fn vite_react_babel_dependencies_namespace_import() {
5875 let source = r#"
5876 import * as react from "@vitejs/plugin-react";
5877
5878 export default defineConfig({
5879 plugins: [
5880 react.default({
5881 babel: {
5882 plugins: ["babel-plugin-ns"],
5883 },
5884 }),
5885 ],
5886 });
5887 "#;
5888 let deps = extract_vite_plugin_option_dependencies(source, &ts_path());
5889 assert_eq!(deps, vec!["babel-plugin-ns".to_string()]);
5890 }
5891
5892 #[test]
5895 fn property_strings_deeply_nested_object_values() {
5896 let source = r#"
5898 export default {
5899 settings: {
5900 a: "val-a",
5901 b: {
5902 c: "val-c",
5903 d: ["val-d1", "val-d2"]
5904 }
5905 }
5906 };
5907 "#;
5908 let values = extract_config_property_strings(source, &js_path(), "settings");
5909 assert!(values.contains(&"val-a".to_string()));
5910 assert!(values.contains(&"val-c".to_string()));
5911 assert!(values.contains(&"val-d1".to_string()));
5912 assert!(values.contains(&"val-d2".to_string()));
5913 }
5914
5915 #[test]
5918 fn aliases_exported_const_form_resolves() {
5919 let source = r#"
5921 export const sharedAliases = { "@": "./src" };
5922 export default defineConfig({ resolve: { alias: sharedAliases } });
5923 "#;
5924 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5925 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
5926 }
5927
5928 #[test]
5931 fn aliases_imported_from_sibling_directory_index_file() {
5932 let dir = tempfile::tempdir().unwrap();
5935 let aliases_dir = dir.path().join("aliases");
5936 std::fs::create_dir_all(&aliases_dir).unwrap();
5937 std::fs::write(
5938 aliases_dir.join("index.js"),
5939 r#"export const aliases = [{ find: "@", replacement: "./src" }];"#,
5940 )
5941 .unwrap();
5942 let config = dir.path().join("vite.config.js");
5943 let source = r#"
5944 import { aliases } from "./aliases";
5945 export default defineConfig({ resolve: { alias: aliases } });
5946 "#;
5947 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
5948 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
5949 }
5950
5951 #[test]
5954 fn aliases_depth_limit_terminates_deep_chain() {
5955 let source = r#"
5958 const a9 = [{ find: "@", replacement: "./src" }];
5959 const a8 = a9;
5960 const a7 = a8;
5961 const a6 = a7;
5962 const a5 = a6;
5963 const a4 = a5;
5964 const a3 = a4;
5965 const a2 = a3;
5966 const a1 = a2;
5967 export default defineConfig({ resolve: { alias: a1 } });
5968 "#;
5969 let got = extract_config_aliases(source, &js_path(), &["resolve", "alias"]);
5971 let _ = got; }
5973
5974 #[test]
5977 fn extract_aliases_file_url_to_path_new_url() {
5978 let source = r#"
5980 import { fileURLToPath, URL } from 'node:url';
5981 export default {
5982 resolve: {
5983 alias: {
5984 "@": fileURLToPath(new URL("./src", import.meta.url))
5985 }
5986 }
5987 };
5988 "#;
5989 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5990 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
5991 }
5992
5993 #[test]
5994 fn extract_path_via_new_url_pathname_member() {
5995 let source = r#"
5997 export default {
5998 resolve: {
5999 alias: {
6000 "@": new URL("./src", import.meta.url).pathname
6001 }
6002 }
6003 };
6004 "#;
6005 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
6006 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
6007 }
6008
6009 #[test]
6012 fn truthy_bool_or_object_null_literal_returns_false() {
6013 let source = r"export default { typescript: null };";
6015 let result = extract_config_truthy_bool_or_object(source, &js_path(), &["typescript"]);
6016 assert!(!result);
6017 }
6018
6019 #[test]
6022 fn string_array_non_array_value_returns_empty() {
6023 let source = r#"export default { items: "not-an-array" };"#;
6024 let result = extract_config_string_array(source, &js_path(), &["items"]);
6025 assert!(result.is_empty());
6026 }
6027
6028 #[test]
6031 fn object_nested_empty_when_inner_value_is_not_object() {
6032 let source = r#"export default { targets: { build: "not-an-object" } };"#;
6034 let results =
6035 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
6036 assert!(results.is_empty());
6037 }
6038
6039 #[test]
6042 fn array_nested_string_or_array_missing_inner_path_returns_empty() {
6043 let source = r#"
6044 export default {
6045 test: {
6046 projects: [
6047 { test: { include: ["**/*.test.ts"] } }
6048 ]
6049 }
6050 };
6051 "#;
6052 let results = extract_config_array_nested_string_or_array(
6053 source,
6054 &ts_path(),
6055 &["test", "projects"],
6056 &["test", "setupFiles"],
6057 );
6058 assert!(results.is_empty());
6059 }
6060
6061 #[test]
6062 fn wrapped_named_const_default_export_resolves() {
6063 let source = r#"
6066 import createMDX from "@next/mdx";
6067 const nextConfig = { pageExtensions: ["ts", "tsx", "md", "mdx"] };
6068 const withMDX = createMDX({});
6069 export default withMDX(nextConfig);
6070 "#;
6071 let exts = extract_config_string_array(source, &ts_path(), &["pageExtensions"]);
6072 assert_eq!(exts, vec!["ts", "tsx", "md", "mdx"]);
6073 }
6074
6075 #[test]
6076 fn wrapped_named_const_module_exports_resolves() {
6077 let source = r#"
6079 const nextJest = require("next/jest");
6080 const createJestConfig = nextJest();
6081 const customConfig = { testMatch: ["**/*.test.ts"] };
6082 module.exports = createJestConfig(customConfig);
6083 "#;
6084 let matches = extract_config_string_array(source, &js_path(), &["testMatch"]);
6085 assert_eq!(matches, vec!["**/*.test.ts"]);
6086 }
6087
6088 #[test]
6089 fn wrapped_named_const_nested_and_curried_resolve() {
6090 let nested = r#"
6091 const nextConfig = { pageExtensions: ["mdx"] };
6092 const withMDX = (c) => c;
6093 const withFoo = (c) => c;
6094 export default withMDX(withFoo(nextConfig));
6095 "#;
6096 assert_eq!(
6097 extract_config_string_array(nested, &js_path(), &["pageExtensions"]),
6098 vec!["mdx"]
6099 );
6100
6101 let curried = r#"
6102 const nextConfig = { pageExtensions: ["md"] };
6103 const compose = (..._p) => (c) => c;
6104 export default compose(a, b)(nextConfig);
6105 "#;
6106 assert_eq!(
6107 extract_config_string_array(curried, &js_path(), &["pageExtensions"]),
6108 vec!["md"]
6109 );
6110 }
6111
6112 #[test]
6113 fn wrapped_inline_object_still_resolves() {
6114 let source = r#"
6116 const withMDX = createMDX({});
6117 export default withMDX({ pageExtensions: ["mdx"] });
6118 "#;
6119 assert_eq!(
6120 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
6121 vec!["mdx"]
6122 );
6123 }
6124}