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>> {
2715 for stmt in &program.body {
2716 if let Statement::ExportDefaultDeclaration(decl) = stmt {
2717 if let ExportDefaultDeclarationKind::FunctionDeclaration(function) = &decl.declaration {
2718 if function.generator {
2719 return None;
2720 }
2721 return function.body.as_ref().and_then(|body| returned_array(body));
2722 }
2723 return decl
2724 .declaration
2725 .as_expression()
2726 .and_then(array_from_config_expression);
2727 }
2728 }
2729 None
2730}
2731
2732fn returned_array<'a>(body: &'a FunctionBody<'a>) -> Option<&'a ArrayExpression<'a>> {
2734 body.statements
2735 .iter()
2736 .find_map(|statement| match statement {
2737 Statement::ReturnStatement(returned) => Some(returned),
2738 _ => None,
2739 })
2740 .and_then(|returned| returned.argument.as_ref())
2741 .and_then(array_from_expression)
2742}
2743
2744fn array_from_config_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
2746 match expr {
2747 Expression::ArrowFunctionExpression(arrow) => match &arrow.body {
2748 ArrowFunctionBody::FunctionBody(body) => returned_array(body),
2749 body => body.as_expression().and_then(array_from_expression),
2750 },
2751 Expression::FunctionExpression(function) => {
2752 if function.generator {
2753 return None;
2754 }
2755 function.body.as_ref().and_then(|body| returned_array(body))
2756 }
2757 Expression::ParenthesizedExpression(paren) => {
2758 array_from_config_expression(&paren.expression)
2759 }
2760 Expression::TSAsExpression(ts_as) => array_from_config_expression(&ts_as.expression),
2761 Expression::TSSatisfiesExpression(ts_sat) => {
2762 array_from_config_expression(&ts_sat.expression)
2763 }
2764 Expression::CallExpression(call) => call
2765 .arguments
2766 .first()
2767 .and_then(Argument::as_expression)
2768 .and_then(array_from_config_expression),
2769 _ => array_from_expression(expr),
2770 }
2771}
2772
2773fn array_from_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
2774 match expr {
2775 Expression::ArrayExpression(arr) => Some(arr),
2776 Expression::ParenthesizedExpression(paren) => array_from_expression(&paren.expression),
2777 Expression::TSAsExpression(ts_as) => array_from_expression(&ts_as.expression),
2778 Expression::TSSatisfiesExpression(ts_sat) => array_from_expression(&ts_sat.expression),
2779 Expression::CallExpression(call) => call
2780 .arguments
2781 .first()
2782 .and_then(Argument::as_expression)
2783 .and_then(array_from_expression),
2784 _ => None,
2785 }
2786}
2787
2788pub(crate) fn lexical_normalize(path: &Path) -> PathBuf {
2789 let mut normalized = PathBuf::new();
2790
2791 for component in path.components() {
2792 match component {
2793 std::path::Component::CurDir => {}
2794 std::path::Component::ParentDir => {
2795 normalized.pop();
2796 }
2797 _ => normalized.push(component.as_os_str()),
2798 }
2799 }
2800
2801 normalized
2802}
2803
2804pub(crate) fn is_inside_root(path: &Path, root: &Path) -> bool {
2810 let Ok(resolved) = std::fs::canonicalize(path) else {
2811 return false;
2812 };
2813 let root = std::fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
2814 resolved.starts_with(root)
2815}
2816
2817pub(crate) fn glob_base_is_inside_root(dir: &Path, pattern: &str, root: &Path) -> bool {
2826 let literal = &pattern[..pattern.find(['*', '?', '[', '{']).unwrap_or(pattern.len())];
2827 lexical_normalize(&dir.join(path_from_config_string(literal))).starts_with(root)
2828}
2829
2830pub(crate) fn is_package_specifier(specifier: &str) -> bool {
2834 !specifier.is_empty()
2835 && specifier != "."
2836 && specifier != ".."
2837 && !specifier.starts_with("./")
2838 && !specifier.starts_with("../")
2839 && !specifier.starts_with('/')
2840 && !specifier.contains(':')
2841 && !specifier.contains('\\')
2842 && !specifier.chars().any(char::is_whitespace)
2843}
2844
2845fn expression_to_string_array(expr: &Expression) -> Vec<String> {
2847 match expr {
2848 Expression::ArrayExpression(arr) => arr
2849 .elements
2850 .iter()
2851 .filter_map(|el| match el {
2852 ArrayExpressionElement::SpreadElement(_) => None,
2853 _ => el.as_expression().and_then(expression_to_string),
2854 })
2855 .collect(),
2856 _ => vec![],
2857 }
2858}
2859
2860fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
2865 let mut values = Vec::new();
2866 match expr {
2867 Expression::StringLiteral(s) => {
2868 values.push(s.value.to_string());
2869 }
2870 Expression::ArrayExpression(arr) => {
2871 for el in &arr.elements {
2872 if let Some(inner) = el.as_expression() {
2873 match inner {
2874 Expression::StringLiteral(s) => {
2875 values.push(s.value.to_string());
2876 }
2877 Expression::ArrayExpression(sub_arr) => {
2878 if let Some(first) = sub_arr.elements.first()
2879 && let Some(first_expr) = first.as_expression()
2880 && let Some(s) = expression_to_string(first_expr)
2881 {
2882 values.push(s);
2883 }
2884 }
2885 _ => {}
2886 }
2887 }
2888 }
2889 }
2890 Expression::ObjectExpression(obj) => {
2891 for prop in &obj.properties {
2892 if let ObjectPropertyKind::ObjectProperty(p) = prop {
2893 match &p.value {
2894 Expression::StringLiteral(s) => {
2895 values.push(s.value.to_string());
2896 }
2897 Expression::ArrayExpression(sub_arr) => {
2898 if let Some(first) = sub_arr.elements.first()
2899 && let Some(first_expr) = first.as_expression()
2900 && let Some(s) = expression_to_string(first_expr)
2901 {
2902 values.push(s);
2903 }
2904 }
2905 _ => {}
2906 }
2907 }
2908 }
2909 }
2910 _ => {}
2911 }
2912 values
2913}
2914
2915fn collect_shallow_string_or_object_property_values(
2917 expr: &Expression,
2918 object_property: &str,
2919) -> Vec<String> {
2920 match expr {
2921 Expression::ArrayExpression(arr) => arr
2922 .elements
2923 .iter()
2924 .filter_map(|element| {
2925 element
2926 .as_expression()
2927 .and_then(|expr| shallow_string_or_object_property(expr, object_property))
2928 })
2929 .collect(),
2930 _ => shallow_string_or_object_property(expr, object_property)
2931 .into_iter()
2932 .collect(),
2933 }
2934}
2935
2936fn shallow_string_or_object_property(expr: &Expression, object_property: &str) -> Option<String> {
2937 match expr {
2938 Expression::ParenthesizedExpression(paren) => {
2939 shallow_string_or_object_property(&paren.expression, object_property)
2940 }
2941 Expression::TSSatisfiesExpression(ts_sat) => {
2942 shallow_string_or_object_property(&ts_sat.expression, object_property)
2943 }
2944 Expression::TSAsExpression(ts_as) => {
2945 shallow_string_or_object_property(&ts_as.expression, object_property)
2946 }
2947 Expression::ArrayExpression(sub_arr) => sub_arr
2948 .elements
2949 .first()
2950 .and_then(ArrayExpressionElement::as_expression)
2951 .and_then(expression_to_string),
2952 Expression::ObjectExpression(obj) => {
2953 find_property(obj, object_property).and_then(|prop| expression_to_string(&prop.value))
2954 }
2955 _ => expression_to_string(expr),
2956 }
2957}
2958
2959fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
2961 match expr {
2962 Expression::StringLiteral(s) => {
2963 values.push(s.value.to_string());
2964 }
2965 Expression::ArrayExpression(arr) => {
2966 for el in &arr.elements {
2967 if let Some(expr) = el.as_expression() {
2968 collect_all_string_values(expr, values);
2969 }
2970 }
2971 }
2972 Expression::ObjectExpression(obj) => {
2973 for prop in &obj.properties {
2974 if let ObjectPropertyKind::ObjectProperty(p) = prop {
2975 collect_all_string_values(&p.value, values);
2976 }
2977 }
2978 }
2979 _ => {}
2980 }
2981}
2982
2983fn property_key_to_string(key: &PropertyKey) -> Option<String> {
2985 match key {
2986 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
2987 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
2988 _ => None,
2989 }
2990}
2991
2992fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
2994 if path.is_empty() {
2995 return None;
2996 }
2997 let prop = find_property(obj, path[0])?;
2998 if path.len() == 1 {
2999 if let Expression::ObjectExpression(nested) = &prop.value {
3000 let keys = nested
3001 .properties
3002 .iter()
3003 .filter_map(|p| {
3004 if let ObjectPropertyKind::ObjectProperty(p) = p {
3005 property_key_to_string(&p.key)
3006 } else {
3007 None
3008 }
3009 })
3010 .collect();
3011 return Some(keys);
3012 }
3013 return None;
3014 }
3015 if let Expression::ObjectExpression(nested) = &prop.value {
3016 get_nested_object_keys(nested, &path[1..])
3017 } else {
3018 None
3019 }
3020}
3021
3022fn get_nested_expression<'a>(
3024 obj: &'a ObjectExpression<'a>,
3025 path: &[&str],
3026) -> Option<&'a Expression<'a>> {
3027 if path.is_empty() {
3028 return None;
3029 }
3030 let prop = find_property(obj, path[0])?;
3031 if path.len() == 1 {
3032 return Some(&prop.value);
3033 }
3034 if let Expression::ObjectExpression(nested) = &prop.value {
3035 get_nested_expression(nested, &path[1..])
3036 } else {
3037 None
3038 }
3039}
3040
3041fn get_nested_string_or_array(
3043 obj: &ObjectExpression,
3044 path: &[&str],
3045 scope: PathScope<'_>,
3046) -> Option<Vec<String>> {
3047 if path.is_empty() {
3048 return None;
3049 }
3050 if path.len() == 1 {
3051 let prop = find_property(obj, path[0])?;
3052 return Some(string_or_array_in(&prop.value, scope));
3053 }
3054 let prop = find_property(obj, path[0])?;
3055 if let Expression::ObjectExpression(nested) = &prop.value {
3056 get_nested_string_or_array(nested, &path[1..], scope)
3057 } else {
3058 None
3059 }
3060}
3061
3062pub(crate) fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
3070 string_or_array_in(expr, PathScope::Bare)
3071}
3072
3073fn string_or_array_in(expr: &Expression, scope: PathScope<'_>) -> Vec<String> {
3074 match expr {
3075 Expression::StringLiteral(s) => vec![s.value.to_string()],
3076 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
3077 .quasis
3078 .first()
3079 .map(|q| vec![q.value.raw.to_string()])
3080 .unwrap_or_default(),
3081 Expression::ArrayExpression(arr) => arr
3082 .elements
3083 .iter()
3084 .filter_map(|el| el.as_expression())
3085 .flat_map(|e| match e {
3086 Expression::ObjectExpression(obj) => find_property(obj, "input")
3087 .map(|p| string_or_array_in(&p.value, scope))
3088 .unwrap_or_default(),
3089 _ => path_string_in(e, scope).into_iter().collect(),
3090 })
3091 .collect(),
3092 Expression::ObjectExpression(obj) => obj
3093 .properties
3094 .iter()
3095 .flat_map(|p| {
3096 if let ObjectPropertyKind::ObjectProperty(p) = p {
3097 match &p.value {
3098 Expression::ArrayExpression(_) => string_or_array_in(&p.value, scope),
3099 Expression::ObjectExpression(value_obj) => {
3100 find_property(value_obj, "import")
3101 .map(|import_prop| string_or_array_in(&import_prop.value, scope))
3102 .unwrap_or_default()
3103 }
3104 _ => path_string_in(&p.value, scope).into_iter().collect(),
3105 }
3106 } else {
3107 Vec::new()
3108 }
3109 })
3110 .collect(),
3111 _ => path_string_in(expr, scope).into_iter().collect(),
3112 }
3113}
3114
3115fn collect_require_sources(expr: &Expression) -> Vec<String> {
3117 let mut sources = Vec::new();
3118 match expr {
3119 Expression::CallExpression(call) if is_require_call(call) => {
3120 if let Some(s) = get_require_source(call) {
3121 sources.push(s);
3122 }
3123 }
3124 Expression::ArrayExpression(arr) => {
3125 for el in &arr.elements {
3126 if let Some(inner) = el.as_expression() {
3127 match inner {
3128 Expression::CallExpression(call) if is_require_call(call) => {
3129 if let Some(s) = get_require_source(call) {
3130 sources.push(s);
3131 }
3132 }
3133 Expression::ArrayExpression(sub_arr) => {
3134 if let Some(first) = sub_arr.elements.first()
3135 && let Some(Expression::CallExpression(call)) =
3136 first.as_expression()
3137 && is_require_call(call)
3138 && let Some(s) = get_require_source(call)
3139 {
3140 sources.push(s);
3141 }
3142 }
3143 _ => {}
3144 }
3145 }
3146 }
3147 }
3148 _ => {}
3149 }
3150 sources
3151}
3152
3153pub(crate) fn is_require_call(call: &CallExpression) -> bool {
3155 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
3156}
3157
3158pub(crate) fn get_require_source(call: &CallExpression) -> Option<String> {
3160 call.arguments.first().and_then(|arg| {
3161 if let Argument::StringLiteral(s) = arg {
3162 Some(s.value.to_string())
3163 } else {
3164 None
3165 }
3166 })
3167}
3168
3169#[cfg(test)]
3170mod tests {
3171 use super::*;
3172 use std::path::PathBuf;
3173
3174 fn js_path() -> PathBuf {
3175 PathBuf::from("config.js")
3176 }
3177
3178 fn ts_path() -> PathBuf {
3179 PathBuf::from("config.ts")
3180 }
3181
3182 #[test]
3183 fn extract_config_array_tuple_heads_reads_first_string_of_each_tuple() {
3184 let source = r#"
3185 export default defineNuxtConfig({
3186 modules: ["@nuxt/image", ["@nuxt/content", { watch: false }], [someModule, {}], []]
3187 });
3188 "#;
3189 assert_eq!(
3190 extract_config_array_tuple_heads(source, &ts_path(), &["modules"]),
3191 vec!["@nuxt/content".to_string()]
3192 );
3193 }
3194
3195 #[test]
3196 fn extract_lazy_imports_bare_arrows() {
3197 let source = r"
3198 import { defineConfig } from '@adonisjs/core/app'
3199 export default defineConfig({
3200 preloads: [
3201 () => import('#start/routes'),
3202 () => import('#start/kernel'),
3203 ],
3204 })
3205 ";
3206 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["preloads"]);
3207 assert_eq!(specs, vec!["#start/routes", "#start/kernel"]);
3208 }
3209
3210 #[test]
3211 fn extract_lazy_imports_object_form_with_file_key() {
3212 let source = r"
3213 export default defineConfig({
3214 providers: [
3215 () => import('@adonisjs/core/providers/app_provider'),
3216 {
3217 file: () => import('@adonisjs/core/providers/repl_provider'),
3218 environment: ['repl', 'test'],
3219 },
3220 ],
3221 })
3222 ";
3223 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
3224 assert_eq!(
3225 specs,
3226 vec![
3227 "@adonisjs/core/providers/app_provider",
3228 "@adonisjs/core/providers/repl_provider",
3229 ]
3230 );
3231 }
3232
3233 #[test]
3234 fn extract_lazy_imports_block_body_with_return() {
3235 let source = r"
3236 export default defineConfig({
3237 commands: [
3238 () => { return import('@adonisjs/core/commands') },
3239 ],
3240 })
3241 ";
3242 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
3243 assert_eq!(specs, vec!["@adonisjs/core/commands"]);
3244 }
3245
3246 #[test]
3247 fn extract_lazy_imports_skips_unknown_element_shapes() {
3248 let source = r"
3249 export default defineConfig({
3250 commands: [
3251 'string-entry',
3252 42,
3253 { other: 'value' },
3254 () => import('@adonisjs/lucid/commands'),
3255 ],
3256 })
3257 ";
3258 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
3259 assert_eq!(specs, vec!["@adonisjs/lucid/commands"]);
3260 }
3261
3262 #[test]
3263 fn extract_lazy_imports_missing_property_returns_empty() {
3264 let source = r"
3265 export default defineConfig({
3266 preloads: [() => import('#start/routes')],
3267 })
3268 ";
3269 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
3270 assert!(specs.is_empty());
3271 }
3272
3273 #[test]
3274 fn extract_imports_basic() {
3275 let source = r"
3276 import foo from 'foo-pkg';
3277 import { bar } from '@scope/bar';
3278 export default {};
3279 ";
3280 let imports = extract_imports(source, &js_path());
3281 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
3282 }
3283
3284 #[test]
3285 fn extract_default_export_object_property() {
3286 let source = r#"export default { testDir: "./tests" };"#;
3287 let val = extract_config_string(source, &js_path(), &["testDir"]);
3288 assert_eq!(val, Some("./tests".to_string()));
3289 }
3290
3291 #[test]
3292 fn extract_define_config_property() {
3293 let source = r#"
3294 import { defineConfig } from 'vitest/config';
3295 export default defineConfig({
3296 test: {
3297 include: ["**/*.test.ts", "**/*.spec.ts"],
3298 setupFiles: ["./test/setup.ts"]
3299 }
3300 });
3301 "#;
3302 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
3303 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
3304
3305 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
3306 assert_eq!(setup, vec!["./test/setup.ts"]);
3307 }
3308
3309 #[test]
3310 fn extract_module_exports_property() {
3311 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
3312 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
3313 assert_eq!(val, Some("jsdom".to_string()));
3314 }
3315
3316 #[test]
3317 fn extract_nested_string_array() {
3318 let source = r#"
3319 export default {
3320 resolve: {
3321 alias: {
3322 "@": "./src"
3323 }
3324 },
3325 test: {
3326 include: ["src/**/*.test.ts"]
3327 }
3328 };
3329 "#;
3330 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
3331 assert_eq!(include, vec!["src/**/*.test.ts"]);
3332 }
3333
3334 #[test]
3335 fn extract_addons_array() {
3336 let source = r#"
3337 export default {
3338 addons: [
3339 "@storybook/addon-a11y",
3340 "@storybook/addon-docs",
3341 "@storybook/addon-links"
3342 ]
3343 };
3344 "#;
3345 let addons = extract_config_property_strings(source, &ts_path(), "addons");
3346 assert_eq!(
3347 addons,
3348 vec![
3349 "@storybook/addon-a11y",
3350 "@storybook/addon-docs",
3351 "@storybook/addon-links"
3352 ]
3353 );
3354 }
3355
3356 #[test]
3357 fn handle_empty_config() {
3358 let source = "";
3359 let result = extract_config_string(source, &js_path(), &["key"]);
3360 assert_eq!(result, None);
3361 }
3362
3363 #[test]
3364 fn object_keys_postcss_plugins() {
3365 let source = r"
3366 module.exports = {
3367 plugins: {
3368 autoprefixer: {},
3369 tailwindcss: {},
3370 'postcss-import': {}
3371 }
3372 };
3373 ";
3374 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3375 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
3376 }
3377
3378 #[test]
3379 fn object_keys_nested_path() {
3380 let source = r"
3381 export default {
3382 build: {
3383 plugins: {
3384 minify: {},
3385 compress: {}
3386 }
3387 }
3388 };
3389 ";
3390 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
3391 assert_eq!(keys, vec!["minify", "compress"]);
3392 }
3393
3394 #[test]
3395 fn object_keys_empty_object() {
3396 let source = r"export default { plugins: {} };";
3397 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3398 assert!(keys.is_empty());
3399 }
3400
3401 #[test]
3402 fn object_keys_non_object_returns_empty() {
3403 let source = r#"export default { plugins: ["a", "b"] };"#;
3404 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3405 assert!(keys.is_empty());
3406 }
3407
3408 #[test]
3409 fn string_or_array_single_string() {
3410 let source = r#"export default { entry: "./src/index.js" };"#;
3411 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3412 assert_eq!(result, vec!["./src/index.js"]);
3413 }
3414
3415 #[test]
3416 fn string_or_array_array() {
3417 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
3418 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3419 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
3420 }
3421
3422 #[test]
3423 fn string_or_array_object_values() {
3424 let source =
3425 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
3426 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3427 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
3428 }
3429
3430 #[test]
3431 fn array_or_object_string_or_array_reads_each_array_element() {
3432 let source = r#"
3433 const shared = { format: ["esm"] };
3434 export default defineConfig([
3435 { entry: "./src/a.ts" },
3436 ({ ...shared, entry: { b: "./src/b.ts" } }),
3437 { entry: ["./src/c.ts"] } satisfies Options,
3438 { format: ["cjs"] },
3439 shared,
3440 ] as Options[]);
3441 "#;
3442 let result = extract_config_array_or_object_string_or_array(source, &ts_path(), &["entry"]);
3443 assert_eq!(result, vec!["./src/a.ts", "./src/b.ts", "./src/c.ts"]);
3444 }
3445
3446 #[test]
3447 fn array_or_object_string_or_array_falls_back_to_config_object() {
3448 let source = r#"export default defineConfig({ entry: { main: "./src/main.ts" } });"#;
3449 let result = extract_config_array_or_object_string_or_array(source, &ts_path(), &["entry"]);
3450 assert_eq!(result, vec!["./src/main.ts"]);
3451 }
3452
3453 #[test]
3454 fn string_or_array_object_array_values() {
3455 let source = r#"export default { entry: { app: ["./src/polyfill.js", "./src/app.js"] } };"#;
3456 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3457 assert_eq!(result, vec!["./src/polyfill.js", "./src/app.js"]);
3458 }
3459
3460 #[test]
3461 fn string_or_array_webpack_entry_descriptors() {
3462 let source = r#"
3463 export default {
3464 entry: {
3465 app: {
3466 import: "./src/app.js",
3467 filename: "pages/app.js",
3468 dependOn: "shared",
3469 },
3470 admin: {
3471 import: ["./src/admin-polyfill.js", "./src/admin.js"],
3472 runtime: "runtime",
3473 },
3474 shared: ["react", "react-dom"],
3475 },
3476 };
3477 "#;
3478 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3479 assert_eq!(
3480 result,
3481 vec![
3482 "./src/app.js",
3483 "./src/admin-polyfill.js",
3484 "./src/admin.js",
3485 "react",
3486 "react-dom"
3487 ]
3488 );
3489 }
3490
3491 #[test]
3492 fn string_or_array_nested_path() {
3493 let source = r#"
3494 export default {
3495 build: {
3496 rollupOptions: {
3497 input: ["./index.html", "./about.html"]
3498 }
3499 }
3500 };
3501 "#;
3502 let result = extract_config_string_or_array(
3503 source,
3504 &js_path(),
3505 &["build", "rollupOptions", "input"],
3506 );
3507 assert_eq!(result, vec!["./index.html", "./about.html"]);
3508 }
3509
3510 #[test]
3511 fn string_or_array_template_literal() {
3512 let source = r"export default { entry: `./src/index.js` };";
3513 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3514 assert_eq!(result, vec!["./src/index.js"]);
3515 }
3516
3517 #[test]
3518 fn string_or_array_object_path_helper_values() {
3519 let source = r#"
3520 import { resolve, join } from "node:path";
3521 import path from "node:path";
3522 export default {
3523 build: {
3524 rollupOptions: {
3525 input: {
3526 app: resolve(__dirname, "src/app.ts"),
3527 modal: path.resolve(__dirname, "src/modal.ts"),
3528 tabs: join(__dirname, "src/tabs.ts"),
3529 styles: resolve(__dirname, "src/index.css"),
3530 },
3531 },
3532 },
3533 };
3534 "#;
3535 let result = extract_config_string_or_array(
3536 source,
3537 &js_path(),
3538 &["build", "rollupOptions", "input"],
3539 );
3540 assert_eq!(
3541 result,
3542 vec!["src/app.ts", "src/modal.ts", "src/tabs.ts", "src/index.css"]
3543 );
3544 }
3545
3546 #[test]
3547 fn string_or_array_array_path_helper_values() {
3548 let source = r#"
3549 import { resolve } from "node:path";
3550 export default {
3551 build: {
3552 rollupOptions: {
3553 input: [resolve(__dirname, "src/a.ts"), "./src/b.ts"],
3554 },
3555 },
3556 };
3557 "#;
3558 let result = extract_config_string_or_array(
3559 source,
3560 &js_path(),
3561 &["build", "rollupOptions", "input"],
3562 );
3563 assert_eq!(result, vec!["src/a.ts", "./src/b.ts"]);
3564 }
3565
3566 #[test]
3567 fn string_or_array_top_level_path_helper_call() {
3568 let source = r#"
3569 import { resolve } from "node:path";
3570 export default { build: { lib: { entry: resolve(__dirname, "src/index.ts") } } };
3571 "#;
3572 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
3573 assert_eq!(result, vec!["src/index.ts"]);
3574 }
3575
3576 #[test]
3577 fn string_or_array_import_meta_dirname_anchor() {
3578 let source = r#"
3579 import { resolve } from "node:path";
3580 export default {
3581 build: { lib: { entry: resolve(import.meta.dirname, "src/index.ts") } },
3582 };
3583 "#;
3584 let result = extract_config_string_or_array(source, &ts_path(), &["build", "lib", "entry"]);
3585 assert_eq!(result, vec!["src/index.ts"]);
3586 }
3587
3588 #[test]
3589 fn string_or_array_non_literal_path_helper_args_dropped() {
3590 let source = r#"
3591 import { resolve } from "node:path";
3592 export default { build: { lib: { entry: resolve(baseDir, "src/index.ts") } } };
3593 "#;
3594 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
3595 assert!(
3596 result.is_empty(),
3597 "non-literal path-helper args must be dropped: {result:?}"
3598 );
3599 }
3600
3601 #[test]
3602 fn require_strings_array() {
3603 let source = r"
3604 module.exports = {
3605 plugins: [
3606 require('autoprefixer'),
3607 require('postcss-import')
3608 ]
3609 };
3610 ";
3611 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3612 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
3613 }
3614
3615 #[test]
3616 fn require_strings_with_tuples() {
3617 let source = r"
3618 module.exports = {
3619 plugins: [
3620 require('autoprefixer'),
3621 [require('postcss-preset-env'), { stage: 3 }]
3622 ]
3623 };
3624 ";
3625 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3626 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
3627 }
3628
3629 #[test]
3630 fn require_strings_empty_array() {
3631 let source = r"module.exports = { plugins: [] };";
3632 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3633 assert!(deps.is_empty());
3634 }
3635
3636 #[test]
3637 fn require_strings_no_require_calls() {
3638 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
3639 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3640 assert!(deps.is_empty());
3641 }
3642
3643 #[test]
3644 fn extract_aliases_from_object_with_file_url_to_path() {
3645 let source = r#"
3646 import { defineConfig } from 'vite';
3647 import { fileURLToPath, URL } from 'node:url';
3648
3649 export default defineConfig({
3650 resolve: {
3651 alias: {
3652 "@": fileURLToPath(new URL("./src", import.meta.url))
3653 }
3654 }
3655 });
3656 "#;
3657
3658 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
3659 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
3660 }
3661
3662 #[test]
3663 fn extract_aliases_from_array_form() {
3664 let source = r#"
3665 export default {
3666 resolve: {
3667 alias: [
3668 { find: "@", replacement: "./src" },
3669 { find: "$utils", replacement: "src/lib/utils" }
3670 ]
3671 }
3672 };
3673 "#;
3674
3675 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
3676 assert_eq!(
3677 aliases,
3678 vec![
3679 ("@".to_string(), "./src".to_string()),
3680 ("$utils".to_string(), "src/lib/utils".to_string())
3681 ]
3682 );
3683 }
3684
3685 #[test]
3686 fn extract_aliases_from_object_with_array_values() {
3687 let source = r#"
3688 ({
3689 compilerOptions: {
3690 paths: {
3691 "@/*": ["./src/*"],
3692 "@shared/*": ["./shared/*", "./fallback/*"]
3693 }
3694 }
3695 })
3696 "#;
3697
3698 let aliases = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
3699 assert_eq!(
3700 aliases,
3701 vec![
3702 ("@/*".to_string(), "./src/*".to_string()),
3703 ("@shared/*".to_string(), "./shared/*".to_string())
3704 ]
3705 );
3706 }
3707
3708 #[test]
3709 fn extract_array_object_strings_mixed_forms() {
3710 let source = r#"
3711 export default {
3712 components: [
3713 "~/components",
3714 { path: "@/feature-components" }
3715 ]
3716 };
3717 "#;
3718
3719 let values =
3720 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
3721 assert_eq!(
3722 values,
3723 vec![
3724 "~/components".to_string(),
3725 "@/feature-components".to_string()
3726 ]
3727 );
3728 }
3729
3730 #[test]
3731 fn extract_config_plugin_option_string_from_json() {
3732 let source = r#"{
3733 "expo": {
3734 "plugins": [
3735 ["expo-router", { "root": "src/app" }]
3736 ]
3737 }
3738 }"#;
3739
3740 let value = extract_config_plugin_option_string(
3741 source,
3742 &json_path(),
3743 &["expo", "plugins"],
3744 "expo-router",
3745 "root",
3746 );
3747
3748 assert_eq!(value, Some("src/app".to_string()));
3749 }
3750
3751 #[test]
3752 fn extract_config_plugin_option_string_from_top_level_plugins() {
3753 let source = r#"{
3754 "plugins": [
3755 ["expo-router", { "root": "./src/routes" }]
3756 ]
3757 }"#;
3758
3759 let value = extract_config_plugin_option_string_from_paths(
3760 source,
3761 &json_path(),
3762 &[&["plugins"], &["expo", "plugins"]],
3763 "expo-router",
3764 "root",
3765 );
3766
3767 assert_eq!(value, Some("./src/routes".to_string()));
3768 }
3769
3770 #[test]
3771 fn extract_config_plugin_option_string_from_ts_config() {
3772 let source = r"
3773 export default {
3774 expo: {
3775 plugins: [
3776 ['expo-router', { root: './src/app' }]
3777 ]
3778 }
3779 };
3780 ";
3781
3782 let value = extract_config_plugin_option_string(
3783 source,
3784 &ts_path(),
3785 &["expo", "plugins"],
3786 "expo-router",
3787 "root",
3788 );
3789
3790 assert_eq!(value, Some("./src/app".to_string()));
3791 }
3792
3793 #[test]
3794 fn extract_config_plugin_option_string_returns_none_when_plugin_missing() {
3795 let source = r#"{
3796 "expo": {
3797 "plugins": [
3798 ["expo-font", {}]
3799 ]
3800 }
3801 }"#;
3802
3803 let value = extract_config_plugin_option_string(
3804 source,
3805 &json_path(),
3806 &["expo", "plugins"],
3807 "expo-router",
3808 "root",
3809 );
3810
3811 assert_eq!(value, None);
3812 }
3813
3814 #[test]
3815 fn vite_react_babel_dependencies_extract_plain_tuple_and_prefixed_entries() {
3816 let source = r#"
3817 import react from "@vitejs/plugin-react";
3818
3819 export default defineConfig({
3820 plugins: [
3821 react({
3822 babel: {
3823 plugins: [
3824 "babel-plugin-plain",
3825 ["module:@preact/signals-react-transform", { mode: "auto" }],
3826 ],
3827 presets: [["@babel/preset-react", { runtime: "automatic" }]],
3828 },
3829 }),
3830 ],
3831 });
3832 "#;
3833
3834 let deps = extract_vite_plugin_option_dependencies(source, &ts_path());
3835
3836 assert_eq!(
3837 deps,
3838 vec![
3839 "babel-plugin-plain".to_string(),
3840 "@preact/signals-react-transform".to_string(),
3841 "@babel/preset-react".to_string(),
3842 ]
3843 );
3844 }
3845
3846 #[test]
3847 fn vite_react_babel_dependencies_support_default_alias_import() {
3848 let source = r#"
3849 import { default as viteReact } from "@vitejs/plugin-react";
3850
3851 export default {
3852 plugins: [
3853 viteReact({
3854 babel: {
3855 plugins: [["module:@scope/pkg/plugin", {}]],
3856 },
3857 }),
3858 ],
3859 };
3860 "#;
3861
3862 let deps = extract_vite_plugin_option_dependencies(source, &ts_path());
3863
3864 assert_eq!(deps, vec!["@scope/pkg".to_string()]);
3865 }
3866
3867 #[test]
3868 fn vite_react_babel_dependencies_ignore_unrelated_plugin_calls() {
3869 let source = r#"
3870 import vue from "@vitejs/plugin-vue";
3871
3872 export default {
3873 plugins: [
3874 vue({
3875 babel: {
3876 plugins: ["@preact/signals-react-transform"],
3877 },
3878 }),
3879 ],
3880 };
3881 "#;
3882
3883 let deps = extract_vite_plugin_option_dependencies(source, &ts_path());
3884
3885 assert!(deps.is_empty());
3886 }
3887
3888 #[test]
3889 fn vite_react_babel_dependencies_skip_relative_and_protocol_entries() {
3890 let source = r#"
3891 import react from "@vitejs/plugin-react";
3892
3893 export default {
3894 plugins: [
3895 react({
3896 babel: {
3897 plugins: ["./local-plugin", "module:./local-prefixed", "http://example.com/plugin"],
3898 },
3899 }),
3900 ],
3901 };
3902 "#;
3903
3904 let deps = extract_vite_plugin_option_dependencies(source, &ts_path());
3905
3906 assert!(deps.is_empty());
3907 }
3908
3909 #[test]
3910 fn normalize_config_path_relative_to_root() {
3911 let config_path = PathBuf::from("/project/vite.config.ts");
3912 let root = PathBuf::from("/project");
3913
3914 assert_eq!(
3915 normalize_config_path("./src/lib", &config_path, &root),
3916 Some("src/lib".to_string())
3917 );
3918 assert_eq!(
3919 normalize_config_path("/src/lib", &config_path, &root),
3920 Some("src/lib".to_string())
3921 );
3922 }
3923
3924 #[test]
3925 fn normalize_config_path_mixed_separators_and_parent_dirs() {
3926 let config_path = PathBuf::from("/project/config/vite.config.ts");
3927 let root = PathBuf::from("/project");
3928
3929 assert_eq!(
3930 normalize_config_path(".\\src\\..\\app\\lib", &config_path, &root),
3931 Some("config/app/lib".to_string())
3932 );
3933 }
3934
3935 #[test]
3938 fn normalize_config_path_reads_an_absolute_path_under_root_as_absolute() {
3939 let root = std::env::temp_dir().join("fallow-2806-project");
3940 let config_path = root.join("vite.config.ts");
3941 let absolute = path_to_config_string(&root.join("src").join("lib"));
3942
3943 assert_eq!(
3944 normalize_config_path(&absolute, &config_path, &root),
3945 Some("src/lib".to_string())
3946 );
3947 assert_eq!(
3948 normalize_filesystem_config_path(&absolute, &config_path, &root),
3949 Some("src/lib".to_string())
3950 );
3951 }
3952
3953 #[cfg(unix)]
3958 #[test]
3959 fn normalize_config_path_keeps_the_vite_root_in_root_reading() {
3960 let temp = tempfile::tempdir().expect("temp dir");
3961 let root = temp.path().join("src");
3962 std::fs::create_dir_all(&root).expect("root dir");
3963 let config_path = root.join("vite.config.ts");
3964 let root_string = path_to_config_string(&root);
3965 let nested = root_string.trim_start_matches('/').to_string();
3966 let under = format!("{root_string}/lib");
3967
3968 assert_eq!(
3969 normalize_config_path(&root_string, &config_path, &root),
3970 Some(nested.clone())
3971 );
3972 assert_eq!(
3973 normalize_config_path(&under, &config_path, &root),
3974 Some("lib".to_string())
3975 );
3976
3977 std::fs::create_dir_all(root.join(&nested)).expect("root-in-root dir");
3978 assert_eq!(
3979 normalize_config_path(&under, &config_path, &root),
3980 Some(format!("{nested}/lib"))
3981 );
3982 assert_eq!(
3983 normalize_filesystem_config_path(&under, &config_path, &root),
3984 Some("lib".to_string())
3985 );
3986 }
3987
3988 #[test]
3991 fn normalize_filesystem_config_path_does_not_read_a_leading_slash_as_root_relative() {
3992 let config_path = PathBuf::from("/project/config/webpack.config.js");
3993 let root = PathBuf::from("/project");
3994
3995 assert_eq!(
3996 normalize_filesystem_config_path("/src/lib", &config_path, &root),
3997 None
3998 );
3999 assert_eq!(
4000 normalize_filesystem_config_path("./src/lib", &config_path, &root),
4001 Some("config/src/lib".to_string())
4002 );
4003 assert_eq!(
4004 normalize_config_path("/src/lib", &config_path, &root),
4005 Some("src/lib".to_string())
4006 );
4007 }
4008
4009 #[test]
4010 fn normalize_config_path_leading_slash_stays_project_relative() {
4011 let config_path = PathBuf::from("/project/vite.config.ts");
4012 let root = PathBuf::from("/project");
4013
4014 assert_eq!(
4015 normalize_config_path("/src\\lib", &config_path, &root),
4016 Some("src/lib".to_string())
4017 );
4018 }
4019
4020 #[test]
4021 fn json_wrapped_in_parens_string() {
4022 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
4023 let val = extract_config_string(source, &js_path(), &["extends"]);
4024 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
4025 }
4026
4027 #[test]
4028 fn json_wrapped_in_parens_nested_array() {
4029 let source =
4030 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
4031 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
4032 assert_eq!(types, vec!["node", "jest"]);
4033
4034 let include = extract_config_string_array(source, &js_path(), &["include"]);
4035 assert_eq!(include, vec!["src/**/*"]);
4036 }
4037
4038 #[test]
4039 fn json_wrapped_in_parens_object_keys() {
4040 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
4041 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
4042 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
4043 }
4044
4045 fn json_path() -> PathBuf {
4046 PathBuf::from("config.json")
4047 }
4048
4049 #[test]
4050 fn json_file_parsed_correctly() {
4051 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
4052 let val = extract_config_string(source, &json_path(), &["key"]);
4053 assert_eq!(val, Some("value".to_string()));
4054
4055 let list = extract_config_string_array(source, &json_path(), &["list"]);
4056 assert_eq!(list, vec!["a", "b"]);
4057 }
4058
4059 #[test]
4060 fn jsonc_file_parsed_correctly() {
4061 let source = r#"{"key": "value"}"#;
4062 let path = PathBuf::from("tsconfig.jsonc");
4063 let val = extract_config_string(source, &path, &["key"]);
4064 assert_eq!(val, Some("value".to_string()));
4065 }
4066
4067 #[test]
4068 fn extract_define_config_arrow_function() {
4069 let source = r#"
4070 import { defineConfig } from 'vite';
4071 export default defineConfig(() => ({
4072 test: {
4073 include: ["**/*.test.ts"]
4074 }
4075 }));
4076 "#;
4077 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
4078 assert_eq!(include, vec!["**/*.test.ts"]);
4079 }
4080
4081 #[test]
4085 fn extract_define_config_block_body_arrow_function() {
4086 let source = r#"
4087 import { defineConfig } from 'vite';
4088 export default defineConfig(({ mode }) => {
4089 const isProduction = mode === 'production';
4090 return {
4091 test: {
4092 environment: "jsdom",
4093 setupFiles: "./tests/setup.ts"
4094 },
4095 base: isProduction ? '/app/' : '/'
4096 };
4097 });
4098 "#;
4099 assert_eq!(
4100 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4101 Some("jsdom")
4102 );
4103 assert_eq!(
4104 extract_config_string(source, &ts_path(), &["test", "setupFiles"]).as_deref(),
4105 Some("./tests/setup.ts")
4106 );
4107 }
4108
4109 #[test]
4110 fn extract_define_config_function_expression() {
4111 let source = r#"
4112 import { defineConfig } from 'vite';
4113 export default defineConfig(function () {
4114 return { test: { environment: "happy-dom" } };
4115 });
4116 "#;
4117 assert_eq!(
4118 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4119 Some("happy-dom")
4120 );
4121 }
4122
4123 #[test]
4127 fn wrapper_options_object_does_not_shadow_named_config_arg() {
4128 let source = r#"
4129 const nextConfig = { pageExtensions: ["page.tsx"] };
4130 module.exports = withSentryConfig(nextConfig, { org: "o", project: "p", silent: true });
4131 "#;
4132 assert_eq!(
4133 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
4134 vec!["page.tsx"]
4135 );
4136 }
4137
4138 #[test]
4139 fn wrapper_options_object_does_not_shadow_named_config_arg_esm() {
4140 let source = r#"
4141 const nextConfig = { pageExtensions: ["page.tsx"] };
4142 export default withSentryConfig(nextConfig, { org: "o", project: "p" });
4143 "#;
4144 assert_eq!(
4145 extract_config_string_array(source, &ts_path(), &["pageExtensions"]),
4146 vec!["page.tsx"]
4147 );
4148 }
4149
4150 #[test]
4152 fn merge_config_extracts_nested_define_config_object() {
4153 let source = r#"
4154 import { defineConfig, mergeConfig } from 'vitest/config';
4155 import viteConfig from './vite.config';
4156 export default mergeConfig(viteConfig, defineConfig({
4157 test: { environment: "jsdom" }
4158 }));
4159 "#;
4160 assert_eq!(
4161 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4162 Some("jsdom")
4163 );
4164 }
4165
4166 #[test]
4167 fn define_config_wrapping_merge_config_extracts_object() {
4168 let source = r#"
4169 import { defineConfig, mergeConfig } from 'vitest/config';
4170 import base from './base';
4171 export default defineConfig(mergeConfig(base, { test: { environment: "jsdom" } }));
4172 "#;
4173 assert_eq!(
4174 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4175 Some("jsdom")
4176 );
4177 }
4178
4179 #[test]
4182 fn inline_object_at_argument_zero_still_wins() {
4183 let source = r#"
4184 const unrelated = { pageExtensions: ["wrong.tsx"] };
4185 module.exports = withSentryConfig({ pageExtensions: ["right.tsx"] }, { org: "o" });
4186 "#;
4187 assert_eq!(
4188 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
4189 vec!["right.tsx"]
4190 );
4191 }
4192
4193 #[test]
4197 fn conditional_config_callback_extracts_branch_return() {
4198 let source = r#"
4199 import { defineConfig } from 'vite';
4200 export default defineConfig(({ command }) => {
4201 if (command === "serve") {
4202 return { test: { environment: "jsdom" } };
4203 } else {
4204 return { test: { environment: "node" } };
4205 }
4206 });
4207 "#;
4208 assert_eq!(
4209 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4210 Some("jsdom"),
4211 "with no top-level return, the first branch in source order wins"
4212 );
4213 }
4214
4215 #[test]
4216 fn try_block_return_is_extracted() {
4217 let source = r#"
4218 export default (() => {
4219 try {
4220 return { test: { environment: "jsdom" } };
4221 } catch (e) {
4222 return { test: { environment: "node" } };
4223 }
4224 });
4225 "#;
4226 assert_eq!(
4227 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4228 Some("jsdom")
4229 );
4230 }
4231
4232 #[test]
4233 fn switch_case_return_is_extracted() {
4234 let source = r#"
4235 import { defineConfig } from 'vite';
4236 export default defineConfig(({ mode }) => {
4237 switch (mode) {
4238 case "test":
4239 return { test: { environment: "jsdom" } };
4240 default:
4241 return { test: { environment: "node" } };
4242 }
4243 });
4244 "#;
4245 assert_eq!(
4246 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4247 Some("jsdom")
4248 );
4249 }
4250
4251 #[test]
4255 fn guard_clause_does_not_shadow_the_top_level_return() {
4256 let source = r#"
4257 import { defineConfig } from 'vite';
4258 export default defineConfig(({ mode }) => {
4259 if (!mode) {
4260 return {};
4261 }
4262 return { test: { environment: "jsdom" } };
4263 });
4264 "#;
4265 assert_eq!(
4266 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4267 Some("jsdom"),
4268 "a return at the callback's own level is the main config"
4269 );
4270 }
4271
4272 #[test]
4275 fn else_if_chain_does_not_shadow_the_top_level_return() {
4276 let source = r#"
4277 import { defineConfig } from 'vite';
4278 export default defineConfig(({ mode }) => {
4279 if (mode === "a") {
4280 return { base: "/a/" };
4281 } else if (mode === "b") {
4282 return { base: "/b/" };
4283 }
4284 return { test: { environment: "jsdom" } };
4285 });
4286 "#;
4287 assert_eq!(
4288 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4289 Some("jsdom")
4290 );
4291 }
4292
4293 #[test]
4294 fn extract_config_from_default_export_function_declaration() {
4295 let source = r#"
4296 export default function createConfig() {
4297 return {
4298 clientModules: ["./src/client/global.js"]
4299 };
4300 }
4301 "#;
4302
4303 let client_modules = extract_config_string_array(source, &ts_path(), &["clientModules"]);
4304 assert_eq!(client_modules, vec!["./src/client/global.js"]);
4305 }
4306
4307 #[test]
4308 fn extract_config_from_default_export_async_function_declaration() {
4309 let source = r#"
4310 export default async function createConfigAsync() {
4311 return {
4312 docs: {
4313 path: "knowledge"
4314 }
4315 };
4316 }
4317 "#;
4318
4319 let docs_path = extract_config_string(source, &ts_path(), &["docs", "path"]);
4320 assert_eq!(docs_path, Some("knowledge".to_string()));
4321 }
4322
4323 #[test]
4324 fn extract_config_from_exported_arrow_function_identifier() {
4325 let source = r#"
4326 const config = async () => {
4327 return {
4328 themes: ["classic"]
4329 };
4330 };
4331
4332 export default config;
4333 "#;
4334
4335 let themes = extract_config_shallow_strings(source, &ts_path(), "themes");
4336 assert_eq!(themes, vec!["classic"]);
4337 }
4338
4339 #[test]
4340 fn module_exports_nested_string() {
4341 let source = r#"
4342 module.exports = {
4343 resolve: {
4344 alias: {
4345 "@": "./src"
4346 }
4347 }
4348 };
4349 "#;
4350 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
4351 assert_eq!(val, Some("./src".to_string()));
4352 }
4353
4354 #[test]
4355 fn property_strings_nested_objects() {
4356 let source = r#"
4357 export default {
4358 plugins: {
4359 group1: { a: "val-a" },
4360 group2: { b: "val-b" }
4361 }
4362 };
4363 "#;
4364 let values = extract_config_property_strings(source, &js_path(), "plugins");
4365 assert!(values.contains(&"val-a".to_string()));
4366 assert!(values.contains(&"val-b".to_string()));
4367 }
4368
4369 #[test]
4370 fn property_strings_missing_key_returns_empty() {
4371 let source = r#"export default { other: "value" };"#;
4372 let values = extract_config_property_strings(source, &js_path(), "missing");
4373 assert!(values.is_empty());
4374 }
4375
4376 #[test]
4377 fn shallow_strings_tuple_array() {
4378 let source = r#"
4379 module.exports = {
4380 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
4381 };
4382 "#;
4383 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
4384 assert_eq!(values, vec!["default", "jest-junit"]);
4385 assert!(!values.contains(&"reports".to_string()));
4386 }
4387
4388 #[test]
4389 fn shallow_strings_single_string() {
4390 let source = r#"export default { preset: "ts-jest" };"#;
4391 let values = extract_config_shallow_strings(source, &js_path(), "preset");
4392 assert_eq!(values, vec!["ts-jest"]);
4393 }
4394
4395 #[test]
4396 fn shallow_strings_missing_key() {
4397 let source = r#"export default { other: "val" };"#;
4398 let values = extract_config_shallow_strings(source, &js_path(), "missing");
4399 assert!(values.is_empty());
4400 }
4401
4402 #[test]
4403 fn shallow_strings_or_object_property_alias_objects() {
4404 let source = r#"
4405 export default {
4406 jsPlugins: [
4407 "eslint-plugin-playwright",
4408 ["eslint-plugin-regexp", { rules: {} }],
4409 { name: "short", specifier: "eslint-plugin-with-long-name" }
4410 ]
4411 };
4412 "#;
4413 let values = extract_config_shallow_strings_or_object_property(
4414 source,
4415 &ts_path(),
4416 "jsPlugins",
4417 "specifier",
4418 );
4419 assert_eq!(
4420 values,
4421 vec![
4422 "eslint-plugin-playwright",
4423 "eslint-plugin-regexp",
4424 "eslint-plugin-with-long-name"
4425 ]
4426 );
4427 }
4428
4429 #[test]
4430 fn nested_shallow_strings_vitest_reporters() {
4431 let source = r#"
4432 export default {
4433 test: {
4434 reporters: ["default", "vitest-sonar-reporter"]
4435 }
4436 };
4437 "#;
4438 let values =
4439 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4440 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
4441 }
4442
4443 #[test]
4444 fn nested_shallow_strings_tuple_format() {
4445 let source = r#"
4446 export default {
4447 test: {
4448 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
4449 }
4450 };
4451 "#;
4452 let values =
4453 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4454 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
4455 }
4456
4457 #[test]
4458 fn nested_shallow_strings_missing_outer() {
4459 let source = r"export default { other: {} };";
4460 let values =
4461 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4462 assert!(values.is_empty());
4463 }
4464
4465 #[test]
4466 fn nested_shallow_strings_missing_inner() {
4467 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
4468 let values =
4469 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4470 assert!(values.is_empty());
4471 }
4472
4473 #[test]
4474 fn string_or_array_missing_path() {
4475 let source = r"export default {};";
4476 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
4477 assert!(result.is_empty());
4478 }
4479
4480 #[test]
4481 fn string_or_array_non_string_values() {
4482 let source = r"export default { entry: [42, true] };";
4483 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
4484 assert!(result.is_empty());
4485 }
4486
4487 #[test]
4488 fn array_nested_extraction() {
4489 let source = r#"
4490 export default defineConfig({
4491 test: {
4492 projects: [
4493 {
4494 test: {
4495 setupFiles: ["./test/setup-a.ts"]
4496 }
4497 },
4498 {
4499 test: {
4500 setupFiles: "./test/setup-b.ts"
4501 }
4502 }
4503 ]
4504 }
4505 });
4506 "#;
4507 let results = extract_config_array_nested_string_or_array(
4508 source,
4509 &ts_path(),
4510 &["test", "projects"],
4511 &["test", "setupFiles"],
4512 );
4513 assert!(results.contains(&"./test/setup-a.ts".to_string()));
4514 assert!(results.contains(&"./test/setup-b.ts".to_string()));
4515 }
4516
4517 #[test]
4518 fn array_nested_empty_when_no_array() {
4519 let source = r#"export default { test: { projects: "not-an-array" } };"#;
4520 let results = extract_config_array_nested_string_or_array(
4521 source,
4522 &js_path(),
4523 &["test", "projects"],
4524 &["test", "setupFiles"],
4525 );
4526 assert!(results.is_empty());
4527 }
4528
4529 #[test]
4530 fn object_nested_extraction() {
4531 let source = r#"{
4532 "projects": {
4533 "app-one": {
4534 "architect": {
4535 "build": {
4536 "options": {
4537 "styles": ["src/styles.css"]
4538 }
4539 }
4540 }
4541 }
4542 }
4543 }"#;
4544 let results = extract_config_object_nested_string_or_array(
4545 source,
4546 &json_path(),
4547 &["projects"],
4548 &["architect", "build", "options", "styles"],
4549 );
4550 assert_eq!(results, vec!["src/styles.css"]);
4551 }
4552
4553 #[test]
4554 fn array_with_object_input_form_extracted() {
4555 let source = r#"{
4556 "projects": {
4557 "app": {
4558 "architect": {
4559 "build": {
4560 "options": {
4561 "styles": [
4562 "src/styles.scss",
4563 { "input": "src/theme.scss", "bundleName": "theme", "inject": false },
4564 { "bundleName": "lazy-only" }
4565 ]
4566 }
4567 }
4568 }
4569 }
4570 }
4571 }"#;
4572 let results = extract_config_object_nested_string_or_array(
4573 source,
4574 &json_path(),
4575 &["projects"],
4576 &["architect", "build", "options", "styles"],
4577 );
4578 assert!(
4579 results.contains(&"src/styles.scss".to_string()),
4580 "string form must still work: {results:?}"
4581 );
4582 assert!(
4583 results.contains(&"src/theme.scss".to_string()),
4584 "object form with `input` must be extracted: {results:?}"
4585 );
4586 assert!(
4587 !results.contains(&"lazy-only".to_string()),
4588 "bundleName must not be misinterpreted as a path: {results:?}"
4589 );
4590 assert!(
4591 !results.contains(&"theme".to_string()),
4592 "bundleName from full object must not leak: {results:?}"
4593 );
4594 }
4595
4596 #[test]
4597 fn object_nested_strings_extraction() {
4598 let source = r#"{
4599 "targets": {
4600 "build": {
4601 "executor": "@angular/build:application"
4602 },
4603 "test": {
4604 "executor": "@nx/vite:test"
4605 }
4606 }
4607 }"#;
4608 let results =
4609 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
4610 assert!(results.contains(&"@angular/build:application".to_string()));
4611 assert!(results.contains(&"@nx/vite:test".to_string()));
4612 }
4613
4614 #[test]
4615 fn require_strings_direct_call() {
4616 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
4617 let deps = extract_config_require_strings(source, &js_path(), "adapter");
4618 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
4619 }
4620
4621 #[test]
4622 fn require_strings_no_matching_key() {
4623 let source = r"module.exports = { other: require('something') };";
4624 let deps = extract_config_require_strings(source, &js_path(), "plugins");
4625 assert!(deps.is_empty());
4626 }
4627
4628 #[test]
4629 fn extract_imports_no_imports() {
4630 let source = r"export default {};";
4631 let imports = extract_imports(source, &js_path());
4632 assert!(imports.is_empty());
4633 }
4634
4635 #[test]
4636 fn extract_imports_side_effect_import() {
4637 let source = r"
4638 import 'polyfill';
4639 import './local-setup';
4640 export default {};
4641 ";
4642 let imports = extract_imports(source, &js_path());
4643 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
4644 }
4645
4646 #[test]
4647 fn extract_imports_mixed_specifiers() {
4648 let source = r"
4649 import defaultExport from 'module-a';
4650 import { named } from 'module-b';
4651 import * as ns from 'module-c';
4652 export default {};
4653 ";
4654 let imports = extract_imports(source, &js_path());
4655 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
4656 }
4657
4658 #[test]
4659 fn template_literal_in_string_or_array() {
4660 let source = r"export default { entry: `./src/index.ts` };";
4661 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
4662 assert_eq!(result, vec!["./src/index.ts"]);
4663 }
4664
4665 #[test]
4666 fn template_literal_in_config_string() {
4667 let source = r"export default { testDir: `./tests` };";
4668 let val = extract_config_string(source, &js_path(), &["testDir"]);
4669 assert_eq!(val, Some("./tests".to_string()));
4670 }
4671
4672 #[test]
4673 fn template_literal_command_recovers_static_command_tokens() {
4674 let source = r"
4675 const PORT = 3000;
4676 export default {
4677 webServer: {
4678 command: `pnpm exec srvx --port ${PORT} --hostname 127.0.0.1`
4679 }
4680 };
4681 ";
4682 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
4683 assert_eq!(
4684 val,
4685 Some("pnpm exec srvx --port --hostname 127.0.0.1".to_string())
4686 );
4687 }
4688
4689 #[test]
4690 fn template_literal_command_skips_dynamic_prefix() {
4691 let source = r"
4692 export default {
4693 webServer: { command: `${serverCommand} && pnpm exec srvx` }
4694 };
4695 ";
4696 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
4697 assert!(val.is_none());
4698 }
4699
4700 #[test]
4701 fn template_literal_command_skips_split_static_token() {
4702 let source = r"
4703 export default {
4704 webServer: { command: `pnpm exec sr${part}vx --port 3000` }
4705 };
4706 ";
4707 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
4708 assert!(val.is_none());
4709 }
4710
4711 #[test]
4712 fn array_object_command_pairs_recover_template_command() {
4713 let source = r"
4714 const PORT = 3000;
4715 export default {
4716 webServer: [
4717 {
4718 command: `pnpm exec srvx --port ${PORT}`,
4719 cwd: 'apps/web'
4720 }
4721 ]
4722 };
4723 ";
4724 let pairs = extract_config_array_object_command_pairs(
4725 source,
4726 &ts_path(),
4727 &["webServer"],
4728 "command",
4729 "cwd",
4730 );
4731 assert_eq!(
4732 pairs,
4733 vec![(
4734 "pnpm exec srvx --port ".to_string(),
4735 Some("apps/web".to_string())
4736 )]
4737 );
4738 }
4739
4740 #[test]
4741 fn nested_string_array_empty_path() {
4742 let source = r#"export default { items: ["a", "b"] };"#;
4743 let result = extract_config_string_array(source, &js_path(), &[]);
4744 assert!(result.is_empty());
4745 }
4746
4747 #[test]
4748 fn nested_string_empty_path() {
4749 let source = r#"export default { key: "val" };"#;
4750 let result = extract_config_string(source, &js_path(), &[]);
4751 assert!(result.is_none());
4752 }
4753
4754 #[test]
4755 fn object_keys_empty_path() {
4756 let source = r"export default { plugins: {} };";
4757 let result = extract_config_object_keys(source, &js_path(), &[]);
4758 assert!(result.is_empty());
4759 }
4760
4761 #[test]
4762 fn no_config_object_returns_empty() {
4763 let source = r"const x = 42;";
4764 let result = extract_config_string(source, &js_path(), &["key"]);
4765 assert!(result.is_none());
4766
4767 let arr = extract_config_string_array(source, &js_path(), &["items"]);
4768 assert!(arr.is_empty());
4769
4770 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
4771 assert!(keys.is_empty());
4772 }
4773
4774 #[test]
4775 fn property_with_string_key() {
4776 let source = r#"export default { "string-key": "value" };"#;
4777 let val = extract_config_string(source, &js_path(), &["string-key"]);
4778 assert_eq!(val, Some("value".to_string()));
4779 }
4780
4781 #[test]
4782 fn nested_navigation_through_non_object() {
4783 let source = r#"export default { level1: "not-an-object" };"#;
4784 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
4785 assert!(val.is_none());
4786 }
4787
4788 #[test]
4789 fn variable_reference_untyped() {
4790 let source = r#"
4791 const config = {
4792 testDir: "./tests"
4793 };
4794 export default config;
4795 "#;
4796 let val = extract_config_string(source, &js_path(), &["testDir"]);
4797 assert_eq!(val, Some("./tests".to_string()));
4798 }
4799
4800 #[test]
4801 fn variable_reference_to_an_exported_const() {
4802 for source in [
4803 r#"
4804 export const config = { testDir: "./tests" };
4805 export default config;
4806 "#,
4807 r#"
4808 export const config = { testDir: "./tests" };
4809 module.exports = config;
4810 "#,
4811 ] {
4812 let val = extract_config_string(source, &js_path(), &["testDir"]);
4813 assert_eq!(val, Some("./tests".to_string()), "source: {source}");
4814 }
4815 }
4816
4817 #[test]
4818 fn variable_reference_with_type_annotation() {
4819 let source = r#"
4820 import type { StorybookConfig } from '@storybook/react-vite';
4821 const config: StorybookConfig = {
4822 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
4823 framework: "@storybook/react-vite"
4824 };
4825 export default config;
4826 "#;
4827 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
4828 assert_eq!(
4829 addons,
4830 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
4831 );
4832
4833 let framework = extract_config_string(source, &ts_path(), &["framework"]);
4834 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
4835 }
4836
4837 #[test]
4838 fn variable_reference_with_define_config() {
4839 let source = r#"
4840 import { defineConfig } from 'vitest/config';
4841 const config = defineConfig({
4842 test: {
4843 include: ["**/*.test.ts"]
4844 }
4845 });
4846 export default config;
4847 "#;
4848 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
4849 assert_eq!(include, vec!["**/*.test.ts"]);
4850 }
4851
4852 #[test]
4853 fn ts_satisfies_direct_export() {
4854 let source = r#"
4855 export default {
4856 testDir: "./tests"
4857 } satisfies PlaywrightTestConfig;
4858 "#;
4859 let val = extract_config_string(source, &ts_path(), &["testDir"]);
4860 assert_eq!(val, Some("./tests".to_string()));
4861 }
4862
4863 #[test]
4864 fn ts_as_direct_export() {
4865 let source = r#"
4866 export default {
4867 testDir: "./tests"
4868 } as const;
4869 "#;
4870 let val = extract_config_string(source, &ts_path(), &["testDir"]);
4871 assert_eq!(val, Some("./tests".to_string()));
4872 }
4873
4874 fn aliases(source: &str) -> Vec<(String, String)> {
4877 extract_config_aliases(source, &js_path(), &["resolve", "alias"])
4878 }
4879
4880 #[test]
4881 fn aliases_inline_object_still_extracted() {
4882 let source = r#"
4884 export default defineConfig({
4885 resolve: { alias: { "@": "./src", utils: "../../utils" } }
4886 });
4887 "#;
4888 let mut got = aliases(source);
4889 got.sort();
4890 assert_eq!(
4891 got,
4892 vec![
4893 ("@".to_string(), "./src".to_string()),
4894 ("utils".to_string(), "../../utils".to_string()),
4895 ]
4896 );
4897 }
4898
4899 #[test]
4900 fn aliases_inline_array_still_extracted() {
4901 let source = r#"
4902 export default defineConfig({
4903 resolve: { alias: [{ find: "@", replacement: "./src" }] }
4904 });
4905 "#;
4906 assert_eq!(
4907 aliases(source),
4908 vec![("@".to_string(), "./src".to_string())]
4909 );
4910 }
4911
4912 #[test]
4913 fn aliases_local_const_array_identifier() {
4914 let source = r#"
4915 const sharedAliases = [{ find: "@", replacement: "./src" }];
4916 export default defineConfig({ resolve: { alias: sharedAliases } });
4917 "#;
4918 assert_eq!(
4919 aliases(source),
4920 vec![("@".to_string(), "./src".to_string())]
4921 );
4922 }
4923
4924 #[test]
4925 fn aliases_local_const_object_identifier() {
4926 let source = r#"
4927 const sharedAliases = { "@": "./src" };
4928 export default defineConfig({ resolve: { alias: sharedAliases } });
4929 "#;
4930 assert_eq!(
4931 aliases(source),
4932 vec![("@".to_string(), "./src".to_string())]
4933 );
4934 }
4935
4936 #[test]
4937 fn aliases_array_spread_of_identifiers_and_inline() {
4938 let source = r##"
4939 const a = [{ find: "@", replacement: "./src" }];
4940 const b = [{ find: "~", replacement: "./lib" }];
4941 export default defineConfig({
4942 resolve: { alias: [...a, ...b, { find: "#", replacement: "./test" }] }
4943 });
4944 "##;
4945 let mut got = aliases(source);
4946 got.sort();
4947 assert_eq!(
4948 got,
4949 vec![
4950 ("#".to_string(), "./test".to_string()),
4951 ("@".to_string(), "./src".to_string()),
4952 ("~".to_string(), "./lib".to_string()),
4953 ]
4954 );
4955 }
4956
4957 #[test]
4958 fn aliases_object_spread_of_identifier_and_inline() {
4959 let source = r#"
4960 const base = { "@": "./src" };
4961 export default defineConfig({
4962 resolve: { alias: { ...base, "~": "./lib" } }
4963 });
4964 "#;
4965 let mut got = aliases(source);
4966 got.sort();
4967 assert_eq!(
4968 got,
4969 vec![
4970 ("@".to_string(), "./src".to_string()),
4971 ("~".to_string(), "./lib".to_string()),
4972 ]
4973 );
4974 }
4975
4976 #[test]
4977 fn aliases_local_const_chained_identifier() {
4978 let source = r#"
4980 const real = [{ find: "@", replacement: "./src" }];
4981 const alias2 = real;
4982 export default defineConfig({ resolve: { alias: alias2 } });
4983 "#;
4984 assert_eq!(
4985 aliases(source),
4986 vec![("@".to_string(), "./src".to_string())]
4987 );
4988 }
4989
4990 #[test]
4991 fn aliases_imported_named_identifier_from_sibling() {
4992 let dir = tempfile::tempdir().unwrap();
4993 std::fs::write(
4994 dir.path().join("vite.shared.js"),
4995 r#"export const sharedAliases = [
4996 { find: "@", replacement: new URL("./src", import.meta.url).pathname },
4997 ];"#,
4998 )
4999 .unwrap();
5000 let config = dir.path().join("vite.config.js");
5001 let source = r#"
5002 import { defineConfig } from "vite";
5003 import { sharedAliases } from "./vite.shared.js";
5004 export default defineConfig({ resolve: { alias: sharedAliases } });
5005 "#;
5006 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
5007 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
5008 }
5009
5010 #[test]
5011 fn aliases_imported_extensionless_specifier_probed() {
5012 let dir = tempfile::tempdir().unwrap();
5013 std::fs::write(
5014 dir.path().join("aliases.mjs"),
5015 r#"export const sharedAliases = { "@": "./src" };"#,
5016 )
5017 .unwrap();
5018 let config = dir.path().join("vite.config.ts");
5019 let source = r#"
5020 import { sharedAliases } from "./aliases";
5021 export default defineConfig({ resolve: { alias: sharedAliases } });
5022 "#;
5023 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
5024 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
5025 }
5026
5027 #[test]
5028 fn aliases_imported_default_export_from_sibling() {
5029 let dir = tempfile::tempdir().unwrap();
5030 std::fs::write(
5031 dir.path().join("aliases.js"),
5032 r#"export default [{ find: "@", replacement: "./src" }];"#,
5033 )
5034 .unwrap();
5035 let config = dir.path().join("vite.config.js");
5036 let source = r#"
5037 import sharedAliases from "./aliases.js";
5038 export default defineConfig({ resolve: { alias: sharedAliases } });
5039 "#;
5040 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
5041 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
5042 }
5043
5044 #[test]
5045 fn aliases_imported_spread_from_two_siblings() {
5046 let dir = tempfile::tempdir().unwrap();
5047 std::fs::write(
5048 dir.path().join("a.js"),
5049 r#"export const a = [{ find: "@", replacement: "./src" }];"#,
5050 )
5051 .unwrap();
5052 std::fs::write(
5053 dir.path().join("b.js"),
5054 r#"export const b = [{ find: "~", replacement: "./lib" }];"#,
5055 )
5056 .unwrap();
5057 let config = dir.path().join("vite.config.js");
5058 let source = r#"
5059 import { a } from "./a.js";
5060 import { b } from "./b.js";
5061 export default defineConfig({ resolve: { alias: [...a, ...b] } });
5062 "#;
5063 let mut got = extract_config_aliases(source, &config, &["resolve", "alias"]);
5064 got.sort();
5065 assert_eq!(
5066 got,
5067 vec![
5068 ("@".to_string(), "./src".to_string()),
5069 ("~".to_string(), "./lib".to_string()),
5070 ]
5071 );
5072 }
5073
5074 #[test]
5075 fn aliases_import_cycle_terminates() {
5076 let dir = tempfile::tempdir().unwrap();
5079 std::fs::write(
5080 dir.path().join("a.js"),
5081 r#"import { b } from "./b.js";
5082 export const a = [{ find: "@", replacement: "./src" }, ...b];"#,
5083 )
5084 .unwrap();
5085 std::fs::write(
5086 dir.path().join("b.js"),
5087 r#"import { a } from "./a.js";
5088 export const b = [...a];"#,
5089 )
5090 .unwrap();
5091 let config = dir.path().join("vite.config.js");
5092 let source = r#"
5093 import { a } from "./a.js";
5094 export default defineConfig({ resolve: { alias: a } });
5095 "#;
5096 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
5097 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
5098 }
5099
5100 #[test]
5101 fn aliases_non_relative_import_not_followed() {
5102 let source = r#"
5105 import { sharedAliases } from "some-pkg";
5106 export default defineConfig({ resolve: { alias: sharedAliases } });
5107 "#;
5108 let dir = tempfile::tempdir().unwrap();
5109 let config = dir.path().join("vite.config.js");
5110 assert!(extract_config_aliases(source, &config, &["resolve", "alias"]).is_empty());
5111 }
5112
5113 #[test]
5114 fn aliases_object_array_value_takes_first_entry() {
5115 let source = r#"
5120 export default {
5121 compilerOptions: { paths: { "@/*": ["./src/*"], "~/*": ["./lib/*", "./vendor/*"] } }
5122 };
5123 "#;
5124 let mut got = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
5125 got.sort();
5126 assert_eq!(
5127 got,
5128 vec![
5129 ("@/*".to_string(), "./src/*".to_string()),
5130 ("~/*".to_string(), "./lib/*".to_string()),
5131 ]
5132 );
5133 }
5134
5135 #[test]
5136 fn aliases_kinded_preserves_is_bare_through_resolution() {
5137 let source = r#"
5140 const a = [{ find: "lodash-es", replacement: "lodash" }];
5141 export default defineConfig({
5142 resolve: { alias: [...a, { find: "@", replacement: "./src" }] }
5143 });
5144 "#;
5145 let mut got = extract_config_aliases_kinded(source, &js_path(), &["resolve", "alias"]);
5146 got.sort();
5147 assert_eq!(
5148 got,
5149 vec![
5150 ("@".to_string(), "./src".to_string(), false),
5151 ("lodash-es".to_string(), "lodash".to_string(), true),
5152 ]
5153 );
5154 }
5155
5156 #[test]
5157 fn aliases_kinded_preserves_is_bare_through_imported_spread() {
5158 let dir = tempfile::tempdir().unwrap();
5159 std::fs::write(
5160 dir.path().join("aliases.js"),
5161 r#"export const packageAliases = [{ find: "lodash-es", replacement: "lodash" }];"#,
5162 )
5163 .unwrap();
5164 let config = dir.path().join("vite.config.js");
5165 let source = r#"
5166 import { packageAliases } from "./aliases.js";
5167 export default defineConfig({
5168 resolve: { alias: [...packageAliases, { find: "@", replacement: "./src" }] }
5169 });
5170 "#;
5171 let mut got = extract_config_aliases_kinded(source, &config, &["resolve", "alias"]);
5172 got.sort();
5173 assert_eq!(
5174 got,
5175 vec![
5176 ("@".to_string(), "./src".to_string(), false),
5177 ("lodash-es".to_string(), "lodash".to_string(), true),
5178 ]
5179 );
5180 }
5181
5182 #[test]
5185 fn extract_command_string_literal() {
5186 let source = r#"export default { start: "node server.js" };"#;
5187 let val = extract_config_command(source, &js_path(), &["start"]);
5188 assert_eq!(val, Some("node server.js".to_string()));
5189 }
5190
5191 #[test]
5192 fn extract_command_nested_path() {
5193 let source = r#"
5194 export default {
5195 scripts: {
5196 dev: "vite dev"
5197 }
5198 };
5199 "#;
5200 let val = extract_config_command(source, &js_path(), &["scripts", "dev"]);
5201 assert_eq!(val, Some("vite dev".to_string()));
5202 }
5203
5204 #[test]
5205 fn extract_command_missing_key_returns_none() {
5206 let source = r#"export default { other: "val" };"#;
5207 let val = extract_config_command(source, &js_path(), &["start"]);
5208 assert!(val.is_none());
5209 }
5210
5211 #[test]
5212 fn extract_command_ts_as_expression() {
5213 let source = r#"export default { start: "node server.js" as string };"#;
5214 let val = extract_config_command(source, &ts_path(), &["start"]);
5215 assert_eq!(val, Some("node server.js".to_string()));
5216 }
5217
5218 #[test]
5219 fn extract_command_ts_satisfies_expression() {
5220 let source = r#"export default { start: "node server.js" satisfies string };"#;
5221 let val = extract_config_command(source, &ts_path(), &["start"]);
5222 assert_eq!(val, Some("node server.js".to_string()));
5223 }
5224
5225 #[test]
5226 fn extract_command_parenthesized_expression() {
5227 let source = r#"export default { start: ("node server.js") };"#;
5228 let val = extract_config_command(source, &js_path(), &["start"]);
5229 assert_eq!(val, Some("node server.js".to_string()));
5230 }
5231
5232 #[test]
5233 fn extract_command_empty_path_returns_none() {
5234 let source = r#"export default { start: "node server.js" };"#;
5235 let val = extract_config_command(source, &js_path(), &[]);
5236 assert!(val.is_none());
5237 }
5238
5239 #[test]
5242 fn truthy_bool_or_object_with_true_value() {
5243 let source = r"export default { typescript: true };";
5244 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5245 assert!(result);
5246 }
5247
5248 #[test]
5249 fn truthy_bool_or_object_with_false_value() {
5250 let source = r"export default { typescript: false };";
5251 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5252 assert!(!result);
5253 }
5254
5255 #[test]
5256 fn truthy_bool_or_object_with_object_value() {
5257 let source = r#"export default { typescript: { reactDocgen: "react-docgen" } };"#;
5258 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5259 assert!(result);
5260 }
5261
5262 #[test]
5263 fn truthy_bool_or_object_missing_key_returns_false() {
5264 let source = r"export default { other: true };";
5265 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5266 assert!(!result);
5267 }
5268
5269 #[test]
5270 fn truthy_bool_or_object_with_string_value_returns_false() {
5271 let source = r#"export default { typescript: "yes" };"#;
5273 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5274 assert!(!result);
5275 }
5276
5277 #[test]
5278 fn truthy_bool_or_object_ts_satisfies_wrapper() {
5279 let source = r"export default { typescript: (true satisfies boolean) };";
5280 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5281 assert!(result);
5282 }
5283
5284 #[test]
5285 fn truthy_bool_or_object_ts_as_wrapper() {
5286 let source = r"export default { typescript: (true as boolean) };";
5287 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5288 assert!(result);
5289 }
5290
5291 #[test]
5292 fn truthy_bool_or_object_parenthesized_wrapper() {
5293 let source = r"export default { typescript: (true) };";
5294 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5295 assert!(result);
5296 }
5297
5298 #[test]
5304 fn static_dir_entries_object_form_exercises_property_string() {
5305 let source = r#"
5308 export default {
5309 staticDirs: [
5310 { from: "./media", to: "/assets" }
5311 ]
5312 };
5313 "#;
5314 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5315 assert_eq!(
5316 entries,
5317 vec![("./media".to_string(), Some("/assets".to_string()))]
5318 );
5319 }
5320
5321 #[test]
5324 fn expression_to_path_values_array_form_via_config_path() {
5325 let source = r#"export default { entries: ["./src/a.ts", "./src/b.ts"] };"#;
5328 let result = extract_config_string_or_array(source, &js_path(), &["entries"]);
5329 assert_eq!(result, vec!["./src/a.ts", "./src/b.ts"]);
5330 }
5331
5332 #[test]
5335 fn array_nested_aliases_object_form() {
5336 let source = r#"
5337 export default {
5338 test: {
5339 projects: [
5340 {
5341 resolve: {
5342 alias: { "@": "./src" }
5343 }
5344 }
5345 ]
5346 }
5347 };
5348 "#;
5349 let aliases = extract_config_array_nested_aliases_kinded(
5350 source,
5351 &ts_path(),
5352 &["test", "projects"],
5353 &["resolve", "alias"],
5354 );
5355 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
5356 }
5357
5358 #[test]
5359 fn array_nested_aliases_empty_when_path_is_not_array() {
5360 let source = r#"export default { test: { projects: "not-an-array" } };"#;
5361 let aliases = extract_config_array_nested_aliases_kinded(
5362 source,
5363 &ts_path(),
5364 &["test", "projects"],
5365 &["resolve", "alias"],
5366 );
5367 assert!(aliases.is_empty());
5368 }
5369
5370 #[test]
5371 fn array_nested_aliases_kinded_tracks_is_bare() {
5372 let source = r#"
5373 export default {
5374 projects: [
5375 {
5376 resolve: {
5377 alias: [
5378 { find: "lodash-es", replacement: "lodash" },
5379 { find: "@", replacement: "./src" }
5380 ]
5381 }
5382 }
5383 ]
5384 };
5385 "#;
5386 let mut aliases = extract_config_array_nested_aliases_kinded(
5387 source,
5388 &ts_path(),
5389 &["projects"],
5390 &["resolve", "alias"],
5391 );
5392 aliases.sort();
5393 assert_eq!(
5394 aliases,
5395 vec![
5396 ("@".to_string(), "./src".to_string(), false),
5397 ("lodash-es".to_string(), "lodash".to_string(), true),
5398 ]
5399 );
5400 }
5401
5402 #[test]
5405 fn default_export_array_aliases_kinded_extracts_from_workspace_config() {
5406 let source = r#"
5407 export default [
5408 {
5409 resolve: {
5410 alias: { "@": "./src" }
5411 }
5412 },
5413 {
5414 resolve: {
5415 alias: [{ find: "~", replacement: "./lib" }]
5416 }
5417 }
5418 ];
5419 "#;
5420 let mut aliases =
5421 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5422 aliases.sort();
5423 assert_eq!(
5424 aliases,
5425 vec![
5426 ("@".to_string(), "./src".to_string(), false),
5427 ("~".to_string(), "./lib".to_string(), false),
5428 ]
5429 );
5430 }
5431
5432 #[test]
5433 fn default_export_array_aliases_kinded_define_workspace_wrapper() {
5434 let source = r#"
5435 export default defineWorkspace([
5436 {
5437 resolve: { alias: { "@": "./src" } }
5438 }
5439 ]);
5440 "#;
5441 let aliases =
5442 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5443 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
5444 }
5445
5446 #[test]
5447 fn default_export_array_aliases_kinded_empty_when_no_alias_path() {
5448 let source = r#"
5449 export default [
5450 { test: { include: ["**/*.test.ts"] } }
5451 ];
5452 "#;
5453 let aliases =
5454 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5455 assert!(aliases.is_empty());
5456 }
5457
5458 #[test]
5461 fn config_default_export_unreachable_when_no_export() {
5462 let source = r"const x = 42;";
5463 assert!(config_default_export_unreachable(source, &js_path()));
5464 }
5465
5466 #[test]
5467 fn config_default_export_unreachable_false_for_object_export() {
5468 let source = r#"export default { key: "value" };"#;
5469 assert!(!config_default_export_unreachable(source, &js_path()));
5470 }
5471
5472 #[test]
5473 fn config_default_export_unreachable_false_for_array_export() {
5474 let source = r#"export default ["a", "b"];"#;
5475 assert!(!config_default_export_unreachable(source, &js_path()));
5476 }
5477
5478 #[test]
5479 fn config_default_export_unreachable_true_for_function_without_return_object() {
5480 let source = r"export default function config() { return 42; }";
5482 assert!(config_default_export_unreachable(source, &js_path()));
5483 }
5484
5485 #[test]
5488 fn static_dir_entries_string_and_object_form() {
5489 let source = r#"
5490 export default {
5491 staticDirs: [
5492 "./public",
5493 { from: "../assets", to: "/static" }
5494 ]
5495 };
5496 "#;
5497 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5498 assert_eq!(
5499 entries,
5500 vec![
5501 ("./public".to_string(), None),
5502 ("../assets".to_string(), Some("/static".to_string())),
5503 ]
5504 );
5505 }
5506
5507 #[test]
5508 fn static_dir_entries_object_without_to() {
5509 let source = r#"
5510 export default {
5511 staticDirs: [
5512 { from: "./media" }
5513 ]
5514 };
5515 "#;
5516 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5517 assert_eq!(entries, vec![("./media".to_string(), None)]);
5518 }
5519
5520 #[test]
5521 fn static_dir_entries_object_missing_from_skipped() {
5522 let source = r#"
5524 export default {
5525 staticDirs: [
5526 { to: "/target" },
5527 "./public"
5528 ]
5529 };
5530 "#;
5531 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5532 assert_eq!(entries, vec![("./public".to_string(), None)]);
5533 }
5534
5535 #[test]
5536 fn static_dir_entries_empty_when_not_array() {
5537 let source = r#"export default { staticDirs: "./public" };"#;
5538 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5539 assert!(entries.is_empty());
5540 }
5541
5542 #[test]
5545 fn aliases_array_form_missing_find_or_replacement_skipped() {
5546 let source = r#"
5548 export default {
5549 resolve: {
5550 alias: [
5551 { replacement: "./src" },
5552 { find: "@" },
5553 { find: "~", replacement: "./lib" }
5554 ]
5555 }
5556 };
5557 "#;
5558 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5559 assert_eq!(aliases, vec![("~".to_string(), "./lib".to_string())]);
5560 }
5561
5562 #[test]
5563 fn aliases_object_form_computed_key_skipped() {
5564 let source = r#"
5566 const k = "@";
5567 export default {
5568 resolve: {
5569 alias: {
5570 [k]: "./src",
5571 "~": "./lib"
5572 }
5573 }
5574 };
5575 "#;
5576 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5577 assert_eq!(aliases, vec![("~".to_string(), "./lib".to_string())]);
5579 }
5580
5581 #[test]
5582 fn aliases_kinded_array_form_path_replacement_is_not_bare() {
5583 let source = r#"
5584 export default {
5585 resolve: {
5586 alias: [{ find: "@", replacement: "./src" }]
5587 }
5588 };
5589 "#;
5590 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5591 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
5592 }
5593
5594 #[test]
5595 fn aliases_kinded_object_form_bare_and_path_discrimination() {
5596 let source = r#"
5597 export default {
5598 resolve: {
5599 alias: {
5600 "lodash-es": "lodash",
5601 "@": "./src"
5602 }
5603 }
5604 };
5605 "#;
5606 let mut aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5607 aliases.sort();
5608 assert_eq!(
5609 aliases,
5610 vec![
5611 ("@".to_string(), "./src".to_string(), false),
5612 ("lodash-es".to_string(), "lodash".to_string(), true),
5613 ]
5614 );
5615 }
5616
5617 #[test]
5618 fn aliases_kinded_parent_relative_replacement_is_not_bare() {
5619 let source = r#"
5620 export default {
5621 resolve: { alias: { "@": "../shared/src" } }
5622 };
5623 "#;
5624 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5625 assert_eq!(
5626 aliases,
5627 vec![("@".to_string(), "../shared/src".to_string(), false)]
5628 );
5629 }
5630
5631 #[test]
5632 fn aliases_kinded_absolute_replacement_is_not_bare() {
5633 let source = r#"
5634 export default {
5635 resolve: { alias: { "@": "/absolute/path" } }
5636 };
5637 "#;
5638 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5639 assert_eq!(
5640 aliases,
5641 vec![("@".to_string(), "/absolute/path".to_string(), false)]
5642 );
5643 }
5644
5645 #[test]
5648 fn default_export_array_ts_as_wrapper() {
5649 let source = r"export default [] as string[];";
5651 assert!(!config_default_export_unreachable(source, &js_path()));
5652 }
5653
5654 #[test]
5655 fn default_export_array_ts_satisfies_wrapper() {
5656 let source = r"export default [] satisfies string[];";
5657 assert!(!config_default_export_unreachable(source, &ts_path()));
5658 }
5659
5660 #[test]
5661 fn default_export_array_define_config_call_wrapper() {
5662 let source = r#"export default defineConfig(["**/*.test.ts"]);"#;
5663 assert!(!config_default_export_unreachable(source, &ts_path()));
5664 }
5665
5666 #[test]
5669 fn shallow_strings_object_with_string_values() {
5670 let source = r#"
5672 export default {
5673 plugins: {
5674 autoprefixer: "autoprefixer",
5675 tailwindcss: "tailwindcss"
5676 }
5677 };
5678 "#;
5679 let vals = extract_config_shallow_strings(source, &js_path(), "plugins");
5680 assert!(vals.contains(&"autoprefixer".to_string()));
5681 assert!(vals.contains(&"tailwindcss".to_string()));
5682 }
5683
5684 #[test]
5685 fn shallow_strings_object_with_sub_array_first_element() {
5686 let source = r#"
5688 export default {
5689 reporters: {
5690 main: ["jest-junit", { outputFile: "report.xml" }],
5691 alt: ["html-reporter"]
5692 }
5693 };
5694 "#;
5695 let vals = extract_config_shallow_strings(source, &js_path(), "reporters");
5696 assert!(vals.contains(&"jest-junit".to_string()));
5697 assert!(vals.contains(&"html-reporter".to_string()));
5698 }
5699
5700 #[test]
5703 fn shallow_strings_or_object_property_non_array_single_string() {
5704 let source = r#"export default { jsPlugins: "eslint-plugin-foo" };"#;
5706 let vals = extract_config_shallow_strings_or_object_property(
5707 source,
5708 &ts_path(),
5709 "jsPlugins",
5710 "specifier",
5711 );
5712 assert_eq!(vals, vec!["eslint-plugin-foo"]);
5713 }
5714
5715 #[test]
5716 fn shallow_strings_or_object_property_ts_satisfies_array_element() {
5717 let source = r#"
5719 export default {
5720 jsPlugins: [
5721 ("eslint-plugin-a" satisfies string)
5722 ]
5723 };
5724 "#;
5725 let vals = extract_config_shallow_strings_or_object_property(
5726 source,
5727 &ts_path(),
5728 "jsPlugins",
5729 "specifier",
5730 );
5731 assert_eq!(vals, vec!["eslint-plugin-a"]);
5732 }
5733
5734 #[test]
5735 fn shallow_strings_or_object_property_ts_as_array_element() {
5736 let source = r#"
5737 export default {
5738 jsPlugins: [
5739 ("eslint-plugin-b" as string)
5740 ]
5741 };
5742 "#;
5743 let vals = extract_config_shallow_strings_or_object_property(
5744 source,
5745 &ts_path(),
5746 "jsPlugins",
5747 "specifier",
5748 );
5749 assert_eq!(vals, vec!["eslint-plugin-b"]);
5750 }
5751
5752 #[test]
5753 fn shallow_strings_or_object_property_sub_array_first_element_string() {
5754 let source = r#"
5756 export default {
5757 jsPlugins: [
5758 ["eslint-plugin-tuple-pkg", { options: true }]
5759 ]
5760 };
5761 "#;
5762 let vals = extract_config_shallow_strings_or_object_property(
5763 source,
5764 &ts_path(),
5765 "jsPlugins",
5766 "specifier",
5767 );
5768 assert_eq!(vals, vec!["eslint-plugin-tuple-pkg"]);
5769 }
5770
5771 #[test]
5774 fn array_object_command_pairs_basic() {
5775 let source = r#"
5776 export default {
5777 webServer: [
5778 { command: "node server.js", cwd: "packages/api" },
5779 { command: "vite dev" }
5780 ]
5781 };
5782 "#;
5783 let pairs = extract_config_array_object_command_pairs(
5784 source,
5785 &ts_path(),
5786 &["webServer"],
5787 "command",
5788 "cwd",
5789 );
5790 assert_eq!(
5791 pairs,
5792 vec![
5793 (
5794 "node server.js".to_string(),
5795 Some("packages/api".to_string())
5796 ),
5797 ("vite dev".to_string(), None),
5798 ]
5799 );
5800 }
5801
5802 #[test]
5803 fn array_object_command_pairs_skips_missing_command() {
5804 let source = r#"
5805 export default {
5806 webServer: [
5807 { cwd: "packages/api" },
5808 { command: "vite dev", cwd: "apps/web" }
5809 ]
5810 };
5811 "#;
5812 let pairs = extract_config_array_object_command_pairs(
5813 source,
5814 &ts_path(),
5815 &["webServer"],
5816 "command",
5817 "cwd",
5818 );
5819 assert_eq!(
5820 pairs,
5821 vec![("vite dev".to_string(), Some("apps/web".to_string()))]
5822 );
5823 }
5824
5825 #[test]
5826 fn array_object_command_pairs_empty_when_not_array() {
5827 let source = r#"export default { webServer: { command: "vite dev" } };"#;
5828 let pairs = extract_config_array_object_command_pairs(
5829 source,
5830 &ts_path(),
5831 &["webServer"],
5832 "command",
5833 "cwd",
5834 );
5835 assert!(pairs.is_empty());
5836 }
5837
5838 #[test]
5841 fn normalize_config_path_empty_string_returns_none() {
5842 let config_path = PathBuf::from("/project/vite.config.ts");
5843 let root = PathBuf::from("/project");
5844 assert_eq!(normalize_config_path("", &config_path, &root), None);
5845 }
5846
5847 #[test]
5848 fn normalize_config_path_escapes_to_above_root_returns_none() {
5849 let config_path = PathBuf::from("/project/vite.config.ts");
5850 let root = PathBuf::from("/project");
5851 assert_eq!(
5853 normalize_config_path("../../etc", &config_path, &root),
5854 None
5855 );
5856 }
5857
5858 #[test]
5859 fn normalize_config_path_dot_slash_resolves_relative_to_config_dir() {
5860 let config_path = PathBuf::from("/project/packages/app/vite.config.ts");
5861 let root = PathBuf::from("/project");
5862 assert_eq!(
5863 normalize_config_path("./src", &config_path, &root),
5864 Some("packages/app/src".to_string())
5865 );
5866 }
5867
5868 #[test]
5871 fn json_config_array_of_arrays_via_shallow_strings() {
5872 let source = r#"{"reporters": ["default", ["jest-junit", {}]]}"#;
5874 let vals = extract_config_shallow_strings(source, &json_path(), "reporters");
5875 assert_eq!(vals, vec!["default", "jest-junit"]);
5876 }
5877
5878 #[test]
5881 fn extract_config_path_string_literal() {
5882 let source = r#"export default { outDir: "./dist" };"#;
5883 let path = extract_config_path(source, &js_path(), &["outDir"]);
5884 assert_eq!(
5885 path.map(|p| p.to_string_lossy().replace('\\', "/")),
5886 Some("./dist".to_string())
5887 );
5888 }
5889
5890 #[test]
5891 fn extract_config_path_with_resolve_call() {
5892 let source = r#"
5893 import { resolve } from "node:path";
5894 export default { outDir: resolve(__dirname, "dist") };
5895 "#;
5896 let path = extract_config_path(source, &js_path(), &["outDir"]);
5897 assert_eq!(
5898 path.map(|p| p.to_string_lossy().replace('\\', "/")),
5899 Some("dist".to_string())
5900 );
5901 }
5902
5903 #[test]
5904 fn extract_config_path_missing_key_returns_none() {
5905 let source = r#"export default { other: "val" };"#;
5906 let path = extract_config_path(source, &js_path(), &["outDir"]);
5907 assert!(path.is_none());
5908 }
5909
5910 #[test]
5913 fn extract_imports_and_requires_both_forms() {
5914 let source = r"
5915 import foo from 'foo-pkg';
5916 require('bar-pkg');
5917 export default {};
5918 ";
5919 let sources = extract_imports_and_requires(source, &js_path());
5920 assert!(sources.contains(&"foo-pkg".to_string()));
5921 assert!(sources.contains(&"bar-pkg".to_string()));
5922 }
5923
5924 #[test]
5925 fn extract_imports_and_requires_skips_non_require_calls() {
5926 let source = r"
5927 import foo from 'foo-pkg';
5928 someOtherCall('bar-pkg');
5929 export default {};
5930 ";
5931 let sources = extract_imports_and_requires(source, &js_path());
5932 assert_eq!(sources, vec!["foo-pkg"]);
5933 }
5934
5935 #[test]
5938 fn nested_shallow_strings_non_object_nested_returns_empty() {
5939 let source = r#"export default { test: "not-an-object" };"#;
5941 let vals =
5942 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
5943 assert!(vals.is_empty());
5944 }
5945
5946 #[test]
5949 fn vite_react_babel_dependencies_namespace_import() {
5950 let source = r#"
5951 import * as react from "@vitejs/plugin-react";
5952
5953 export default defineConfig({
5954 plugins: [
5955 react.default({
5956 babel: {
5957 plugins: ["babel-plugin-ns"],
5958 },
5959 }),
5960 ],
5961 });
5962 "#;
5963 let deps = extract_vite_plugin_option_dependencies(source, &ts_path());
5964 assert_eq!(deps, vec!["babel-plugin-ns".to_string()]);
5965 }
5966
5967 #[test]
5970 fn property_strings_deeply_nested_object_values() {
5971 let source = r#"
5973 export default {
5974 settings: {
5975 a: "val-a",
5976 b: {
5977 c: "val-c",
5978 d: ["val-d1", "val-d2"]
5979 }
5980 }
5981 };
5982 "#;
5983 let values = extract_config_property_strings(source, &js_path(), "settings");
5984 assert!(values.contains(&"val-a".to_string()));
5985 assert!(values.contains(&"val-c".to_string()));
5986 assert!(values.contains(&"val-d1".to_string()));
5987 assert!(values.contains(&"val-d2".to_string()));
5988 }
5989
5990 #[test]
5993 fn aliases_exported_const_form_resolves() {
5994 let source = r#"
5996 export const sharedAliases = { "@": "./src" };
5997 export default defineConfig({ resolve: { alias: sharedAliases } });
5998 "#;
5999 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
6000 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
6001 }
6002
6003 #[test]
6006 fn aliases_imported_from_sibling_directory_index_file() {
6007 let dir = tempfile::tempdir().unwrap();
6010 let aliases_dir = dir.path().join("aliases");
6011 std::fs::create_dir_all(&aliases_dir).unwrap();
6012 std::fs::write(
6013 aliases_dir.join("index.js"),
6014 r#"export const aliases = [{ find: "@", replacement: "./src" }];"#,
6015 )
6016 .unwrap();
6017 let config = dir.path().join("vite.config.js");
6018 let source = r#"
6019 import { aliases } from "./aliases";
6020 export default defineConfig({ resolve: { alias: aliases } });
6021 "#;
6022 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
6023 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
6024 }
6025
6026 #[test]
6029 fn aliases_depth_limit_terminates_deep_chain() {
6030 let source = r#"
6033 const a9 = [{ find: "@", replacement: "./src" }];
6034 const a8 = a9;
6035 const a7 = a8;
6036 const a6 = a7;
6037 const a5 = a6;
6038 const a4 = a5;
6039 const a3 = a4;
6040 const a2 = a3;
6041 const a1 = a2;
6042 export default defineConfig({ resolve: { alias: a1 } });
6043 "#;
6044 let got = extract_config_aliases(source, &js_path(), &["resolve", "alias"]);
6046 let _ = got; }
6048
6049 #[test]
6052 fn extract_aliases_file_url_to_path_new_url() {
6053 let source = r#"
6055 import { fileURLToPath, URL } from 'node:url';
6056 export default {
6057 resolve: {
6058 alias: {
6059 "@": fileURLToPath(new URL("./src", import.meta.url))
6060 }
6061 }
6062 };
6063 "#;
6064 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
6065 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
6066 }
6067
6068 #[test]
6069 fn extract_path_via_new_url_pathname_member() {
6070 let source = r#"
6072 export default {
6073 resolve: {
6074 alias: {
6075 "@": new URL("./src", import.meta.url).pathname
6076 }
6077 }
6078 };
6079 "#;
6080 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
6081 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
6082 }
6083
6084 #[test]
6087 fn truthy_bool_or_object_null_literal_returns_false() {
6088 let source = r"export default { typescript: null };";
6090 let result = extract_config_truthy_bool_or_object(source, &js_path(), &["typescript"]);
6091 assert!(!result);
6092 }
6093
6094 #[test]
6097 fn string_array_non_array_value_returns_empty() {
6098 let source = r#"export default { items: "not-an-array" };"#;
6099 let result = extract_config_string_array(source, &js_path(), &["items"]);
6100 assert!(result.is_empty());
6101 }
6102
6103 #[test]
6106 fn object_nested_empty_when_inner_value_is_not_object() {
6107 let source = r#"export default { targets: { build: "not-an-object" } };"#;
6109 let results =
6110 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
6111 assert!(results.is_empty());
6112 }
6113
6114 #[test]
6117 fn array_nested_string_or_array_missing_inner_path_returns_empty() {
6118 let source = r#"
6119 export default {
6120 test: {
6121 projects: [
6122 { test: { include: ["**/*.test.ts"] } }
6123 ]
6124 }
6125 };
6126 "#;
6127 let results = extract_config_array_nested_string_or_array(
6128 source,
6129 &ts_path(),
6130 &["test", "projects"],
6131 &["test", "setupFiles"],
6132 );
6133 assert!(results.is_empty());
6134 }
6135
6136 #[test]
6137 fn wrapped_named_const_default_export_resolves() {
6138 let source = r#"
6141 import createMDX from "@next/mdx";
6142 const nextConfig = { pageExtensions: ["ts", "tsx", "md", "mdx"] };
6143 const withMDX = createMDX({});
6144 export default withMDX(nextConfig);
6145 "#;
6146 let exts = extract_config_string_array(source, &ts_path(), &["pageExtensions"]);
6147 assert_eq!(exts, vec!["ts", "tsx", "md", "mdx"]);
6148 }
6149
6150 #[test]
6151 fn wrapped_named_const_module_exports_resolves() {
6152 let source = r#"
6154 const nextJest = require("next/jest");
6155 const createJestConfig = nextJest();
6156 const customConfig = { testMatch: ["**/*.test.ts"] };
6157 module.exports = createJestConfig(customConfig);
6158 "#;
6159 let matches = extract_config_string_array(source, &js_path(), &["testMatch"]);
6160 assert_eq!(matches, vec!["**/*.test.ts"]);
6161 }
6162
6163 #[test]
6164 fn wrapped_named_const_nested_and_curried_resolve() {
6165 let nested = r#"
6166 const nextConfig = { pageExtensions: ["mdx"] };
6167 const withMDX = (c) => c;
6168 const withFoo = (c) => c;
6169 export default withMDX(withFoo(nextConfig));
6170 "#;
6171 assert_eq!(
6172 extract_config_string_array(nested, &js_path(), &["pageExtensions"]),
6173 vec!["mdx"]
6174 );
6175
6176 let curried = r#"
6177 const nextConfig = { pageExtensions: ["md"] };
6178 const compose = (..._p) => (c) => c;
6179 export default compose(a, b)(nextConfig);
6180 "#;
6181 assert_eq!(
6182 extract_config_string_array(curried, &js_path(), &["pageExtensions"]),
6183 vec!["md"]
6184 );
6185 }
6186
6187 #[test]
6188 fn wrapped_inline_object_still_resolves() {
6189 let source = r#"
6191 const withMDX = createMDX({});
6192 export default withMDX({ pageExtensions: ["mdx"] });
6193 "#;
6194 assert_eq!(
6195 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
6196 vec!["mdx"]
6197 );
6198 }
6199}