1use std::path::{Path, PathBuf};
15
16use fallow_extract::visitor::extract_import_from_callable;
17use oxc_allocator::Allocator;
18#[allow(clippy::wildcard_imports, reason = "many AST types used")]
19use oxc_ast::ast::*;
20use oxc_parser::Parser;
21use oxc_span::SourceType;
22
23#[must_use]
25pub fn extract_imports(source: &str, path: &Path) -> Vec<String> {
26 extract_from_source(source, path, |program| {
27 let mut sources = Vec::new();
28 for stmt in &program.body {
29 if let Statement::ImportDeclaration(decl) = stmt {
30 sources.push(decl.source.value.to_string());
31 }
32 }
33 Some(sources)
34 })
35 .unwrap_or_default()
36}
37
38#[must_use]
46pub fn extract_imports_and_requires(source: &str, path: &Path) -> Vec<String> {
47 extract_from_source(source, path, |program| {
48 let mut sources = Vec::new();
49 for stmt in &program.body {
50 match stmt {
51 Statement::ImportDeclaration(decl) => {
52 sources.push(decl.source.value.to_string());
53 }
54 Statement::ExpressionStatement(expr) => {
55 if let Expression::CallExpression(call) = &expr.expression
56 && is_require_call(call)
57 && let Some(s) = get_require_source(call)
58 {
59 sources.push(s);
60 }
61 }
62 _ => {}
63 }
64 }
65 Some(sources)
66 })
67 .unwrap_or_default()
68}
69
70#[must_use]
72pub fn extract_config_string_array(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
73 extract_from_source(source, path, |program| {
74 let obj = find_config_object(program)?;
75 get_nested_string_array_from_object(obj, prop_path)
76 })
77 .unwrap_or_default()
78}
79
80#[must_use]
82pub fn extract_config_string(source: &str, path: &Path, prop_path: &[&str]) -> Option<String> {
83 extract_from_source(source, path, |program| {
84 let obj = find_config_object(program)?;
85 get_nested_string_from_object(obj, prop_path)
86 })
87}
88
89#[must_use]
96pub fn extract_config_property_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
97 extract_from_source(source, path, |program| {
98 let obj = find_config_object(program)?;
99 let mut values = Vec::new();
100 if let Some(prop) = find_property(obj, key) {
101 collect_all_string_values(&prop.value, &mut values);
102 }
103 Some(values)
104 })
105 .unwrap_or_default()
106}
107
108#[must_use]
115pub fn extract_config_shallow_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
116 extract_from_source(source, path, |program| {
117 let obj = find_config_object(program)?;
118 let prop = find_property(obj, key)?;
119 Some(collect_shallow_string_values(&prop.value))
120 })
121 .unwrap_or_default()
122}
123
124#[must_use]
129pub fn extract_config_shallow_strings_or_object_property(
130 source: &str,
131 path: &Path,
132 key: &str,
133 object_property: &str,
134) -> Vec<String> {
135 extract_from_source(source, path, |program| {
136 let obj = find_config_object(program)?;
137 let prop = find_property(obj, key)?;
138 Some(collect_shallow_string_or_object_property_values(
139 &prop.value,
140 object_property,
141 ))
142 })
143 .unwrap_or_default()
144}
145
146#[must_use]
152pub fn extract_config_nested_shallow_strings(
153 source: &str,
154 path: &Path,
155 outer_path: &[&str],
156 key: &str,
157) -> Vec<String> {
158 extract_from_source(source, path, |program| {
159 let obj = find_config_object(program)?;
160 let nested = get_nested_expression(obj, outer_path)?;
161 if let Expression::ObjectExpression(nested_obj) = nested {
162 let prop = find_property(nested_obj, key)?;
163 Some(collect_shallow_string_values(&prop.value))
164 } else {
165 None
166 }
167 })
168 .unwrap_or_default()
169}
170
171pub fn find_config_object_pub<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
173 find_config_object(program)
174}
175
176pub(crate) fn property_expr<'a>(
178 obj: &'a ObjectExpression<'a>,
179 key: &str,
180) -> Option<&'a Expression<'a>> {
181 find_property(obj, key).map(|prop| &prop.value)
182}
183
184pub(crate) fn property_object<'a>(
186 obj: &'a ObjectExpression<'a>,
187 key: &str,
188) -> Option<&'a ObjectExpression<'a>> {
189 property_expr(obj, key).and_then(object_expression)
190}
191
192pub(crate) fn property_string(obj: &ObjectExpression<'_>, key: &str) -> Option<String> {
194 property_expr(obj, key).and_then(expression_to_string)
195}
196
197pub(crate) fn object_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ObjectExpression<'a>> {
199 match expr {
200 Expression::ObjectExpression(obj) => Some(obj),
201 Expression::ParenthesizedExpression(paren) => object_expression(&paren.expression),
202 Expression::TSSatisfiesExpression(ts_sat) => object_expression(&ts_sat.expression),
203 Expression::TSAsExpression(ts_as) => object_expression(&ts_as.expression),
204 _ => None,
205 }
206}
207
208pub(crate) fn array_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
210 match expr {
211 Expression::ArrayExpression(arr) => Some(arr),
212 Expression::ParenthesizedExpression(paren) => array_expression(&paren.expression),
213 Expression::TSSatisfiesExpression(ts_sat) => array_expression(&ts_sat.expression),
214 Expression::TSAsExpression(ts_as) => array_expression(&ts_as.expression),
215 _ => None,
216 }
217}
218
219pub(crate) fn expression_to_path_values(expr: &Expression<'_>) -> Vec<String> {
221 match expr {
222 Expression::ArrayExpression(arr) => arr
223 .elements
224 .iter()
225 .filter_map(|element| element.as_expression().and_then(expression_to_path_string))
226 .collect(),
227 _ => expression_to_path_string(expr).into_iter().collect(),
228 }
229}
230
231pub(crate) fn is_disabled_expression(expr: &Expression<'_>) -> bool {
233 matches!(expr, Expression::BooleanLiteral(boolean) if !boolean.value)
234 || matches!(expr, Expression::NullLiteral(_))
235}
236
237#[must_use]
239pub fn extract_config_truthy_bool_or_object(source: &str, path: &Path, prop_path: &[&str]) -> bool {
240 extract_from_source(source, path, |program| {
241 let obj = find_config_object(program)?;
242 let expr = get_nested_expression(obj, prop_path)?;
243 Some(is_truthy_bool_or_object(expr))
244 })
245 .unwrap_or(false)
246}
247
248fn is_truthy_bool_or_object(expr: &Expression<'_>) -> bool {
249 match expr {
250 Expression::BooleanLiteral(boolean) => boolean.value,
251 Expression::ObjectExpression(_) => true,
252 Expression::ParenthesizedExpression(paren) => is_truthy_bool_or_object(&paren.expression),
253 Expression::TSSatisfiesExpression(ts_sat) => is_truthy_bool_or_object(&ts_sat.expression),
254 Expression::TSAsExpression(ts_as) => is_truthy_bool_or_object(&ts_as.expression),
255 _ => false,
256 }
257}
258
259#[must_use]
264pub fn extract_config_object_keys(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
265 extract_from_source(source, path, |program| {
266 let obj = find_config_object(program)?;
267 get_nested_object_keys(obj, prop_path)
268 })
269 .unwrap_or_default()
270}
271
272#[must_use]
281pub fn extract_config_string_or_array(
282 source: &str,
283 path: &Path,
284 prop_path: &[&str],
285) -> Vec<String> {
286 extract_from_source(source, path, |program| {
287 let obj = find_config_object(program)?;
288 get_nested_string_or_array(obj, prop_path)
289 })
290 .unwrap_or_default()
291}
292
293#[must_use]
295pub fn extract_config_path_string(source: &str, path: &Path, prop_path: &[&str]) -> Option<String> {
296 extract_from_source(source, path, |program| {
297 let obj = find_config_object(program)?;
298 let expr = get_nested_expression(obj, prop_path)?;
299 expression_to_path_string(expr)
300 })
301}
302
303#[must_use]
310pub fn extract_config_array_nested_string_or_array(
311 source: &str,
312 path: &Path,
313 array_path: &[&str],
314 inner_path: &[&str],
315) -> Vec<String> {
316 extract_from_source(source, path, |program| {
317 let obj = find_config_object(program)?;
318 let array_expr = get_nested_expression(obj, array_path)?;
319 let Expression::ArrayExpression(arr) = array_expr else {
320 return None;
321 };
322 let mut results = Vec::new();
323 for element in &arr.elements {
324 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
325 && let Some(values) = get_nested_string_or_array(element_obj, inner_path)
326 {
327 results.extend(values);
328 }
329 }
330 if results.is_empty() {
331 None
332 } else {
333 Some(results)
334 }
335 })
336 .unwrap_or_default()
337}
338
339#[must_use]
346pub fn extract_config_object_nested_string_or_array(
347 source: &str,
348 path: &Path,
349 object_path: &[&str],
350 inner_path: &[&str],
351) -> Vec<String> {
352 extract_config_object_nested(source, path, object_path, |value_obj| {
353 get_nested_string_or_array(value_obj, inner_path)
354 })
355}
356
357#[must_use]
362pub fn extract_config_object_nested_strings(
363 source: &str,
364 path: &Path,
365 object_path: &[&str],
366 inner_path: &[&str],
367) -> Vec<String> {
368 extract_config_object_nested(source, path, object_path, |value_obj| {
369 get_nested_string_from_object(value_obj, inner_path).map(|s| vec![s])
370 })
371}
372
373fn extract_config_object_nested(
378 source: &str,
379 path: &Path,
380 object_path: &[&str],
381 extract_fn: impl Fn(&ObjectExpression<'_>) -> Option<Vec<String>>,
382) -> Vec<String> {
383 extract_from_source(source, path, |program| {
384 let obj = find_config_object(program)?;
385 let obj_expr = get_nested_expression(obj, object_path)?;
386 let Expression::ObjectExpression(target_obj) = obj_expr else {
387 return None;
388 };
389 let mut results = Vec::new();
390 for prop in &target_obj.properties {
391 if let ObjectPropertyKind::ObjectProperty(p) = prop
392 && let Expression::ObjectExpression(value_obj) = &p.value
393 && let Some(values) = extract_fn(value_obj)
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]
413pub fn extract_config_require_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
414 extract_from_source(source, path, |program| {
415 let obj = find_config_object(program)?;
416 let prop = find_property(obj, key)?;
417 Some(collect_require_sources(&prop.value))
418 })
419 .unwrap_or_default()
420}
421
422#[must_use]
429pub fn extract_config_aliases(
430 source: &str,
431 path: &Path,
432 prop_path: &[&str],
433) -> Vec<(String, String)> {
434 extract_from_source(source, path, |program| {
435 let obj = find_config_object(program)?;
436 let expr = get_nested_expression(obj, prop_path)?;
437 let aliases = expression_to_alias_pairs(expr);
438 (!aliases.is_empty()).then_some(aliases)
439 })
440 .unwrap_or_default()
441}
442
443#[must_use]
451pub fn extract_config_array_nested_aliases(
452 source: &str,
453 path: &Path,
454 array_path: &[&str],
455 alias_path: &[&str],
456) -> Vec<(String, String)> {
457 extract_from_source(source, path, |program| {
458 let obj = find_config_object(program)?;
459 let array_expr = get_nested_expression(obj, array_path)?;
460 let Expression::ArrayExpression(arr) = array_expr else {
461 return None;
462 };
463 let mut results = Vec::new();
464 for element in &arr.elements {
465 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
466 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
467 {
468 results.extend(expression_to_alias_pairs(alias_expr));
469 }
470 }
471 (!results.is_empty()).then_some(results)
472 })
473 .unwrap_or_default()
474}
475
476#[must_use]
485pub fn extract_config_aliases_kinded(
486 source: &str,
487 path: &Path,
488 prop_path: &[&str],
489) -> Vec<(String, String, bool)> {
490 extract_from_source(source, path, |program| {
491 let obj = find_config_object(program)?;
492 let expr = get_nested_expression(obj, prop_path)?;
493 let aliases = expression_to_alias_pairs_kinded(expr);
494 (!aliases.is_empty()).then_some(aliases)
495 })
496 .unwrap_or_default()
497}
498
499#[must_use]
502pub fn extract_config_array_nested_aliases_kinded(
503 source: &str,
504 path: &Path,
505 array_path: &[&str],
506 alias_path: &[&str],
507) -> Vec<(String, String, bool)> {
508 extract_from_source(source, path, |program| {
509 let obj = find_config_object(program)?;
510 let array_expr = get_nested_expression(obj, array_path)?;
511 let Expression::ArrayExpression(arr) = array_expr else {
512 return None;
513 };
514 let mut results = Vec::new();
515 for element in &arr.elements {
516 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
517 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
518 {
519 results.extend(expression_to_alias_pairs_kinded(alias_expr));
520 }
521 }
522 (!results.is_empty()).then_some(results)
523 })
524 .unwrap_or_default()
525}
526
527#[must_use]
534pub fn extract_default_export_array_aliases_kinded(
535 source: &str,
536 path: &Path,
537 alias_path: &[&str],
538) -> Vec<(String, String, bool)> {
539 extract_from_source(source, path, |program| {
540 let arr = find_default_export_array(program)?;
541 let mut results = Vec::new();
542 for element in &arr.elements {
543 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
544 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
545 {
546 results.extend(expression_to_alias_pairs_kinded(alias_expr));
547 }
548 }
549 (!results.is_empty()).then_some(results)
550 })
551 .unwrap_or_default()
552}
553
554#[must_use]
560pub fn config_default_export_unreachable(source: &str, path: &Path) -> bool {
561 extract_from_source(source, path, |program| {
562 let reachable =
563 find_config_object(program).is_some() || find_default_export_array(program).is_some();
564 Some(reachable)
565 })
566 .is_some_and(|reachable| !reachable)
567}
568
569#[must_use]
575pub fn extract_config_array_object_strings(
576 source: &str,
577 path: &Path,
578 array_path: &[&str],
579 key: &str,
580) -> Vec<String> {
581 extract_from_source(source, path, |program| {
582 let obj = find_config_object(program)?;
583 let array_expr = get_nested_expression(obj, array_path)?;
584 let Expression::ArrayExpression(arr) = array_expr else {
585 return None;
586 };
587
588 let mut results = Vec::new();
589 for element in &arr.elements {
590 let Some(expr) = element.as_expression() else {
591 continue;
592 };
593 match expr {
594 Expression::ObjectExpression(item) => {
595 if let Some(prop) = find_property(item, key)
596 && let Some(value) = expression_to_path_string(&prop.value)
597 {
598 results.push(value);
599 }
600 }
601 _ => {
602 if let Some(value) = expression_to_path_string(expr) {
603 results.push(value);
604 }
605 }
606 }
607 }
608
609 (!results.is_empty()).then_some(results)
610 })
611 .unwrap_or_default()
612}
613
614#[must_use]
619pub fn extract_config_static_dir_entries(
620 source: &str,
621 path: &Path,
622 array_path: &[&str],
623) -> Vec<(String, Option<String>)> {
624 extract_from_source(source, path, |program| {
625 let obj = find_config_object(program)?;
626 let array_expr = get_nested_expression(obj, array_path)?;
627 let Expression::ArrayExpression(arr) = array_expr else {
628 return None;
629 };
630
631 let mut results = Vec::new();
632 for element in &arr.elements {
633 let Some(expr) = element.as_expression() else {
634 continue;
635 };
636 match expr {
637 Expression::ObjectExpression(item) => {
638 if let Some(from) = property_string(item, "from") {
639 let to = property_string(item, "to");
640 results.push((from, to));
641 }
642 }
643 _ => {
644 if let Some(from) = expression_to_path_string(expr) {
645 results.push((from, None));
646 }
647 }
648 }
649 }
650
651 (!results.is_empty()).then_some(results)
652 })
653 .unwrap_or_default()
654}
655
656#[must_use]
667pub fn extract_config_array_object_string_pairs(
668 source: &str,
669 path: &Path,
670 array_path: &[&str],
671 primary_key: &str,
672 secondary_key: &str,
673) -> Vec<(String, Option<String>)> {
674 extract_from_source(source, path, |program| {
675 let obj = find_config_object(program)?;
676 let array_expr = get_nested_expression(obj, array_path)?;
677 let Expression::ArrayExpression(arr) = array_expr else {
678 return None;
679 };
680
681 let mut results = Vec::new();
682 for element in &arr.elements {
683 let Some(Expression::ObjectExpression(item)) = element.as_expression() else {
684 continue;
685 };
686 let Some(primary) = find_property(item, primary_key)
687 .and_then(|prop| expression_to_path_string(&prop.value))
688 else {
689 continue;
690 };
691 let secondary = find_property(item, secondary_key)
692 .and_then(|prop| expression_to_path_string(&prop.value));
693 results.push((primary, secondary));
694 }
695
696 (!results.is_empty()).then_some(results)
697 })
698 .unwrap_or_default()
699}
700
701#[must_use]
747pub fn extract_lazy_imports_in_array(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
748 extract_from_source(source, path, |program| {
749 let obj = find_config_object(program)?;
750 let array_expr = get_nested_expression(obj, prop_path)?;
751 let Expression::ArrayExpression(arr) = array_expr else {
752 return None;
753 };
754 let mut specs = Vec::new();
755 for element in &arr.elements {
756 let Some(expr) = element.as_expression() else {
757 continue;
758 };
759 if let Some(spec) = lazy_import_specifier(expr) {
760 specs.push(spec);
761 }
762 }
763 (!specs.is_empty()).then_some(specs)
764 })
765 .unwrap_or_default()
766}
767
768fn lazy_import_specifier(expr: &Expression<'_>) -> Option<String> {
781 let callable = match expr {
782 Expression::ObjectExpression(obj) => &find_property(obj, "file")?.value,
783 _ => expr,
784 };
785 let import_expr = extract_import_from_callable(callable)?;
786 expression_to_string(&import_expr.source)
787}
788
789#[must_use]
796pub fn extract_config_plugin_option_string(
797 source: &str,
798 path: &Path,
799 plugins_path: &[&str],
800 plugin_name: &str,
801 option_key: &str,
802) -> Option<String> {
803 extract_from_source(source, path, |program| {
804 let obj = find_config_object(program)?;
805 let plugins_expr = get_nested_expression(obj, plugins_path)?;
806 let Expression::ArrayExpression(plugins) = plugins_expr else {
807 return None;
808 };
809
810 for entry in &plugins.elements {
811 let Some(Expression::ArrayExpression(tuple)) = entry.as_expression() else {
812 continue;
813 };
814 let Some(plugin_expr) = tuple
815 .elements
816 .first()
817 .and_then(ArrayExpressionElement::as_expression)
818 else {
819 continue;
820 };
821 if expression_to_string(plugin_expr).as_deref() != Some(plugin_name) {
822 continue;
823 }
824
825 let Some(options_expr) = tuple
826 .elements
827 .get(1)
828 .and_then(ArrayExpressionElement::as_expression)
829 else {
830 continue;
831 };
832 let Expression::ObjectExpression(options_obj) = options_expr else {
833 continue;
834 };
835 let option = find_property(options_obj, option_key)?;
836 return expression_to_path_string(&option.value);
837 }
838
839 None
840 })
841}
842
843#[must_use]
845pub fn extract_config_plugin_option_string_from_paths(
846 source: &str,
847 path: &Path,
848 plugin_paths: &[&[&str]],
849 plugin_name: &str,
850 option_key: &str,
851) -> Option<String> {
852 plugin_paths.iter().find_map(|plugins_path| {
853 extract_config_plugin_option_string(source, path, plugins_path, plugin_name, option_key)
854 })
855}
856
857#[must_use]
860pub fn extract_vite_react_babel_dependencies(source: &str, path: &Path) -> Vec<String> {
861 extract_from_source(source, path, |program| {
862 let react_plugin_imports = collect_vite_react_plugin_imports(program);
863 if react_plugin_imports.is_empty() {
864 return None;
865 }
866
867 let obj = find_config_object(program)?;
868 let plugins = get_nested_expression(obj, &["plugins"])?;
869 let Expression::ArrayExpression(plugin_array) = plugins else {
870 return None;
871 };
872
873 let mut deps = Vec::new();
874 for element in &plugin_array.elements {
875 let Some(Expression::CallExpression(call)) = element.as_expression() else {
876 continue;
877 };
878 if !is_vite_react_plugin_call(call, &react_plugin_imports) {
879 continue;
880 }
881 let Some(Expression::ObjectExpression(options)) =
882 call.arguments.first().and_then(Argument::as_expression)
883 else {
884 continue;
885 };
886 collect_vite_react_babel_dependencies(options, &mut deps);
887 }
888
889 (!deps.is_empty()).then_some(deps)
890 })
891 .unwrap_or_default()
892}
893
894#[must_use]
899pub fn normalize_config_path(raw: &str, config_path: &Path, root: &Path) -> Option<String> {
900 if raw.is_empty() {
901 return None;
902 }
903
904 let candidate = if let Some(stripped) = raw.strip_prefix('/') {
905 lexical_normalize(&root.join(stripped))
906 } else {
907 let path = Path::new(raw);
908 if path.is_absolute() {
909 lexical_normalize(path)
910 } else {
911 let base = config_path.parent().unwrap_or(root);
912 lexical_normalize(&base.join(path))
913 }
914 };
915
916 let relative = candidate.strip_prefix(root).ok()?;
917 let normalized = relative.to_string_lossy().replace('\\', "/");
918 (!normalized.is_empty()).then_some(normalized)
919}
920
921pub(crate) fn extract_from_source<T>(
930 source: &str,
931 path: &Path,
932 extractor: impl FnOnce(&Program) -> Option<T>,
933) -> Option<T> {
934 let source_type = SourceType::from_path(path).unwrap_or_default();
935 let alloc = Allocator::default();
936
937 let is_json = path
940 .extension()
941 .is_some_and(|ext| ext == "json" || ext == "jsonc");
942 if is_json {
943 let wrapped = format!("({source})");
944 let parsed = Parser::new(&alloc, &wrapped, SourceType::mjs()).parse();
945 return extractor(&parsed.program);
946 }
947
948 let parsed = Parser::new(&alloc, source, source_type).parse();
949 extractor(&parsed.program)
950}
951
952#[derive(Default)]
953struct ViteReactPluginImports {
954 callables: Vec<String>,
955 namespaces: Vec<String>,
956}
957
958impl ViteReactPluginImports {
959 fn is_empty(&self) -> bool {
960 self.callables.is_empty() && self.namespaces.is_empty()
961 }
962}
963
964fn collect_vite_react_plugin_imports(program: &Program<'_>) -> ViteReactPluginImports {
965 let mut imports = ViteReactPluginImports::default();
966
967 for stmt in &program.body {
968 let Statement::ImportDeclaration(decl) = stmt else {
969 continue;
970 };
971 if decl.source.value != "@vitejs/plugin-react" {
972 continue;
973 }
974 let Some(specifiers) = &decl.specifiers else {
975 continue;
976 };
977 for specifier in specifiers {
978 match specifier {
979 ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
980 push_unique_string(&mut imports.callables, specifier.local.name.to_string());
981 }
982 ImportDeclarationSpecifier::ImportSpecifier(specifier)
983 if specifier.imported.name().as_ref() == "default" =>
984 {
985 push_unique_string(&mut imports.callables, specifier.local.name.to_string());
986 }
987 ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
988 push_unique_string(&mut imports.namespaces, specifier.local.name.to_string());
989 }
990 ImportDeclarationSpecifier::ImportSpecifier(_) => {}
991 }
992 }
993 }
994
995 imports
996}
997
998fn is_vite_react_plugin_call(call: &CallExpression<'_>, imports: &ViteReactPluginImports) -> bool {
999 match &call.callee {
1000 Expression::Identifier(identifier) => imports
1001 .callables
1002 .iter()
1003 .any(|name| name == identifier.name.as_str()),
1004 Expression::StaticMemberExpression(member) if matches!(&member.object, Expression::Identifier(object) if imports.namespaces.iter().any(|name| name == object.name.as_str())) => {
1005 member.property.name == "default"
1006 }
1007 _ => false,
1008 }
1009}
1010
1011fn collect_vite_react_babel_dependencies(options: &ObjectExpression<'_>, deps: &mut Vec<String>) {
1012 let Some(babel) = property_object(options, "babel") else {
1013 return;
1014 };
1015 for key in ["plugins", "presets"] {
1016 let Some(prop) = find_property(babel, key) else {
1017 continue;
1018 };
1019 for raw in collect_shallow_string_values(&prop.value) {
1020 if let Some(dep) = vite_react_babel_dependency_name(&raw) {
1021 push_unique_string(deps, dep);
1022 }
1023 }
1024 }
1025}
1026
1027fn vite_react_babel_dependency_name(raw: &str) -> Option<String> {
1028 let raw = raw.trim();
1029 let specifier = raw.strip_prefix("module:").unwrap_or(raw).trim();
1030 if specifier.is_empty()
1031 || specifier.starts_with('.')
1032 || specifier.starts_with('/')
1033 || specifier.contains(':')
1034 || specifier.contains('\\')
1035 {
1036 return None;
1037 }
1038 Some(crate::resolve::extract_package_name(specifier))
1039}
1040
1041fn push_unique_string(items: &mut Vec<String>, value: String) {
1042 if !items.contains(&value) {
1043 items.push(value);
1044 }
1045}
1046
1047fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
1059 for stmt in &program.body {
1060 match stmt {
1061 Statement::ExportDefaultDeclaration(decl) => {
1063 let expr: Option<&Expression> = match &decl.declaration {
1065 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
1066 return Some(obj);
1067 }
1068 ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
1069 return extract_object_from_function(func);
1070 }
1071 _ => decl.declaration.as_expression(),
1072 };
1073 if let Some(expr) = expr {
1074 if let Some(obj) = extract_object_from_expression(expr) {
1076 return Some(obj);
1077 }
1078 if let Some(name) = unwrap_to_identifier_name(expr) {
1081 return find_variable_init_object(program, name);
1082 }
1083 }
1084 }
1085 Statement::ExpressionStatement(expr_stmt) => {
1087 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
1088 && is_module_exports_target(&assign.left)
1089 {
1090 return extract_object_from_expression(&assign.right);
1091 }
1092 }
1093 _ => {}
1094 }
1095 }
1096
1097 if program.body.len() == 1
1100 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
1101 {
1102 match &expr_stmt.expression {
1103 Expression::ObjectExpression(obj) => return Some(obj),
1104 Expression::ParenthesizedExpression(paren) => {
1105 if let Expression::ObjectExpression(obj) = &paren.expression {
1106 return Some(obj);
1107 }
1108 }
1109 _ => {}
1110 }
1111 }
1112
1113 None
1114}
1115
1116fn extract_object_from_expression<'a>(
1118 expr: &'a Expression<'a>,
1119) -> Option<&'a ObjectExpression<'a>> {
1120 match expr {
1121 Expression::ObjectExpression(obj) => Some(obj),
1123 Expression::CallExpression(call) => {
1125 for arg in &call.arguments {
1127 match arg {
1128 Argument::ObjectExpression(obj) => return Some(obj),
1129 Argument::ArrowFunctionExpression(arrow) => {
1131 if arrow.expression
1132 && !arrow.body.statements.is_empty()
1133 && let Statement::ExpressionStatement(expr_stmt) =
1134 &arrow.body.statements[0]
1135 {
1136 return extract_object_from_expression(&expr_stmt.expression);
1137 }
1138 }
1139 _ => {}
1140 }
1141 }
1142 None
1143 }
1144 Expression::ParenthesizedExpression(paren) => {
1146 extract_object_from_expression(&paren.expression)
1147 }
1148 Expression::TSSatisfiesExpression(ts_sat) => {
1150 extract_object_from_expression(&ts_sat.expression)
1151 }
1152 Expression::TSAsExpression(ts_as) => extract_object_from_expression(&ts_as.expression),
1153 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
1154 Expression::FunctionExpression(func) => extract_object_from_function(func),
1155 _ => None,
1156 }
1157}
1158
1159fn extract_object_from_arrow_function<'a>(
1160 arrow: &'a ArrowFunctionExpression<'a>,
1161) -> Option<&'a ObjectExpression<'a>> {
1162 if arrow.expression {
1163 arrow.body.statements.first().and_then(|stmt| {
1164 if let Statement::ExpressionStatement(expr_stmt) = stmt {
1165 extract_object_from_expression(&expr_stmt.expression)
1166 } else {
1167 None
1168 }
1169 })
1170 } else {
1171 extract_object_from_function_body(&arrow.body)
1172 }
1173}
1174
1175fn extract_object_from_function<'a>(func: &'a Function<'a>) -> Option<&'a ObjectExpression<'a>> {
1176 func.body
1177 .as_ref()
1178 .and_then(|body| extract_object_from_function_body(body))
1179}
1180
1181fn extract_object_from_function_body<'a>(
1182 body: &'a FunctionBody<'a>,
1183) -> Option<&'a ObjectExpression<'a>> {
1184 for stmt in &body.statements {
1185 if let Statement::ReturnStatement(ret) = stmt
1186 && let Some(argument) = &ret.argument
1187 && let Some(obj) = extract_object_from_expression(argument)
1188 {
1189 return Some(obj);
1190 }
1191 }
1192 None
1193}
1194
1195fn is_module_exports_target(target: &AssignmentTarget) -> bool {
1197 if let AssignmentTarget::StaticMemberExpression(member) = target
1198 && let Expression::Identifier(obj) = &member.object
1199 {
1200 return obj.name == "module" && member.property.name == "exports";
1201 }
1202 false
1203}
1204
1205fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
1209 match expr {
1210 Expression::Identifier(id) => Some(&id.name),
1211 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
1212 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
1213 _ => None,
1214 }
1215}
1216
1217fn find_variable_init_object<'a>(
1222 program: &'a Program,
1223 name: &str,
1224) -> Option<&'a ObjectExpression<'a>> {
1225 for stmt in &program.body {
1226 if let Statement::VariableDeclaration(decl) = stmt {
1227 for declarator in &decl.declarations {
1228 if let BindingPattern::BindingIdentifier(id) = &declarator.id
1229 && id.name == name
1230 && let Some(init) = &declarator.init
1231 {
1232 return extract_object_from_expression(init);
1233 }
1234 }
1235 }
1236 }
1237 None
1238}
1239
1240pub(crate) fn find_property<'a>(
1242 obj: &'a ObjectExpression<'a>,
1243 key: &str,
1244) -> Option<&'a ObjectProperty<'a>> {
1245 for prop in &obj.properties {
1246 if let ObjectPropertyKind::ObjectProperty(p) = prop
1247 && property_key_matches(&p.key, key)
1248 {
1249 return Some(p);
1250 }
1251 }
1252 None
1253}
1254
1255pub(crate) fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
1257 match key {
1258 PropertyKey::StaticIdentifier(id) => id.name == name,
1259 PropertyKey::StringLiteral(s) => s.value == name,
1260 _ => false,
1261 }
1262}
1263
1264fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
1266 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
1267}
1268
1269fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
1271 find_property(obj, key)
1272 .map(|p| expression_to_string_array(&p.value))
1273 .unwrap_or_default()
1274}
1275
1276fn get_nested_string_array_from_object(
1278 obj: &ObjectExpression,
1279 path: &[&str],
1280) -> Option<Vec<String>> {
1281 if path.is_empty() {
1282 return None;
1283 }
1284 if path.len() == 1 {
1285 return Some(get_object_string_array_property(obj, path[0]));
1286 }
1287 let prop = find_property(obj, path[0])?;
1289 if let Expression::ObjectExpression(nested) = &prop.value {
1290 get_nested_string_array_from_object(nested, &path[1..])
1291 } else {
1292 None
1293 }
1294}
1295
1296fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
1298 if path.is_empty() {
1299 return None;
1300 }
1301 if path.len() == 1 {
1302 return get_object_string_property(obj, path[0]);
1303 }
1304 let prop = find_property(obj, path[0])?;
1305 if let Expression::ObjectExpression(nested) = &prop.value {
1306 get_nested_string_from_object(nested, &path[1..])
1307 } else {
1308 None
1309 }
1310}
1311
1312pub(crate) fn expression_to_string(expr: &Expression) -> Option<String> {
1314 match expr {
1315 Expression::StringLiteral(s) => Some(s.value.to_string()),
1316 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
1317 t.quasis.first().map(|q| q.value.raw.to_string())
1319 }
1320 _ => None,
1321 }
1322}
1323
1324pub(crate) fn expression_to_path_string(expr: &Expression) -> Option<String> {
1326 match expr {
1327 Expression::ParenthesizedExpression(paren) => expression_to_path_string(&paren.expression),
1328 Expression::TSAsExpression(ts_as) => expression_to_path_string(&ts_as.expression),
1329 Expression::TSSatisfiesExpression(ts_sat) => expression_to_path_string(&ts_sat.expression),
1330 Expression::StaticMemberExpression(member) if member.property.name == "pathname" => {
1331 expression_to_path_string(&member.object)
1332 }
1333 Expression::CallExpression(call) => call_expression_to_path_string(call),
1334 Expression::NewExpression(new_expr) => new_expression_to_path_string(new_expr),
1335 _ => expression_to_string(expr),
1336 }
1337}
1338
1339fn call_expression_to_path_string(call: &CallExpression) -> Option<String> {
1340 if matches!(&call.callee, Expression::Identifier(id) if id.name == "fileURLToPath") {
1341 return call
1342 .arguments
1343 .first()
1344 .and_then(Argument::as_expression)
1345 .and_then(expression_to_path_string);
1346 }
1347
1348 let callee_name = match &call.callee {
1349 Expression::Identifier(id) => Some(id.name.as_str()),
1350 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
1351 _ => None,
1352 }?;
1353
1354 if !matches!(callee_name, "resolve" | "join") {
1355 return None;
1356 }
1357
1358 let mut segments = Vec::new();
1359 for (index, arg) in call.arguments.iter().enumerate() {
1360 let expr = arg.as_expression()?;
1361
1362 if is_dirname_anchor(expr) {
1363 if index == 0 {
1364 continue;
1365 }
1366 return None;
1367 }
1368
1369 segments.push(expression_to_string(expr)?);
1370 }
1371
1372 (!segments.is_empty()).then(|| join_path_segments(&segments))
1373}
1374
1375fn is_dirname_anchor(expr: &Expression) -> bool {
1380 match expr {
1381 Expression::Identifier(id) => id.name == "__dirname",
1382 Expression::StaticMemberExpression(member) => {
1383 member.property.name == "dirname" && is_import_meta_expression(&member.object)
1384 }
1385 _ => false,
1386 }
1387}
1388
1389fn is_import_meta_expression(expr: &Expression) -> bool {
1391 matches!(
1392 expr,
1393 Expression::MetaProperty(meta) if meta.meta.name == "import" && meta.property.name == "meta"
1394 )
1395}
1396
1397fn new_expression_to_path_string(new_expr: &NewExpression) -> Option<String> {
1398 if !matches!(&new_expr.callee, Expression::Identifier(id) if id.name == "URL") {
1399 return None;
1400 }
1401
1402 let source = new_expr
1403 .arguments
1404 .first()
1405 .and_then(Argument::as_expression)
1406 .and_then(expression_to_string)?;
1407
1408 let base = new_expr
1409 .arguments
1410 .get(1)
1411 .and_then(Argument::as_expression)?;
1412 is_import_meta_url_expression(base).then_some(source)
1413}
1414
1415fn is_import_meta_url_expression(expr: &Expression) -> bool {
1416 if let Expression::StaticMemberExpression(member) = expr {
1417 member.property.name == "url" && matches!(member.object, Expression::MetaProperty(_))
1418 } else {
1419 false
1420 }
1421}
1422
1423fn join_path_segments(segments: &[String]) -> String {
1424 let mut joined = PathBuf::new();
1425 for segment in segments {
1426 joined.push(segment);
1427 }
1428 joined.to_string_lossy().replace('\\', "/")
1429}
1430
1431fn expression_to_alias_pairs(expr: &Expression) -> Vec<(String, String)> {
1432 match expr {
1433 Expression::ObjectExpression(obj) => obj
1434 .properties
1435 .iter()
1436 .filter_map(|prop| {
1437 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
1438 return None;
1439 };
1440 let find = property_key_to_string(&prop.key)?;
1441 let replacement = expression_to_path_values(&prop.value).into_iter().next()?;
1442 Some((find, replacement))
1443 })
1444 .collect(),
1445 Expression::ArrayExpression(arr) => arr
1446 .elements
1447 .iter()
1448 .filter_map(|element| {
1449 let Expression::ObjectExpression(obj) = element.as_expression()? else {
1450 return None;
1451 };
1452 let find = find_property(obj, "find")
1453 .and_then(|prop| expression_to_string(&prop.value))?;
1454 let replacement = find_property(obj, "replacement")
1455 .and_then(|prop| expression_to_path_string(&prop.value))?;
1456 Some((find, replacement))
1457 })
1458 .collect(),
1459 _ => Vec::new(),
1460 }
1461}
1462
1463fn expression_to_alias_pairs_kinded(expr: &Expression) -> Vec<(String, String, bool)> {
1467 match expr {
1468 Expression::ObjectExpression(obj) => obj
1469 .properties
1470 .iter()
1471 .filter_map(|prop| {
1472 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
1473 return None;
1474 };
1475 let find = property_key_to_string(&prop.key)?;
1476 let (replacement, is_bare) = alias_replacement_kinded(&prop.value)?;
1477 Some((find, replacement, is_bare))
1478 })
1479 .collect(),
1480 Expression::ArrayExpression(arr) => arr
1481 .elements
1482 .iter()
1483 .filter_map(|element| {
1484 let Expression::ObjectExpression(obj) = element.as_expression()? else {
1485 return None;
1486 };
1487 let find = find_property(obj, "find")
1488 .and_then(|prop| expression_to_string(&prop.value))?;
1489 let (replacement, is_bare) = find_property(obj, "replacement")
1490 .and_then(|prop| alias_replacement_kinded(&prop.value))?;
1491 Some((find, replacement, is_bare))
1492 })
1493 .collect(),
1494 _ => Vec::new(),
1495 }
1496}
1497
1498fn alias_replacement_kinded(expr: &Expression) -> Option<(String, bool)> {
1505 match expr {
1506 Expression::ParenthesizedExpression(paren) => alias_replacement_kinded(&paren.expression),
1507 Expression::TSAsExpression(ts_as) => alias_replacement_kinded(&ts_as.expression),
1508 Expression::TSSatisfiesExpression(ts_sat) => alias_replacement_kinded(&ts_sat.expression),
1509 Expression::StringLiteral(s) => {
1510 let value = s.value.to_string();
1511 let is_bare =
1512 !value.starts_with("./") && !value.starts_with("../") && !value.starts_with('/');
1513 Some((value, is_bare))
1514 }
1515 _ => expression_to_path_string(expr).map(|value| (value, false)),
1518 }
1519}
1520
1521fn find_default_export_array<'a>(program: &'a Program<'a>) -> Option<&'a ArrayExpression<'a>> {
1526 for stmt in &program.body {
1527 if let Statement::ExportDefaultDeclaration(decl) = stmt
1528 && let Some(expr) = decl.declaration.as_expression()
1529 {
1530 return array_from_expression(expr);
1531 }
1532 }
1533 None
1534}
1535
1536fn array_from_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
1537 match expr {
1538 Expression::ArrayExpression(arr) => Some(arr),
1539 Expression::ParenthesizedExpression(paren) => array_from_expression(&paren.expression),
1540 Expression::TSAsExpression(ts_as) => array_from_expression(&ts_as.expression),
1541 Expression::TSSatisfiesExpression(ts_sat) => array_from_expression(&ts_sat.expression),
1542 Expression::CallExpression(call) => call
1544 .arguments
1545 .first()
1546 .and_then(Argument::as_expression)
1547 .and_then(array_from_expression),
1548 _ => None,
1549 }
1550}
1551
1552fn lexical_normalize(path: &Path) -> PathBuf {
1553 let mut normalized = PathBuf::new();
1554
1555 for component in path.components() {
1556 match component {
1557 std::path::Component::CurDir => {}
1558 std::path::Component::ParentDir => {
1559 normalized.pop();
1560 }
1561 _ => normalized.push(component.as_os_str()),
1562 }
1563 }
1564
1565 normalized
1566}
1567
1568fn expression_to_string_array(expr: &Expression) -> Vec<String> {
1570 match expr {
1571 Expression::ArrayExpression(arr) => arr
1572 .elements
1573 .iter()
1574 .filter_map(|el| match el {
1575 ArrayExpressionElement::SpreadElement(_) => None,
1576 _ => el.as_expression().and_then(expression_to_string),
1577 })
1578 .collect(),
1579 _ => vec![],
1580 }
1581}
1582
1583fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
1588 let mut values = Vec::new();
1589 match expr {
1590 Expression::StringLiteral(s) => {
1591 values.push(s.value.to_string());
1592 }
1593 Expression::ArrayExpression(arr) => {
1594 for el in &arr.elements {
1595 if let Some(inner) = el.as_expression() {
1596 match inner {
1597 Expression::StringLiteral(s) => {
1598 values.push(s.value.to_string());
1599 }
1600 Expression::ArrayExpression(sub_arr) => {
1602 if let Some(first) = sub_arr.elements.first()
1603 && let Some(first_expr) = first.as_expression()
1604 && let Some(s) = expression_to_string(first_expr)
1605 {
1606 values.push(s);
1607 }
1608 }
1609 _ => {}
1610 }
1611 }
1612 }
1613 }
1614 Expression::ObjectExpression(obj) => {
1616 for prop in &obj.properties {
1617 if let ObjectPropertyKind::ObjectProperty(p) = prop {
1618 match &p.value {
1619 Expression::StringLiteral(s) => {
1620 values.push(s.value.to_string());
1621 }
1622 Expression::ArrayExpression(sub_arr) => {
1624 if let Some(first) = sub_arr.elements.first()
1625 && let Some(first_expr) = first.as_expression()
1626 && let Some(s) = expression_to_string(first_expr)
1627 {
1628 values.push(s);
1629 }
1630 }
1631 _ => {}
1632 }
1633 }
1634 }
1635 }
1636 _ => {}
1637 }
1638 values
1639}
1640
1641fn collect_shallow_string_or_object_property_values(
1643 expr: &Expression,
1644 object_property: &str,
1645) -> Vec<String> {
1646 match expr {
1647 Expression::ArrayExpression(arr) => arr
1648 .elements
1649 .iter()
1650 .filter_map(|element| {
1651 element
1652 .as_expression()
1653 .and_then(|expr| shallow_string_or_object_property(expr, object_property))
1654 })
1655 .collect(),
1656 _ => shallow_string_or_object_property(expr, object_property)
1657 .into_iter()
1658 .collect(),
1659 }
1660}
1661
1662fn shallow_string_or_object_property(expr: &Expression, object_property: &str) -> Option<String> {
1663 match expr {
1664 Expression::ParenthesizedExpression(paren) => {
1665 shallow_string_or_object_property(&paren.expression, object_property)
1666 }
1667 Expression::TSSatisfiesExpression(ts_sat) => {
1668 shallow_string_or_object_property(&ts_sat.expression, object_property)
1669 }
1670 Expression::TSAsExpression(ts_as) => {
1671 shallow_string_or_object_property(&ts_as.expression, object_property)
1672 }
1673 Expression::ArrayExpression(sub_arr) => sub_arr
1674 .elements
1675 .first()
1676 .and_then(ArrayExpressionElement::as_expression)
1677 .and_then(expression_to_string),
1678 Expression::ObjectExpression(obj) => {
1679 find_property(obj, object_property).and_then(|prop| expression_to_string(&prop.value))
1680 }
1681 _ => expression_to_string(expr),
1682 }
1683}
1684
1685fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
1687 match expr {
1688 Expression::StringLiteral(s) => {
1689 values.push(s.value.to_string());
1690 }
1691 Expression::ArrayExpression(arr) => {
1692 for el in &arr.elements {
1693 if let Some(expr) = el.as_expression() {
1694 collect_all_string_values(expr, values);
1695 }
1696 }
1697 }
1698 Expression::ObjectExpression(obj) => {
1699 for prop in &obj.properties {
1700 if let ObjectPropertyKind::ObjectProperty(p) = prop {
1701 collect_all_string_values(&p.value, values);
1702 }
1703 }
1704 }
1705 _ => {}
1706 }
1707}
1708
1709fn property_key_to_string(key: &PropertyKey) -> Option<String> {
1711 match key {
1712 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
1713 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
1714 _ => None,
1715 }
1716}
1717
1718fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
1720 if path.is_empty() {
1721 return None;
1722 }
1723 let prop = find_property(obj, path[0])?;
1724 if path.len() == 1 {
1725 if let Expression::ObjectExpression(nested) = &prop.value {
1726 let keys = nested
1727 .properties
1728 .iter()
1729 .filter_map(|p| {
1730 if let ObjectPropertyKind::ObjectProperty(p) = p {
1731 property_key_to_string(&p.key)
1732 } else {
1733 None
1734 }
1735 })
1736 .collect();
1737 return Some(keys);
1738 }
1739 return None;
1740 }
1741 if let Expression::ObjectExpression(nested) = &prop.value {
1742 get_nested_object_keys(nested, &path[1..])
1743 } else {
1744 None
1745 }
1746}
1747
1748fn get_nested_expression<'a>(
1750 obj: &'a ObjectExpression<'a>,
1751 path: &[&str],
1752) -> Option<&'a Expression<'a>> {
1753 if path.is_empty() {
1754 return None;
1755 }
1756 let prop = find_property(obj, path[0])?;
1757 if path.len() == 1 {
1758 return Some(&prop.value);
1759 }
1760 if let Expression::ObjectExpression(nested) = &prop.value {
1761 get_nested_expression(nested, &path[1..])
1762 } else {
1763 None
1764 }
1765}
1766
1767fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
1769 if path.is_empty() {
1770 return None;
1771 }
1772 if path.len() == 1 {
1773 let prop = find_property(obj, path[0])?;
1774 return Some(expression_to_string_or_array(&prop.value));
1775 }
1776 let prop = find_property(obj, path[0])?;
1777 if let Expression::ObjectExpression(nested) = &prop.value {
1778 get_nested_string_or_array(nested, &path[1..])
1779 } else {
1780 None
1781 }
1782}
1783
1784fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
1792 match expr {
1793 Expression::StringLiteral(s) => vec![s.value.to_string()],
1794 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
1795 .quasis
1796 .first()
1797 .map(|q| vec![q.value.raw.to_string()])
1798 .unwrap_or_default(),
1799 Expression::ArrayExpression(arr) => arr
1800 .elements
1801 .iter()
1802 .filter_map(|el| el.as_expression())
1803 .flat_map(|e| match e {
1804 Expression::ObjectExpression(obj) => find_property(obj, "input")
1805 .map(|p| expression_to_string_or_array(&p.value))
1806 .unwrap_or_default(),
1807 _ => expression_to_path_string(e).into_iter().collect(),
1813 })
1814 .collect(),
1815 Expression::ObjectExpression(obj) => obj
1816 .properties
1817 .iter()
1818 .flat_map(|p| {
1819 if let ObjectPropertyKind::ObjectProperty(p) = p {
1820 match &p.value {
1821 Expression::ArrayExpression(_) => expression_to_string_or_array(&p.value),
1822 Expression::ObjectExpression(value_obj) => {
1823 find_property(value_obj, "import")
1824 .map(|import_prop| {
1825 expression_to_string_or_array(&import_prop.value)
1826 })
1827 .unwrap_or_default()
1828 }
1829 _ => expression_to_path_string(&p.value).into_iter().collect(),
1830 }
1831 } else {
1832 Vec::new()
1833 }
1834 })
1835 .collect(),
1836 _ => expression_to_path_string(expr).into_iter().collect(),
1838 }
1839}
1840
1841fn collect_require_sources(expr: &Expression) -> Vec<String> {
1843 let mut sources = Vec::new();
1844 match expr {
1845 Expression::CallExpression(call) if is_require_call(call) => {
1846 if let Some(s) = get_require_source(call) {
1847 sources.push(s);
1848 }
1849 }
1850 Expression::ArrayExpression(arr) => {
1851 for el in &arr.elements {
1852 if let Some(inner) = el.as_expression() {
1853 match inner {
1854 Expression::CallExpression(call) if is_require_call(call) => {
1855 if let Some(s) = get_require_source(call) {
1856 sources.push(s);
1857 }
1858 }
1859 Expression::ArrayExpression(sub_arr) => {
1861 if let Some(first) = sub_arr.elements.first()
1862 && let Some(Expression::CallExpression(call)) =
1863 first.as_expression()
1864 && is_require_call(call)
1865 && let Some(s) = get_require_source(call)
1866 {
1867 sources.push(s);
1868 }
1869 }
1870 _ => {}
1871 }
1872 }
1873 }
1874 }
1875 _ => {}
1876 }
1877 sources
1878}
1879
1880fn is_require_call(call: &CallExpression) -> bool {
1882 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
1883}
1884
1885fn get_require_source(call: &CallExpression) -> Option<String> {
1887 call.arguments.first().and_then(|arg| {
1888 if let Argument::StringLiteral(s) = arg {
1889 Some(s.value.to_string())
1890 } else {
1891 None
1892 }
1893 })
1894}
1895
1896#[cfg(test)]
1897mod tests {
1898 use super::*;
1899 use std::path::PathBuf;
1900
1901 fn js_path() -> PathBuf {
1902 PathBuf::from("config.js")
1903 }
1904
1905 fn ts_path() -> PathBuf {
1906 PathBuf::from("config.ts")
1907 }
1908
1909 #[test]
1910 fn extract_lazy_imports_bare_arrows() {
1911 let source = r"
1912 import { defineConfig } from '@adonisjs/core/app'
1913 export default defineConfig({
1914 preloads: [
1915 () => import('#start/routes'),
1916 () => import('#start/kernel'),
1917 ],
1918 })
1919 ";
1920 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["preloads"]);
1921 assert_eq!(specs, vec!["#start/routes", "#start/kernel"]);
1922 }
1923
1924 #[test]
1925 fn extract_lazy_imports_object_form_with_file_key() {
1926 let source = r"
1927 export default defineConfig({
1928 providers: [
1929 () => import('@adonisjs/core/providers/app_provider'),
1930 {
1931 file: () => import('@adonisjs/core/providers/repl_provider'),
1932 environment: ['repl', 'test'],
1933 },
1934 ],
1935 })
1936 ";
1937 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
1938 assert_eq!(
1939 specs,
1940 vec![
1941 "@adonisjs/core/providers/app_provider",
1942 "@adonisjs/core/providers/repl_provider",
1943 ]
1944 );
1945 }
1946
1947 #[test]
1948 fn extract_lazy_imports_block_body_with_return() {
1949 let source = r"
1951 export default defineConfig({
1952 commands: [
1953 () => { return import('@adonisjs/core/commands') },
1954 ],
1955 })
1956 ";
1957 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
1958 assert_eq!(specs, vec!["@adonisjs/core/commands"]);
1959 }
1960
1961 #[test]
1962 fn extract_lazy_imports_skips_unknown_element_shapes() {
1963 let source = r"
1966 export default defineConfig({
1967 commands: [
1968 'string-entry',
1969 42,
1970 { other: 'value' },
1971 () => import('@adonisjs/lucid/commands'),
1972 ],
1973 })
1974 ";
1975 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
1976 assert_eq!(specs, vec!["@adonisjs/lucid/commands"]);
1977 }
1978
1979 #[test]
1980 fn extract_lazy_imports_missing_property_returns_empty() {
1981 let source = r"
1982 export default defineConfig({
1983 preloads: [() => import('#start/routes')],
1984 })
1985 ";
1986 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
1987 assert!(specs.is_empty());
1988 }
1989
1990 #[test]
1991 fn extract_imports_basic() {
1992 let source = r"
1993 import foo from 'foo-pkg';
1994 import { bar } from '@scope/bar';
1995 export default {};
1996 ";
1997 let imports = extract_imports(source, &js_path());
1998 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
1999 }
2000
2001 #[test]
2002 fn extract_default_export_object_property() {
2003 let source = r#"export default { testDir: "./tests" };"#;
2004 let val = extract_config_string(source, &js_path(), &["testDir"]);
2005 assert_eq!(val, Some("./tests".to_string()));
2006 }
2007
2008 #[test]
2009 fn extract_define_config_property() {
2010 let source = r#"
2011 import { defineConfig } from 'vitest/config';
2012 export default defineConfig({
2013 test: {
2014 include: ["**/*.test.ts", "**/*.spec.ts"],
2015 setupFiles: ["./test/setup.ts"]
2016 }
2017 });
2018 "#;
2019 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
2020 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
2021
2022 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
2023 assert_eq!(setup, vec!["./test/setup.ts"]);
2024 }
2025
2026 #[test]
2027 fn extract_module_exports_property() {
2028 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
2029 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
2030 assert_eq!(val, Some("jsdom".to_string()));
2031 }
2032
2033 #[test]
2034 fn extract_nested_string_array() {
2035 let source = r#"
2036 export default {
2037 resolve: {
2038 alias: {
2039 "@": "./src"
2040 }
2041 },
2042 test: {
2043 include: ["src/**/*.test.ts"]
2044 }
2045 };
2046 "#;
2047 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
2048 assert_eq!(include, vec!["src/**/*.test.ts"]);
2049 }
2050
2051 #[test]
2052 fn extract_addons_array() {
2053 let source = r#"
2054 export default {
2055 addons: [
2056 "@storybook/addon-a11y",
2057 "@storybook/addon-docs",
2058 "@storybook/addon-links"
2059 ]
2060 };
2061 "#;
2062 let addons = extract_config_property_strings(source, &ts_path(), "addons");
2063 assert_eq!(
2064 addons,
2065 vec![
2066 "@storybook/addon-a11y",
2067 "@storybook/addon-docs",
2068 "@storybook/addon-links"
2069 ]
2070 );
2071 }
2072
2073 #[test]
2074 fn handle_empty_config() {
2075 let source = "";
2076 let result = extract_config_string(source, &js_path(), &["key"]);
2077 assert_eq!(result, None);
2078 }
2079
2080 #[test]
2083 fn object_keys_postcss_plugins() {
2084 let source = r"
2085 module.exports = {
2086 plugins: {
2087 autoprefixer: {},
2088 tailwindcss: {},
2089 'postcss-import': {}
2090 }
2091 };
2092 ";
2093 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2094 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
2095 }
2096
2097 #[test]
2098 fn object_keys_nested_path() {
2099 let source = r"
2100 export default {
2101 build: {
2102 plugins: {
2103 minify: {},
2104 compress: {}
2105 }
2106 }
2107 };
2108 ";
2109 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
2110 assert_eq!(keys, vec!["minify", "compress"]);
2111 }
2112
2113 #[test]
2114 fn object_keys_empty_object() {
2115 let source = r"export default { plugins: {} };";
2116 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2117 assert!(keys.is_empty());
2118 }
2119
2120 #[test]
2121 fn object_keys_non_object_returns_empty() {
2122 let source = r#"export default { plugins: ["a", "b"] };"#;
2123 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2124 assert!(keys.is_empty());
2125 }
2126
2127 #[test]
2130 fn string_or_array_single_string() {
2131 let source = r#"export default { entry: "./src/index.js" };"#;
2132 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2133 assert_eq!(result, vec!["./src/index.js"]);
2134 }
2135
2136 #[test]
2137 fn string_or_array_array() {
2138 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
2139 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2140 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
2141 }
2142
2143 #[test]
2144 fn string_or_array_object_values() {
2145 let source =
2146 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
2147 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2148 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
2149 }
2150
2151 #[test]
2152 fn string_or_array_object_array_values() {
2153 let source = r#"export default { entry: { app: ["./src/polyfill.js", "./src/app.js"] } };"#;
2154 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2155 assert_eq!(result, vec!["./src/polyfill.js", "./src/app.js"]);
2156 }
2157
2158 #[test]
2159 fn string_or_array_webpack_entry_descriptors() {
2160 let source = r#"
2161 export default {
2162 entry: {
2163 app: {
2164 import: "./src/app.js",
2165 filename: "pages/app.js",
2166 dependOn: "shared",
2167 },
2168 admin: {
2169 import: ["./src/admin-polyfill.js", "./src/admin.js"],
2170 runtime: "runtime",
2171 },
2172 shared: ["react", "react-dom"],
2173 },
2174 };
2175 "#;
2176 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2177 assert_eq!(
2178 result,
2179 vec![
2180 "./src/app.js",
2181 "./src/admin-polyfill.js",
2182 "./src/admin.js",
2183 "react",
2184 "react-dom"
2185 ]
2186 );
2187 }
2188
2189 #[test]
2190 fn string_or_array_nested_path() {
2191 let source = r#"
2192 export default {
2193 build: {
2194 rollupOptions: {
2195 input: ["./index.html", "./about.html"]
2196 }
2197 }
2198 };
2199 "#;
2200 let result = extract_config_string_or_array(
2201 source,
2202 &js_path(),
2203 &["build", "rollupOptions", "input"],
2204 );
2205 assert_eq!(result, vec!["./index.html", "./about.html"]);
2206 }
2207
2208 #[test]
2209 fn string_or_array_template_literal() {
2210 let source = r"export default { entry: `./src/index.js` };";
2211 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2212 assert_eq!(result, vec!["./src/index.js"]);
2213 }
2214
2215 #[test]
2216 fn string_or_array_object_path_helper_values() {
2217 let source = r#"
2220 import { resolve, join } from "node:path";
2221 import path from "node:path";
2222 export default {
2223 build: {
2224 rollupOptions: {
2225 input: {
2226 app: resolve(__dirname, "src/app.ts"),
2227 modal: path.resolve(__dirname, "src/modal.ts"),
2228 tabs: join(__dirname, "src/tabs.ts"),
2229 styles: resolve(__dirname, "src/index.css"),
2230 },
2231 },
2232 },
2233 };
2234 "#;
2235 let result = extract_config_string_or_array(
2236 source,
2237 &js_path(),
2238 &["build", "rollupOptions", "input"],
2239 );
2240 assert_eq!(
2241 result,
2242 vec!["src/app.ts", "src/modal.ts", "src/tabs.ts", "src/index.css"]
2243 );
2244 }
2245
2246 #[test]
2247 fn string_or_array_array_path_helper_values() {
2248 let source = r#"
2249 import { resolve } from "node:path";
2250 export default {
2251 build: {
2252 rollupOptions: {
2253 input: [resolve(__dirname, "src/a.ts"), "./src/b.ts"],
2254 },
2255 },
2256 };
2257 "#;
2258 let result = extract_config_string_or_array(
2259 source,
2260 &js_path(),
2261 &["build", "rollupOptions", "input"],
2262 );
2263 assert_eq!(result, vec!["src/a.ts", "./src/b.ts"]);
2264 }
2265
2266 #[test]
2267 fn string_or_array_top_level_path_helper_call() {
2268 let source = r#"
2269 import { resolve } from "node:path";
2270 export default { build: { lib: { entry: resolve(__dirname, "src/index.ts") } } };
2271 "#;
2272 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
2273 assert_eq!(result, vec!["src/index.ts"]);
2274 }
2275
2276 #[test]
2277 fn string_or_array_import_meta_dirname_anchor() {
2278 let source = r#"
2279 import { resolve } from "node:path";
2280 export default {
2281 build: { lib: { entry: resolve(import.meta.dirname, "src/index.ts") } },
2282 };
2283 "#;
2284 let result = extract_config_string_or_array(source, &ts_path(), &["build", "lib", "entry"]);
2285 assert_eq!(result, vec!["src/index.ts"]);
2286 }
2287
2288 #[test]
2289 fn string_or_array_non_literal_path_helper_args_dropped() {
2290 let source = r#"
2293 import { resolve } from "node:path";
2294 export default { build: { lib: { entry: resolve(baseDir, "src/index.ts") } } };
2295 "#;
2296 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
2297 assert!(
2298 result.is_empty(),
2299 "non-literal path-helper args must be dropped: {result:?}"
2300 );
2301 }
2302
2303 #[test]
2306 fn require_strings_array() {
2307 let source = r"
2308 module.exports = {
2309 plugins: [
2310 require('autoprefixer'),
2311 require('postcss-import')
2312 ]
2313 };
2314 ";
2315 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2316 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
2317 }
2318
2319 #[test]
2320 fn require_strings_with_tuples() {
2321 let source = r"
2322 module.exports = {
2323 plugins: [
2324 require('autoprefixer'),
2325 [require('postcss-preset-env'), { stage: 3 }]
2326 ]
2327 };
2328 ";
2329 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2330 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
2331 }
2332
2333 #[test]
2334 fn require_strings_empty_array() {
2335 let source = r"module.exports = { plugins: [] };";
2336 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2337 assert!(deps.is_empty());
2338 }
2339
2340 #[test]
2341 fn require_strings_no_require_calls() {
2342 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
2343 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2344 assert!(deps.is_empty());
2345 }
2346
2347 #[test]
2348 fn extract_aliases_from_object_with_file_url_to_path() {
2349 let source = r#"
2350 import { defineConfig } from 'vite';
2351 import { fileURLToPath, URL } from 'node:url';
2352
2353 export default defineConfig({
2354 resolve: {
2355 alias: {
2356 "@": fileURLToPath(new URL("./src", import.meta.url))
2357 }
2358 }
2359 });
2360 "#;
2361
2362 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
2363 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
2364 }
2365
2366 #[test]
2367 fn extract_aliases_from_array_form() {
2368 let source = r#"
2369 export default {
2370 resolve: {
2371 alias: [
2372 { find: "@", replacement: "./src" },
2373 { find: "$utils", replacement: "src/lib/utils" }
2374 ]
2375 }
2376 };
2377 "#;
2378
2379 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
2380 assert_eq!(
2381 aliases,
2382 vec![
2383 ("@".to_string(), "./src".to_string()),
2384 ("$utils".to_string(), "src/lib/utils".to_string())
2385 ]
2386 );
2387 }
2388
2389 #[test]
2390 fn extract_aliases_from_object_with_array_values() {
2391 let source = r#"
2392 ({
2393 compilerOptions: {
2394 paths: {
2395 "@/*": ["./src/*"],
2396 "@shared/*": ["./shared/*", "./fallback/*"]
2397 }
2398 }
2399 })
2400 "#;
2401
2402 let aliases = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
2403 assert_eq!(
2404 aliases,
2405 vec![
2406 ("@/*".to_string(), "./src/*".to_string()),
2407 ("@shared/*".to_string(), "./shared/*".to_string())
2408 ]
2409 );
2410 }
2411
2412 #[test]
2413 fn extract_array_object_strings_mixed_forms() {
2414 let source = r#"
2415 export default {
2416 components: [
2417 "~/components",
2418 { path: "@/feature-components" }
2419 ]
2420 };
2421 "#;
2422
2423 let values =
2424 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
2425 assert_eq!(
2426 values,
2427 vec![
2428 "~/components".to_string(),
2429 "@/feature-components".to_string()
2430 ]
2431 );
2432 }
2433
2434 #[test]
2435 fn extract_array_object_string_pairs_with_and_without_secondary() {
2436 let source = r#"
2437 export default {
2438 webServer: [
2439 { command: "tsx scripts/api.ts", cwd: "packages/api" },
2440 { command: "tsx scripts/web.ts" }
2441 ]
2442 };
2443 "#;
2444
2445 let pairs = extract_config_array_object_string_pairs(
2446 source,
2447 &ts_path(),
2448 &["webServer"],
2449 "command",
2450 "cwd",
2451 );
2452 assert_eq!(
2453 pairs,
2454 vec![
2455 (
2456 "tsx scripts/api.ts".to_string(),
2457 Some("packages/api".to_string())
2458 ),
2459 ("tsx scripts/web.ts".to_string(), None),
2460 ]
2461 );
2462 }
2463
2464 #[test]
2465 fn extract_array_object_string_pairs_skips_elements_missing_primary() {
2466 let source = r#"
2467 export default {
2468 webServer: [
2469 { cwd: "packages/api" },
2470 { command: "srvx --port 3000" }
2471 ]
2472 };
2473 "#;
2474
2475 let pairs = extract_config_array_object_string_pairs(
2476 source,
2477 &ts_path(),
2478 &["webServer"],
2479 "command",
2480 "cwd",
2481 );
2482 assert_eq!(pairs, vec![("srvx --port 3000".to_string(), None)]);
2483 }
2484
2485 #[test]
2486 fn extract_array_object_string_pairs_empty_for_object_form() {
2487 let source = r#"
2490 export default {
2491 webServer: { command: "srvx --port 3000" }
2492 };
2493 "#;
2494
2495 let pairs = extract_config_array_object_string_pairs(
2496 source,
2497 &ts_path(),
2498 &["webServer"],
2499 "command",
2500 "cwd",
2501 );
2502 assert!(pairs.is_empty());
2503 }
2504
2505 #[test]
2506 fn extract_config_plugin_option_string_from_json() {
2507 let source = r#"{
2508 "expo": {
2509 "plugins": [
2510 ["expo-router", { "root": "src/app" }]
2511 ]
2512 }
2513 }"#;
2514
2515 let value = extract_config_plugin_option_string(
2516 source,
2517 &json_path(),
2518 &["expo", "plugins"],
2519 "expo-router",
2520 "root",
2521 );
2522
2523 assert_eq!(value, Some("src/app".to_string()));
2524 }
2525
2526 #[test]
2527 fn extract_config_plugin_option_string_from_top_level_plugins() {
2528 let source = r#"{
2529 "plugins": [
2530 ["expo-router", { "root": "./src/routes" }]
2531 ]
2532 }"#;
2533
2534 let value = extract_config_plugin_option_string_from_paths(
2535 source,
2536 &json_path(),
2537 &[&["plugins"], &["expo", "plugins"]],
2538 "expo-router",
2539 "root",
2540 );
2541
2542 assert_eq!(value, Some("./src/routes".to_string()));
2543 }
2544
2545 #[test]
2546 fn extract_config_plugin_option_string_from_ts_config() {
2547 let source = r"
2548 export default {
2549 expo: {
2550 plugins: [
2551 ['expo-router', { root: './src/app' }]
2552 ]
2553 }
2554 };
2555 ";
2556
2557 let value = extract_config_plugin_option_string(
2558 source,
2559 &ts_path(),
2560 &["expo", "plugins"],
2561 "expo-router",
2562 "root",
2563 );
2564
2565 assert_eq!(value, Some("./src/app".to_string()));
2566 }
2567
2568 #[test]
2569 fn extract_config_plugin_option_string_returns_none_when_plugin_missing() {
2570 let source = r#"{
2571 "expo": {
2572 "plugins": [
2573 ["expo-font", {}]
2574 ]
2575 }
2576 }"#;
2577
2578 let value = extract_config_plugin_option_string(
2579 source,
2580 &json_path(),
2581 &["expo", "plugins"],
2582 "expo-router",
2583 "root",
2584 );
2585
2586 assert_eq!(value, None);
2587 }
2588
2589 #[test]
2590 fn vite_react_babel_dependencies_extract_plain_tuple_and_prefixed_entries() {
2591 let source = r#"
2592 import react from "@vitejs/plugin-react";
2593
2594 export default defineConfig({
2595 plugins: [
2596 react({
2597 babel: {
2598 plugins: [
2599 "babel-plugin-plain",
2600 ["module:@preact/signals-react-transform", { mode: "auto" }],
2601 ],
2602 presets: [["@babel/preset-react", { runtime: "automatic" }]],
2603 },
2604 }),
2605 ],
2606 });
2607 "#;
2608
2609 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
2610
2611 assert_eq!(
2612 deps,
2613 vec![
2614 "babel-plugin-plain".to_string(),
2615 "@preact/signals-react-transform".to_string(),
2616 "@babel/preset-react".to_string(),
2617 ]
2618 );
2619 }
2620
2621 #[test]
2622 fn vite_react_babel_dependencies_support_default_alias_import() {
2623 let source = r#"
2624 import { default as viteReact } from "@vitejs/plugin-react";
2625
2626 export default {
2627 plugins: [
2628 viteReact({
2629 babel: {
2630 plugins: [["module:@scope/pkg/plugin", {}]],
2631 },
2632 }),
2633 ],
2634 };
2635 "#;
2636
2637 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
2638
2639 assert_eq!(deps, vec!["@scope/pkg".to_string()]);
2640 }
2641
2642 #[test]
2643 fn vite_react_babel_dependencies_ignore_unrelated_plugin_calls() {
2644 let source = r#"
2645 import vue from "@vitejs/plugin-vue";
2646
2647 export default {
2648 plugins: [
2649 vue({
2650 babel: {
2651 plugins: ["@preact/signals-react-transform"],
2652 },
2653 }),
2654 ],
2655 };
2656 "#;
2657
2658 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
2659
2660 assert!(deps.is_empty());
2661 }
2662
2663 #[test]
2664 fn vite_react_babel_dependencies_skip_relative_and_protocol_entries() {
2665 let source = r#"
2666 import react from "@vitejs/plugin-react";
2667
2668 export default {
2669 plugins: [
2670 react({
2671 babel: {
2672 plugins: ["./local-plugin", "module:./local-prefixed", "http://example.com/plugin"],
2673 },
2674 }),
2675 ],
2676 };
2677 "#;
2678
2679 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
2680
2681 assert!(deps.is_empty());
2682 }
2683
2684 #[test]
2685 fn normalize_config_path_relative_to_root() {
2686 let config_path = PathBuf::from("/project/vite.config.ts");
2687 let root = PathBuf::from("/project");
2688
2689 assert_eq!(
2690 normalize_config_path("./src/lib", &config_path, &root),
2691 Some("src/lib".to_string())
2692 );
2693 assert_eq!(
2694 normalize_config_path("/src/lib", &config_path, &root),
2695 Some("src/lib".to_string())
2696 );
2697 }
2698
2699 #[test]
2702 fn json_wrapped_in_parens_string() {
2703 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
2704 let val = extract_config_string(source, &js_path(), &["extends"]);
2705 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
2706 }
2707
2708 #[test]
2709 fn json_wrapped_in_parens_nested_array() {
2710 let source =
2711 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
2712 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
2713 assert_eq!(types, vec!["node", "jest"]);
2714
2715 let include = extract_config_string_array(source, &js_path(), &["include"]);
2716 assert_eq!(include, vec!["src/**/*"]);
2717 }
2718
2719 #[test]
2720 fn json_wrapped_in_parens_object_keys() {
2721 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
2722 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2723 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
2724 }
2725
2726 fn json_path() -> PathBuf {
2729 PathBuf::from("config.json")
2730 }
2731
2732 #[test]
2733 fn json_file_parsed_correctly() {
2734 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
2735 let val = extract_config_string(source, &json_path(), &["key"]);
2736 assert_eq!(val, Some("value".to_string()));
2737
2738 let list = extract_config_string_array(source, &json_path(), &["list"]);
2739 assert_eq!(list, vec!["a", "b"]);
2740 }
2741
2742 #[test]
2743 fn jsonc_file_parsed_correctly() {
2744 let source = r#"{"key": "value"}"#;
2745 let path = PathBuf::from("tsconfig.jsonc");
2746 let val = extract_config_string(source, &path, &["key"]);
2747 assert_eq!(val, Some("value".to_string()));
2748 }
2749
2750 #[test]
2753 fn extract_define_config_arrow_function() {
2754 let source = r#"
2755 import { defineConfig } from 'vite';
2756 export default defineConfig(() => ({
2757 test: {
2758 include: ["**/*.test.ts"]
2759 }
2760 }));
2761 "#;
2762 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
2763 assert_eq!(include, vec!["**/*.test.ts"]);
2764 }
2765
2766 #[test]
2767 fn extract_config_from_default_export_function_declaration() {
2768 let source = r#"
2769 export default function createConfig() {
2770 return {
2771 clientModules: ["./src/client/global.js"]
2772 };
2773 }
2774 "#;
2775
2776 let client_modules = extract_config_string_array(source, &ts_path(), &["clientModules"]);
2777 assert_eq!(client_modules, vec!["./src/client/global.js"]);
2778 }
2779
2780 #[test]
2781 fn extract_config_from_default_export_async_function_declaration() {
2782 let source = r#"
2783 export default async function createConfigAsync() {
2784 return {
2785 docs: {
2786 path: "knowledge"
2787 }
2788 };
2789 }
2790 "#;
2791
2792 let docs_path = extract_config_string(source, &ts_path(), &["docs", "path"]);
2793 assert_eq!(docs_path, Some("knowledge".to_string()));
2794 }
2795
2796 #[test]
2797 fn extract_config_from_exported_arrow_function_identifier() {
2798 let source = r#"
2799 const config = async () => {
2800 return {
2801 themes: ["classic"]
2802 };
2803 };
2804
2805 export default config;
2806 "#;
2807
2808 let themes = extract_config_shallow_strings(source, &ts_path(), "themes");
2809 assert_eq!(themes, vec!["classic"]);
2810 }
2811
2812 #[test]
2815 fn module_exports_nested_string() {
2816 let source = r#"
2817 module.exports = {
2818 resolve: {
2819 alias: {
2820 "@": "./src"
2821 }
2822 }
2823 };
2824 "#;
2825 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
2826 assert_eq!(val, Some("./src".to_string()));
2827 }
2828
2829 #[test]
2832 fn property_strings_nested_objects() {
2833 let source = r#"
2834 export default {
2835 plugins: {
2836 group1: { a: "val-a" },
2837 group2: { b: "val-b" }
2838 }
2839 };
2840 "#;
2841 let values = extract_config_property_strings(source, &js_path(), "plugins");
2842 assert!(values.contains(&"val-a".to_string()));
2843 assert!(values.contains(&"val-b".to_string()));
2844 }
2845
2846 #[test]
2847 fn property_strings_missing_key_returns_empty() {
2848 let source = r#"export default { other: "value" };"#;
2849 let values = extract_config_property_strings(source, &js_path(), "missing");
2850 assert!(values.is_empty());
2851 }
2852
2853 #[test]
2856 fn shallow_strings_tuple_array() {
2857 let source = r#"
2858 module.exports = {
2859 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
2860 };
2861 "#;
2862 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
2863 assert_eq!(values, vec!["default", "jest-junit"]);
2864 assert!(!values.contains(&"reports".to_string()));
2866 }
2867
2868 #[test]
2869 fn shallow_strings_single_string() {
2870 let source = r#"export default { preset: "ts-jest" };"#;
2871 let values = extract_config_shallow_strings(source, &js_path(), "preset");
2872 assert_eq!(values, vec!["ts-jest"]);
2873 }
2874
2875 #[test]
2876 fn shallow_strings_missing_key() {
2877 let source = r#"export default { other: "val" };"#;
2878 let values = extract_config_shallow_strings(source, &js_path(), "missing");
2879 assert!(values.is_empty());
2880 }
2881
2882 #[test]
2883 fn shallow_strings_or_object_property_alias_objects() {
2884 let source = r#"
2885 export default {
2886 jsPlugins: [
2887 "eslint-plugin-playwright",
2888 ["eslint-plugin-regexp", { rules: {} }],
2889 { name: "short", specifier: "eslint-plugin-with-long-name" }
2890 ]
2891 };
2892 "#;
2893 let values = extract_config_shallow_strings_or_object_property(
2894 source,
2895 &ts_path(),
2896 "jsPlugins",
2897 "specifier",
2898 );
2899 assert_eq!(
2900 values,
2901 vec![
2902 "eslint-plugin-playwright",
2903 "eslint-plugin-regexp",
2904 "eslint-plugin-with-long-name"
2905 ]
2906 );
2907 }
2908
2909 #[test]
2912 fn nested_shallow_strings_vitest_reporters() {
2913 let source = r#"
2914 export default {
2915 test: {
2916 reporters: ["default", "vitest-sonar-reporter"]
2917 }
2918 };
2919 "#;
2920 let values =
2921 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2922 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
2923 }
2924
2925 #[test]
2926 fn nested_shallow_strings_tuple_format() {
2927 let source = r#"
2928 export default {
2929 test: {
2930 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
2931 }
2932 };
2933 "#;
2934 let values =
2935 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2936 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
2937 }
2938
2939 #[test]
2940 fn nested_shallow_strings_missing_outer() {
2941 let source = r"export default { other: {} };";
2942 let values =
2943 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2944 assert!(values.is_empty());
2945 }
2946
2947 #[test]
2948 fn nested_shallow_strings_missing_inner() {
2949 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
2950 let values =
2951 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
2952 assert!(values.is_empty());
2953 }
2954
2955 #[test]
2958 fn string_or_array_missing_path() {
2959 let source = r"export default {};";
2960 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2961 assert!(result.is_empty());
2962 }
2963
2964 #[test]
2965 fn string_or_array_non_string_values() {
2966 let source = r"export default { entry: [42, true] };";
2968 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2969 assert!(result.is_empty());
2970 }
2971
2972 #[test]
2975 fn array_nested_extraction() {
2976 let source = r#"
2977 export default defineConfig({
2978 test: {
2979 projects: [
2980 {
2981 test: {
2982 setupFiles: ["./test/setup-a.ts"]
2983 }
2984 },
2985 {
2986 test: {
2987 setupFiles: "./test/setup-b.ts"
2988 }
2989 }
2990 ]
2991 }
2992 });
2993 "#;
2994 let results = extract_config_array_nested_string_or_array(
2995 source,
2996 &ts_path(),
2997 &["test", "projects"],
2998 &["test", "setupFiles"],
2999 );
3000 assert!(results.contains(&"./test/setup-a.ts".to_string()));
3001 assert!(results.contains(&"./test/setup-b.ts".to_string()));
3002 }
3003
3004 #[test]
3005 fn array_nested_empty_when_no_array() {
3006 let source = r#"export default { test: { projects: "not-an-array" } };"#;
3007 let results = extract_config_array_nested_string_or_array(
3008 source,
3009 &js_path(),
3010 &["test", "projects"],
3011 &["test", "setupFiles"],
3012 );
3013 assert!(results.is_empty());
3014 }
3015
3016 #[test]
3019 fn object_nested_extraction() {
3020 let source = r#"{
3021 "projects": {
3022 "app-one": {
3023 "architect": {
3024 "build": {
3025 "options": {
3026 "styles": ["src/styles.css"]
3027 }
3028 }
3029 }
3030 }
3031 }
3032 }"#;
3033 let results = extract_config_object_nested_string_or_array(
3034 source,
3035 &json_path(),
3036 &["projects"],
3037 &["architect", "build", "options", "styles"],
3038 );
3039 assert_eq!(results, vec!["src/styles.css"]);
3040 }
3041
3042 #[test]
3043 fn array_with_object_input_form_extracted() {
3044 let source = r#"{
3050 "projects": {
3051 "app": {
3052 "architect": {
3053 "build": {
3054 "options": {
3055 "styles": [
3056 "src/styles.scss",
3057 { "input": "src/theme.scss", "bundleName": "theme", "inject": false },
3058 { "bundleName": "lazy-only" }
3059 ]
3060 }
3061 }
3062 }
3063 }
3064 }
3065 }"#;
3066 let results = extract_config_object_nested_string_or_array(
3067 source,
3068 &json_path(),
3069 &["projects"],
3070 &["architect", "build", "options", "styles"],
3071 );
3072 assert!(
3073 results.contains(&"src/styles.scss".to_string()),
3074 "string form must still work: {results:?}"
3075 );
3076 assert!(
3077 results.contains(&"src/theme.scss".to_string()),
3078 "object form with `input` must be extracted: {results:?}"
3079 );
3080 assert!(
3083 !results.contains(&"lazy-only".to_string()),
3084 "bundleName must not be misinterpreted as a path: {results:?}"
3085 );
3086 assert!(
3087 !results.contains(&"theme".to_string()),
3088 "bundleName from full object must not leak: {results:?}"
3089 );
3090 }
3091
3092 #[test]
3095 fn object_nested_strings_extraction() {
3096 let source = r#"{
3097 "targets": {
3098 "build": {
3099 "executor": "@angular/build:application"
3100 },
3101 "test": {
3102 "executor": "@nx/vite:test"
3103 }
3104 }
3105 }"#;
3106 let results =
3107 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
3108 assert!(results.contains(&"@angular/build:application".to_string()));
3109 assert!(results.contains(&"@nx/vite:test".to_string()));
3110 }
3111
3112 #[test]
3115 fn require_strings_direct_call() {
3116 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
3117 let deps = extract_config_require_strings(source, &js_path(), "adapter");
3118 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
3119 }
3120
3121 #[test]
3122 fn require_strings_no_matching_key() {
3123 let source = r"module.exports = { other: require('something') };";
3124 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3125 assert!(deps.is_empty());
3126 }
3127
3128 #[test]
3131 fn extract_imports_no_imports() {
3132 let source = r"export default {};";
3133 let imports = extract_imports(source, &js_path());
3134 assert!(imports.is_empty());
3135 }
3136
3137 #[test]
3138 fn extract_imports_side_effect_import() {
3139 let source = r"
3140 import 'polyfill';
3141 import './local-setup';
3142 export default {};
3143 ";
3144 let imports = extract_imports(source, &js_path());
3145 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
3146 }
3147
3148 #[test]
3149 fn extract_imports_mixed_specifiers() {
3150 let source = r"
3151 import defaultExport from 'module-a';
3152 import { named } from 'module-b';
3153 import * as ns from 'module-c';
3154 export default {};
3155 ";
3156 let imports = extract_imports(source, &js_path());
3157 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
3158 }
3159
3160 #[test]
3163 fn template_literal_in_string_or_array() {
3164 let source = r"export default { entry: `./src/index.ts` };";
3165 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
3166 assert_eq!(result, vec!["./src/index.ts"]);
3167 }
3168
3169 #[test]
3170 fn template_literal_in_config_string() {
3171 let source = r"export default { testDir: `./tests` };";
3172 let val = extract_config_string(source, &js_path(), &["testDir"]);
3173 assert_eq!(val, Some("./tests".to_string()));
3174 }
3175
3176 #[test]
3179 fn nested_string_array_empty_path() {
3180 let source = r#"export default { items: ["a", "b"] };"#;
3181 let result = extract_config_string_array(source, &js_path(), &[]);
3182 assert!(result.is_empty());
3183 }
3184
3185 #[test]
3186 fn nested_string_empty_path() {
3187 let source = r#"export default { key: "val" };"#;
3188 let result = extract_config_string(source, &js_path(), &[]);
3189 assert!(result.is_none());
3190 }
3191
3192 #[test]
3193 fn object_keys_empty_path() {
3194 let source = r"export default { plugins: {} };";
3195 let result = extract_config_object_keys(source, &js_path(), &[]);
3196 assert!(result.is_empty());
3197 }
3198
3199 #[test]
3202 fn no_config_object_returns_empty() {
3203 let source = r"const x = 42;";
3205 let result = extract_config_string(source, &js_path(), &["key"]);
3206 assert!(result.is_none());
3207
3208 let arr = extract_config_string_array(source, &js_path(), &["items"]);
3209 assert!(arr.is_empty());
3210
3211 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3212 assert!(keys.is_empty());
3213 }
3214
3215 #[test]
3218 fn property_with_string_key() {
3219 let source = r#"export default { "string-key": "value" };"#;
3220 let val = extract_config_string(source, &js_path(), &["string-key"]);
3221 assert_eq!(val, Some("value".to_string()));
3222 }
3223
3224 #[test]
3225 fn nested_navigation_through_non_object() {
3226 let source = r#"export default { level1: "not-an-object" };"#;
3228 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
3229 assert!(val.is_none());
3230 }
3231
3232 #[test]
3235 fn variable_reference_untyped() {
3236 let source = r#"
3237 const config = {
3238 testDir: "./tests"
3239 };
3240 export default config;
3241 "#;
3242 let val = extract_config_string(source, &js_path(), &["testDir"]);
3243 assert_eq!(val, Some("./tests".to_string()));
3244 }
3245
3246 #[test]
3247 fn variable_reference_with_type_annotation() {
3248 let source = r#"
3249 import type { StorybookConfig } from '@storybook/react-vite';
3250 const config: StorybookConfig = {
3251 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
3252 framework: "@storybook/react-vite"
3253 };
3254 export default config;
3255 "#;
3256 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
3257 assert_eq!(
3258 addons,
3259 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
3260 );
3261
3262 let framework = extract_config_string(source, &ts_path(), &["framework"]);
3263 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
3264 }
3265
3266 #[test]
3267 fn variable_reference_with_define_config() {
3268 let source = r#"
3269 import { defineConfig } from 'vitest/config';
3270 const config = defineConfig({
3271 test: {
3272 include: ["**/*.test.ts"]
3273 }
3274 });
3275 export default config;
3276 "#;
3277 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
3278 assert_eq!(include, vec!["**/*.test.ts"]);
3279 }
3280
3281 #[test]
3284 fn ts_satisfies_direct_export() {
3285 let source = r#"
3286 export default {
3287 testDir: "./tests"
3288 } satisfies PlaywrightTestConfig;
3289 "#;
3290 let val = extract_config_string(source, &ts_path(), &["testDir"]);
3291 assert_eq!(val, Some("./tests".to_string()));
3292 }
3293
3294 #[test]
3295 fn ts_as_direct_export() {
3296 let source = r#"
3297 export default {
3298 testDir: "./tests"
3299 } as const;
3300 "#;
3301 let val = extract_config_string(source, &ts_path(), &["testDir"]);
3302 assert_eq!(val, Some("./tests".to_string()));
3303 }
3304}