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_parser::Parser;
12use oxc_span::SourceType;
13use rustc_hash::FxHashSet;
14
15#[must_use]
17pub fn extract_imports(source: &str, path: &Path) -> Vec<String> {
18 extract_from_source(source, path, |program| {
19 let mut sources = Vec::new();
20 for stmt in &program.body {
21 if let Statement::ImportDeclaration(decl) = stmt {
22 sources.push(decl.source.value.to_string());
23 }
24 }
25 Some(sources)
26 })
27 .unwrap_or_default()
28}
29
30#[must_use]
32pub fn extract_imports_and_requires(source: &str, path: &Path) -> Vec<String> {
33 extract_from_source(source, path, |program| {
34 let mut sources = Vec::new();
35 for stmt in &program.body {
36 match stmt {
37 Statement::ImportDeclaration(decl) => {
38 sources.push(decl.source.value.to_string());
39 }
40 Statement::ExpressionStatement(expr) => {
41 if let Expression::CallExpression(call) = &expr.expression
42 && is_require_call(call)
43 && let Some(s) = get_require_source(call)
44 {
45 sources.push(s);
46 }
47 }
48 _ => {}
49 }
50 }
51 Some(sources)
52 })
53 .unwrap_or_default()
54}
55
56#[must_use]
58pub fn extract_config_string_array(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
59 extract_from_source(source, path, |program| {
60 let obj = find_config_object(program)?;
61 get_nested_string_array_from_object(obj, prop_path)
62 })
63 .unwrap_or_default()
64}
65
66#[must_use]
68pub fn extract_config_string(source: &str, path: &Path, prop_path: &[&str]) -> Option<String> {
69 extract_from_source(source, path, |program| {
70 let obj = find_config_object(program)?;
71 get_nested_string_from_object(obj, prop_path)
72 })
73}
74
75#[must_use]
77pub fn extract_config_command(source: &str, path: &Path, prop_path: &[&str]) -> Option<String> {
78 extract_from_source(source, path, |program| {
79 let obj = find_config_object(program)?;
80 get_nested_command_from_object(obj, prop_path)
81 })
82}
83
84#[must_use]
87pub fn extract_config_property_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
88 extract_from_source(source, path, |program| {
89 let obj = find_config_object(program)?;
90 let mut values = Vec::new();
91 if let Some(prop) = find_property(obj, key) {
92 collect_all_string_values(&prop.value, &mut values);
93 }
94 Some(values)
95 })
96 .unwrap_or_default()
97}
98
99#[must_use]
101pub fn extract_config_shallow_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
102 extract_from_source(source, path, |program| {
103 let obj = find_config_object(program)?;
104 let prop = find_property(obj, key)?;
105 Some(collect_shallow_string_values(&prop.value))
106 })
107 .unwrap_or_default()
108}
109
110#[must_use]
112pub fn extract_config_shallow_strings_or_object_property(
113 source: &str,
114 path: &Path,
115 key: &str,
116 object_property: &str,
117) -> Vec<String> {
118 extract_from_source(source, path, |program| {
119 let obj = find_config_object(program)?;
120 let prop = find_property(obj, key)?;
121 Some(collect_shallow_string_or_object_property_values(
122 &prop.value,
123 object_property,
124 ))
125 })
126 .unwrap_or_default()
127}
128
129#[must_use]
131pub fn extract_config_nested_shallow_strings(
132 source: &str,
133 path: &Path,
134 outer_path: &[&str],
135 key: &str,
136) -> Vec<String> {
137 extract_from_source(source, path, |program| {
138 let obj = find_config_object(program)?;
139 let nested = get_nested_expression(obj, outer_path)?;
140 if let Expression::ObjectExpression(nested_obj) = nested {
141 let prop = find_property(nested_obj, key)?;
142 Some(collect_shallow_string_values(&prop.value))
143 } else {
144 None
145 }
146 })
147 .unwrap_or_default()
148}
149
150pub fn find_config_object_pub<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
152 find_config_object(program)
153}
154
155pub(crate) fn property_expr<'a>(
157 obj: &'a ObjectExpression<'a>,
158 key: &str,
159) -> Option<&'a Expression<'a>> {
160 find_property(obj, key).map(|prop| &prop.value)
161}
162
163pub(crate) fn property_object<'a>(
165 obj: &'a ObjectExpression<'a>,
166 key: &str,
167) -> Option<&'a ObjectExpression<'a>> {
168 property_expr(obj, key).and_then(object_expression)
169}
170
171pub(crate) fn property_string(obj: &ObjectExpression<'_>, key: &str) -> Option<String> {
173 property_expr(obj, key).and_then(expression_to_string)
174}
175
176pub(crate) fn object_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ObjectExpression<'a>> {
178 match expr {
179 Expression::ObjectExpression(obj) => Some(obj),
180 Expression::ParenthesizedExpression(paren) => object_expression(&paren.expression),
181 Expression::TSSatisfiesExpression(ts_sat) => object_expression(&ts_sat.expression),
182 Expression::TSAsExpression(ts_as) => object_expression(&ts_as.expression),
183 _ => None,
184 }
185}
186
187pub(crate) fn array_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
189 match expr {
190 Expression::ArrayExpression(arr) => Some(arr),
191 Expression::ParenthesizedExpression(paren) => array_expression(&paren.expression),
192 Expression::TSSatisfiesExpression(ts_sat) => array_expression(&ts_sat.expression),
193 Expression::TSAsExpression(ts_as) => array_expression(&ts_as.expression),
194 _ => None,
195 }
196}
197
198pub(crate) fn path_from_config_string(raw: &str) -> PathBuf {
201 PathBuf::from(raw.replace('\\', "/"))
202}
203
204pub(crate) fn path_to_config_string(path: &Path) -> String {
206 path.to_string_lossy().replace('\\', "/")
207}
208
209pub(crate) fn expression_to_path(expr: &Expression<'_>) -> Option<PathBuf> {
211 expression_to_path_string(expr).map(|path| path_from_config_string(&path))
212}
213
214pub(crate) fn expression_to_path_values(expr: &Expression<'_>) -> Vec<PathBuf> {
216 match expr {
217 Expression::ArrayExpression(arr) => arr
218 .elements
219 .iter()
220 .filter_map(|element| element.as_expression().and_then(expression_to_path))
221 .collect(),
222 _ => expression_to_path(expr).into_iter().collect(),
223 }
224}
225
226pub(crate) fn is_disabled_expression(expr: &Expression<'_>) -> bool {
228 matches!(expr, Expression::BooleanLiteral(boolean) if !boolean.value)
229 || matches!(expr, Expression::NullLiteral(_))
230}
231
232#[must_use]
234pub fn extract_config_truthy_bool_or_object(source: &str, path: &Path, prop_path: &[&str]) -> bool {
235 extract_from_source(source, path, |program| {
236 let obj = find_config_object(program)?;
237 let expr = get_nested_expression(obj, prop_path)?;
238 Some(is_truthy_bool_or_object(expr))
239 })
240 .unwrap_or(false)
241}
242
243fn is_truthy_bool_or_object(expr: &Expression<'_>) -> bool {
244 match expr {
245 Expression::BooleanLiteral(boolean) => boolean.value,
246 Expression::ObjectExpression(_) => true,
247 Expression::ParenthesizedExpression(paren) => is_truthy_bool_or_object(&paren.expression),
248 Expression::TSSatisfiesExpression(ts_sat) => is_truthy_bool_or_object(&ts_sat.expression),
249 Expression::TSAsExpression(ts_as) => is_truthy_bool_or_object(&ts_as.expression),
250 _ => false,
251 }
252}
253
254#[must_use]
256pub fn extract_config_object_keys(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
257 extract_from_source(source, path, |program| {
258 let obj = find_config_object(program)?;
259 get_nested_object_keys(obj, prop_path)
260 })
261 .unwrap_or_default()
262}
263
264#[must_use]
267pub fn extract_config_string_or_array(
268 source: &str,
269 path: &Path,
270 prop_path: &[&str],
271) -> Vec<String> {
272 extract_from_source(source, path, |program| {
273 let obj = find_config_object(program)?;
274 get_nested_string_or_array(obj, prop_path)
275 })
276 .unwrap_or_default()
277}
278
279#[must_use]
281pub fn extract_config_path(source: &str, path: &Path, prop_path: &[&str]) -> Option<PathBuf> {
282 extract_from_source(source, path, |program| {
283 let obj = find_config_object(program)?;
284 let expr = get_nested_expression(obj, prop_path)?;
285 expression_to_path(expr)
286 })
287}
288
289#[must_use]
291pub fn extract_config_array_nested_string_or_array(
292 source: &str,
293 path: &Path,
294 array_path: &[&str],
295 inner_path: &[&str],
296) -> Vec<String> {
297 extract_from_source(source, path, |program| {
298 let obj = find_config_object(program)?;
299 let array_expr = get_nested_expression(obj, array_path)?;
300 let Expression::ArrayExpression(arr) = array_expr else {
301 return None;
302 };
303 let mut results = Vec::new();
304 for element in &arr.elements {
305 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
306 && let Some(values) = get_nested_string_or_array(element_obj, inner_path)
307 {
308 results.extend(values);
309 }
310 }
311 if results.is_empty() {
312 None
313 } else {
314 Some(results)
315 }
316 })
317 .unwrap_or_default()
318}
319
320#[must_use]
322pub fn extract_config_object_nested_string_or_array(
323 source: &str,
324 path: &Path,
325 object_path: &[&str],
326 inner_path: &[&str],
327) -> Vec<String> {
328 extract_config_object_nested(source, path, object_path, |value_obj| {
329 get_nested_string_or_array(value_obj, inner_path)
330 })
331}
332
333#[must_use]
335pub fn extract_config_object_nested_strings(
336 source: &str,
337 path: &Path,
338 object_path: &[&str],
339 inner_path: &[&str],
340) -> Vec<String> {
341 extract_config_object_nested(source, path, object_path, |value_obj| {
342 get_nested_string_from_object(value_obj, inner_path).map(|s| vec![s])
343 })
344}
345
346fn extract_config_object_nested(
348 source: &str,
349 path: &Path,
350 object_path: &[&str],
351 extract_fn: impl Fn(&ObjectExpression<'_>) -> Option<Vec<String>>,
352) -> Vec<String> {
353 extract_from_source(source, path, |program| {
354 let obj = find_config_object(program)?;
355 let obj_expr = get_nested_expression(obj, object_path)?;
356 let Expression::ObjectExpression(target_obj) = obj_expr else {
357 return None;
358 };
359 let mut results = Vec::new();
360 for prop in &target_obj.properties {
361 if let ObjectPropertyKind::ObjectProperty(p) = prop
362 && let Expression::ObjectExpression(value_obj) = &p.value
363 && let Some(values) = extract_fn(value_obj)
364 {
365 results.extend(values);
366 }
367 }
368 if results.is_empty() {
369 None
370 } else {
371 Some(results)
372 }
373 })
374 .unwrap_or_default()
375}
376
377#[must_use]
379pub fn extract_config_require_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
380 extract_from_source(source, path, |program| {
381 let obj = find_config_object(program)?;
382 let prop = find_property(obj, key)?;
383 Some(collect_require_sources(&prop.value))
384 })
385 .unwrap_or_default()
386}
387
388#[must_use]
390pub fn extract_config_aliases(
391 source: &str,
392 path: &Path,
393 prop_path: &[&str],
394) -> Vec<(String, String)> {
395 extract_config_aliases_kinded(source, path, prop_path)
396 .into_iter()
397 .map(|(find, replacement, _is_bare)| (find, replacement))
398 .collect()
399}
400
401#[must_use]
403pub fn extract_config_path_aliases(
404 source: &str,
405 path: &Path,
406 prop_path: &[&str],
407) -> Vec<(String, PathBuf)> {
408 extract_config_aliases_kinded(source, path, prop_path)
409 .into_iter()
410 .map(|(find, replacement, _is_bare)| (find, path_from_config_string(&replacement)))
411 .collect()
412}
413
414#[must_use]
416pub fn extract_config_array_nested_aliases(
417 source: &str,
418 path: &Path,
419 array_path: &[&str],
420 alias_path: &[&str],
421) -> Vec<(String, String)> {
422 extract_from_source(source, path, |program| {
423 let obj = find_config_object(program)?;
424 let array_expr = get_nested_expression(obj, array_path)?;
425 let Expression::ArrayExpression(arr) = array_expr else {
426 return None;
427 };
428 let mut results = Vec::new();
429 for element in &arr.elements {
430 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
431 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
432 {
433 results.extend(expression_to_alias_pairs(alias_expr));
434 }
435 }
436 (!results.is_empty()).then_some(results)
437 })
438 .unwrap_or_default()
439}
440
441#[must_use]
443pub fn extract_config_aliases_kinded(
444 source: &str,
445 path: &Path,
446 prop_path: &[&str],
447) -> Vec<(String, String, bool)> {
448 extract_from_source(source, path, |program| {
449 let obj = find_config_object(program)?;
450 let expr = get_nested_expression(obj, prop_path)?;
451 let mut visited = FxHashSet::default();
452 let aliases = resolve_alias_pairs_kinded(program, path, expr, &mut visited, 0);
453 (!aliases.is_empty()).then_some(aliases)
454 })
455 .unwrap_or_default()
456}
457
458#[must_use]
460pub fn extract_config_array_nested_aliases_kinded(
461 source: &str,
462 path: &Path,
463 array_path: &[&str],
464 alias_path: &[&str],
465) -> Vec<(String, String, bool)> {
466 extract_from_source(source, path, |program| {
467 let obj = find_config_object(program)?;
468 let array_expr = get_nested_expression(obj, array_path)?;
469 let Expression::ArrayExpression(arr) = array_expr else {
470 return None;
471 };
472 let mut results = Vec::new();
473 for element in &arr.elements {
474 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
475 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
476 {
477 results.extend(expression_to_alias_pairs_kinded(alias_expr));
478 }
479 }
480 (!results.is_empty()).then_some(results)
481 })
482 .unwrap_or_default()
483}
484
485#[must_use]
487pub fn extract_default_export_array_aliases_kinded(
488 source: &str,
489 path: &Path,
490 alias_path: &[&str],
491) -> Vec<(String, String, bool)> {
492 extract_from_source(source, path, |program| {
493 let arr = find_default_export_array(program)?;
494 let mut results = Vec::new();
495 for element in &arr.elements {
496 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
497 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
498 {
499 results.extend(expression_to_alias_pairs_kinded(alias_expr));
500 }
501 }
502 (!results.is_empty()).then_some(results)
503 })
504 .unwrap_or_default()
505}
506
507#[must_use]
509pub fn config_default_export_unreachable(source: &str, path: &Path) -> bool {
510 extract_from_source(source, path, |program| {
511 let reachable =
512 find_config_object(program).is_some() || find_default_export_array(program).is_some();
513 Some(reachable)
514 })
515 .is_some_and(|reachable| !reachable)
516}
517
518#[must_use]
524pub fn extract_config_array_object_strings(
525 source: &str,
526 path: &Path,
527 array_path: &[&str],
528 key: &str,
529) -> Vec<String> {
530 extract_from_source(source, path, |program| {
531 let obj = find_config_object(program)?;
532 let array_expr = get_nested_expression(obj, array_path)?;
533 let Expression::ArrayExpression(arr) = array_expr else {
534 return None;
535 };
536
537 let mut results = Vec::new();
538 for element in &arr.elements {
539 let Some(expr) = element.as_expression() else {
540 continue;
541 };
542 match expr {
543 Expression::ObjectExpression(item) => {
544 if let Some(prop) = find_property(item, key)
545 && let Some(value) = expression_to_path_string(&prop.value)
546 {
547 results.push(value);
548 }
549 }
550 _ => {
551 if let Some(value) = expression_to_path_string(expr) {
552 results.push(value);
553 }
554 }
555 }
556 }
557
558 (!results.is_empty()).then_some(results)
559 })
560 .unwrap_or_default()
561}
562
563#[must_use]
568pub fn extract_config_static_dir_entries(
569 source: &str,
570 path: &Path,
571 array_path: &[&str],
572) -> Vec<(String, Option<String>)> {
573 extract_from_source(source, path, |program| {
574 let obj = find_config_object(program)?;
575 let array_expr = get_nested_expression(obj, array_path)?;
576 let Expression::ArrayExpression(arr) = array_expr else {
577 return None;
578 };
579
580 let mut results = Vec::new();
581 for element in &arr.elements {
582 let Some(expr) = element.as_expression() else {
583 continue;
584 };
585 match expr {
586 Expression::ObjectExpression(item) => {
587 if let Some(from) = property_string(item, "from") {
588 let to = property_string(item, "to");
589 results.push((from, to));
590 }
591 }
592 _ => {
593 if let Some(from) = expression_to_path_string(expr) {
594 results.push((from, None));
595 }
596 }
597 }
598 }
599
600 (!results.is_empty()).then_some(results)
601 })
602 .unwrap_or_default()
603}
604
605#[must_use]
616pub fn extract_config_array_object_string_pairs(
617 source: &str,
618 path: &Path,
619 array_path: &[&str],
620 primary_key: &str,
621 secondary_key: &str,
622) -> Vec<(String, Option<String>)> {
623 extract_from_source(source, path, |program| {
624 let obj = find_config_object(program)?;
625 let array_expr = get_nested_expression(obj, array_path)?;
626 let Expression::ArrayExpression(arr) = array_expr else {
627 return None;
628 };
629
630 let mut results = Vec::new();
631 for element in &arr.elements {
632 let Some(Expression::ObjectExpression(item)) = element.as_expression() else {
633 continue;
634 };
635 let Some(primary) = find_property(item, primary_key)
636 .and_then(|prop| expression_to_path_string(&prop.value))
637 else {
638 continue;
639 };
640 let secondary = find_property(item, secondary_key)
641 .and_then(|prop| expression_to_path_string(&prop.value));
642 results.push((primary, secondary));
643 }
644
645 (!results.is_empty()).then_some(results)
646 })
647 .unwrap_or_default()
648}
649
650#[must_use]
652pub fn extract_config_array_object_command_pairs(
653 source: &str,
654 path: &Path,
655 array_path: &[&str],
656 primary_key: &str,
657 secondary_key: &str,
658) -> Vec<(String, Option<String>)> {
659 extract_from_source(source, path, |program| {
660 let obj = find_config_object(program)?;
661 let array_expr = get_nested_expression(obj, array_path)?;
662 let Expression::ArrayExpression(arr) = array_expr else {
663 return None;
664 };
665
666 let mut results = Vec::new();
667 for element in &arr.elements {
668 let Some(Expression::ObjectExpression(item)) = element.as_expression() else {
669 continue;
670 };
671 let Some(primary) = find_property(item, primary_key)
672 .and_then(|prop| expression_to_command(&prop.value))
673 else {
674 continue;
675 };
676 let secondary = find_property(item, secondary_key)
677 .and_then(|prop| expression_to_path_string(&prop.value));
678 results.push((primary, secondary));
679 }
680
681 (!results.is_empty()).then_some(results)
682 })
683 .unwrap_or_default()
684}
685
686#[must_use]
732pub fn extract_lazy_imports_in_array(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
733 extract_from_source(source, path, |program| {
734 let obj = find_config_object(program)?;
735 let array_expr = get_nested_expression(obj, prop_path)?;
736 let Expression::ArrayExpression(arr) = array_expr else {
737 return None;
738 };
739 let mut specs = Vec::new();
740 for element in &arr.elements {
741 let Some(expr) = element.as_expression() else {
742 continue;
743 };
744 if let Some(spec) = lazy_import_specifier(expr) {
745 specs.push(spec);
746 }
747 }
748 (!specs.is_empty()).then_some(specs)
749 })
750 .unwrap_or_default()
751}
752
753fn lazy_import_specifier(expr: &Expression<'_>) -> Option<String> {
766 let callable = match expr {
767 Expression::ObjectExpression(obj) => &find_property(obj, "file")?.value,
768 _ => expr,
769 };
770 let import_expr = extract_import_from_callable(callable)?;
771 expression_to_string(&import_expr.source)
772}
773
774#[must_use]
781pub fn extract_config_plugin_option_string(
782 source: &str,
783 path: &Path,
784 plugins_path: &[&str],
785 plugin_name: &str,
786 option_key: &str,
787) -> Option<String> {
788 extract_from_source(source, path, |program| {
789 let obj = find_config_object(program)?;
790 let plugins_expr = get_nested_expression(obj, plugins_path)?;
791 let Expression::ArrayExpression(plugins) = plugins_expr else {
792 return None;
793 };
794
795 for entry in &plugins.elements {
796 let Some(Expression::ArrayExpression(tuple)) = entry.as_expression() else {
797 continue;
798 };
799 let Some(plugin_expr) = tuple
800 .elements
801 .first()
802 .and_then(ArrayExpressionElement::as_expression)
803 else {
804 continue;
805 };
806 if expression_to_string(plugin_expr).as_deref() != Some(plugin_name) {
807 continue;
808 }
809
810 let Some(options_expr) = tuple
811 .elements
812 .get(1)
813 .and_then(ArrayExpressionElement::as_expression)
814 else {
815 continue;
816 };
817 let Expression::ObjectExpression(options_obj) = options_expr else {
818 continue;
819 };
820 let option = find_property(options_obj, option_key)?;
821 return expression_to_path_string(&option.value);
822 }
823
824 None
825 })
826}
827
828#[must_use]
830pub fn extract_config_plugin_option_string_from_paths(
831 source: &str,
832 path: &Path,
833 plugin_paths: &[&[&str]],
834 plugin_name: &str,
835 option_key: &str,
836) -> Option<String> {
837 plugin_paths.iter().find_map(|plugins_path| {
838 extract_config_plugin_option_string(source, path, plugins_path, plugin_name, option_key)
839 })
840}
841
842#[must_use]
845pub fn extract_vite_react_babel_dependencies(source: &str, path: &Path) -> Vec<String> {
846 extract_from_source(source, path, |program| {
847 let react_plugin_imports = collect_vite_react_plugin_imports(program);
848 if react_plugin_imports.is_empty() {
849 return None;
850 }
851
852 let obj = find_config_object(program)?;
853 let plugins = get_nested_expression(obj, &["plugins"])?;
854 let Expression::ArrayExpression(plugin_array) = plugins else {
855 return None;
856 };
857
858 let mut deps = Vec::new();
859 for element in &plugin_array.elements {
860 let Some(Expression::CallExpression(call)) = element.as_expression() else {
861 continue;
862 };
863 if !is_vite_react_plugin_call(call, &react_plugin_imports) {
864 continue;
865 }
866 let Some(Expression::ObjectExpression(options)) =
867 call.arguments.first().and_then(Argument::as_expression)
868 else {
869 continue;
870 };
871 collect_vite_react_babel_dependencies(options, &mut deps);
872 }
873
874 (!deps.is_empty()).then_some(deps)
875 })
876 .unwrap_or_default()
877}
878
879#[must_use]
884pub fn normalize_config_path_buf(
885 raw: impl AsRef<Path>,
886 config_path: &Path,
887 root: &Path,
888) -> Option<PathBuf> {
889 let raw = raw.as_ref();
890 if raw.as_os_str().is_empty() {
891 return None;
892 }
893
894 let raw_string = path_to_config_string(raw);
895 let raw_path = Path::new(&raw_string);
896 let candidate = if let Some(stripped) = raw_string.strip_prefix('/') {
897 lexical_normalize(&root.join(stripped))
898 } else if raw_path.is_absolute() {
899 lexical_normalize(raw_path)
900 } else {
901 let base = config_path.parent().unwrap_or(root);
902 lexical_normalize(&base.join(raw_path))
903 };
904
905 let relative = candidate.strip_prefix(root).ok()?;
906 (!relative.as_os_str().is_empty()).then(|| relative.to_path_buf())
907}
908
909#[must_use]
911pub fn normalize_config_path(
912 raw: impl AsRef<Path>,
913 config_path: &Path,
914 root: &Path,
915) -> Option<String> {
916 normalize_config_path_buf(raw, config_path, root).map(|path| path_to_config_string(&path))
917}
918
919pub(crate) fn extract_from_source<T>(
926 source: &str,
927 path: &Path,
928 extractor: impl FnOnce(&Program) -> Option<T>,
929) -> Option<T> {
930 let source_type = SourceType::from_path(path).unwrap_or_default();
931 let alloc = Allocator::default();
932
933 let is_json = path
934 .extension()
935 .is_some_and(|ext| ext == "json" || ext == "jsonc");
936 if is_json {
937 let wrapped = format!("({source})");
938 let parsed = Parser::new(&alloc, &wrapped, SourceType::mjs()).parse();
939 return extractor(&parsed.program);
940 }
941
942 let parsed = Parser::new(&alloc, source, source_type).parse();
943 extractor(&parsed.program)
944}
945
946#[derive(Default)]
947struct ViteReactPluginImports {
948 callables: Vec<String>,
949 namespaces: Vec<String>,
950}
951
952impl ViteReactPluginImports {
953 fn is_empty(&self) -> bool {
954 self.callables.is_empty() && self.namespaces.is_empty()
955 }
956}
957
958fn collect_vite_react_plugin_imports(program: &Program<'_>) -> ViteReactPluginImports {
959 let mut imports = ViteReactPluginImports::default();
960
961 for stmt in &program.body {
962 let Statement::ImportDeclaration(decl) = stmt else {
963 continue;
964 };
965 if decl.source.value != "@vitejs/plugin-react" {
966 continue;
967 }
968 let Some(specifiers) = &decl.specifiers else {
969 continue;
970 };
971 for specifier in specifiers {
972 match specifier {
973 ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
974 push_unique_string(&mut imports.callables, specifier.local.name.to_string());
975 }
976 ImportDeclarationSpecifier::ImportSpecifier(specifier)
977 if specifier.imported.name().as_ref() == "default" =>
978 {
979 push_unique_string(&mut imports.callables, specifier.local.name.to_string());
980 }
981 ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
982 push_unique_string(&mut imports.namespaces, specifier.local.name.to_string());
983 }
984 ImportDeclarationSpecifier::ImportSpecifier(_) => {}
985 }
986 }
987 }
988
989 imports
990}
991
992fn is_vite_react_plugin_call(call: &CallExpression<'_>, imports: &ViteReactPluginImports) -> bool {
993 match &call.callee {
994 Expression::Identifier(identifier) => imports
995 .callables
996 .iter()
997 .any(|name| name == identifier.name.as_str()),
998 Expression::StaticMemberExpression(member) if matches!(&member.object, Expression::Identifier(object) if imports.namespaces.iter().any(|name| name == object.name.as_str())) => {
999 member.property.name == "default"
1000 }
1001 _ => false,
1002 }
1003}
1004
1005fn collect_vite_react_babel_dependencies(options: &ObjectExpression<'_>, deps: &mut Vec<String>) {
1006 let Some(babel) = property_object(options, "babel") else {
1007 return;
1008 };
1009 for key in ["plugins", "presets"] {
1010 let Some(prop) = find_property(babel, key) else {
1011 continue;
1012 };
1013 for raw in collect_shallow_string_values(&prop.value) {
1014 if let Some(dep) = vite_react_babel_dependency_name(&raw) {
1015 push_unique_string(deps, dep);
1016 }
1017 }
1018 }
1019}
1020
1021fn vite_react_babel_dependency_name(raw: &str) -> Option<String> {
1022 let raw = raw.trim();
1023 let specifier = raw.strip_prefix("module:").unwrap_or(raw).trim();
1024 if specifier.is_empty()
1025 || specifier.starts_with('.')
1026 || specifier.starts_with('/')
1027 || specifier.contains(':')
1028 || specifier.contains('\\')
1029 {
1030 return None;
1031 }
1032 Some(crate::resolve::extract_package_name(specifier))
1033}
1034
1035fn push_unique_string(items: &mut Vec<String>, value: String) {
1036 if !items.contains(&value) {
1037 items.push(value);
1038 }
1039}
1040
1041fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
1053 for stmt in &program.body {
1054 match stmt {
1055 Statement::ExportDefaultDeclaration(decl) => {
1056 let expr: Option<&Expression> = match &decl.declaration {
1057 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
1058 return Some(obj);
1059 }
1060 ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
1061 return extract_object_from_function(func);
1062 }
1063 _ => decl.declaration.as_expression(),
1064 };
1065 if let Some(expr) = expr {
1066 if let Some(obj) = extract_object_from_expression(expr) {
1067 return Some(obj);
1068 }
1069 if let Some(name) = unwrap_to_identifier_name(expr) {
1070 return find_variable_init_object(program, name);
1071 }
1072 if let Some(obj) = resolve_wrapped_config_object(program, expr) {
1073 return Some(obj);
1074 }
1075 }
1076 }
1077 Statement::ExpressionStatement(expr_stmt) => {
1078 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
1079 && is_module_exports_target(&assign.left)
1080 {
1081 if let Some(obj) = extract_object_from_expression(&assign.right) {
1082 return Some(obj);
1083 }
1084 if let Some(name) = unwrap_to_identifier_name(&assign.right) {
1085 return find_variable_init_object(program, name);
1086 }
1087 return resolve_wrapped_config_object(program, &assign.right);
1088 }
1089 }
1090 _ => {}
1091 }
1092 }
1093
1094 if program.body.len() == 1
1095 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
1096 {
1097 match &expr_stmt.expression {
1098 Expression::ObjectExpression(obj) => return Some(obj),
1099 Expression::ParenthesizedExpression(paren) => {
1100 if let Expression::ObjectExpression(obj) = &paren.expression {
1101 return Some(obj);
1102 }
1103 }
1104 _ => {}
1105 }
1106 }
1107
1108 None
1109}
1110
1111fn extract_object_from_expression<'a>(
1113 expr: &'a Expression<'a>,
1114) -> Option<&'a ObjectExpression<'a>> {
1115 match expr {
1116 Expression::ObjectExpression(obj) => Some(obj),
1117 Expression::CallExpression(call) => {
1118 for arg in &call.arguments {
1119 match arg {
1120 Argument::ObjectExpression(obj) => return Some(obj),
1121 Argument::ArrowFunctionExpression(arrow) => {
1122 if arrow.expression
1123 && !arrow.body.statements.is_empty()
1124 && let Statement::ExpressionStatement(expr_stmt) =
1125 &arrow.body.statements[0]
1126 {
1127 return extract_object_from_expression(&expr_stmt.expression);
1128 }
1129 }
1130 _ => {}
1131 }
1132 }
1133 None
1134 }
1135 Expression::ParenthesizedExpression(paren) => {
1136 extract_object_from_expression(&paren.expression)
1137 }
1138 Expression::TSSatisfiesExpression(ts_sat) => {
1139 extract_object_from_expression(&ts_sat.expression)
1140 }
1141 Expression::TSAsExpression(ts_as) => extract_object_from_expression(&ts_as.expression),
1142 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
1143 Expression::FunctionExpression(func) => extract_object_from_function(func),
1144 _ => None,
1145 }
1146}
1147
1148fn extract_object_from_arrow_function<'a>(
1149 arrow: &'a ArrowFunctionExpression<'a>,
1150) -> Option<&'a ObjectExpression<'a>> {
1151 if arrow.expression {
1152 arrow.body.statements.first().and_then(|stmt| {
1153 if let Statement::ExpressionStatement(expr_stmt) = stmt {
1154 extract_object_from_expression(&expr_stmt.expression)
1155 } else {
1156 None
1157 }
1158 })
1159 } else {
1160 extract_object_from_function_body(&arrow.body)
1161 }
1162}
1163
1164fn extract_object_from_function<'a>(func: &'a Function<'a>) -> Option<&'a ObjectExpression<'a>> {
1165 func.body
1166 .as_ref()
1167 .and_then(|body| extract_object_from_function_body(body))
1168}
1169
1170fn extract_object_from_function_body<'a>(
1171 body: &'a FunctionBody<'a>,
1172) -> Option<&'a ObjectExpression<'a>> {
1173 for stmt in &body.statements {
1174 if let Statement::ReturnStatement(ret) = stmt
1175 && let Some(argument) = &ret.argument
1176 && let Some(obj) = extract_object_from_expression(argument)
1177 {
1178 return Some(obj);
1179 }
1180 }
1181 None
1182}
1183
1184fn is_module_exports_target(target: &AssignmentTarget) -> bool {
1186 if let AssignmentTarget::StaticMemberExpression(member) = target
1187 && let Expression::Identifier(obj) = &member.object
1188 {
1189 return obj.name == "module" && member.property.name == "exports";
1190 }
1191 false
1192}
1193
1194fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
1198 match expr {
1199 Expression::Identifier(id) => Some(&id.name),
1200 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
1201 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
1202 _ => None,
1203 }
1204}
1205
1206fn find_variable_init_object<'a>(
1211 program: &'a Program,
1212 name: &str,
1213) -> Option<&'a ObjectExpression<'a>> {
1214 for stmt in &program.body {
1215 if let Statement::VariableDeclaration(decl) = stmt {
1216 for declarator in &decl.declarations {
1217 if let BindingPattern::BindingIdentifier(id) = &declarator.id
1218 && id.name == name
1219 && let Some(init) = &declarator.init
1220 {
1221 return extract_object_from_expression(init);
1222 }
1223 }
1224 }
1225 }
1226 None
1227}
1228
1229fn resolve_wrapped_config_object<'a>(
1242 program: &'a Program,
1243 expr: &'a Expression<'a>,
1244) -> Option<&'a ObjectExpression<'a>> {
1245 let call = match expr {
1246 Expression::CallExpression(call) => call,
1247 Expression::ParenthesizedExpression(paren) => {
1248 return resolve_wrapped_config_object(program, &paren.expression);
1249 }
1250 Expression::TSSatisfiesExpression(ts_sat) => {
1251 return resolve_wrapped_config_object(program, &ts_sat.expression);
1252 }
1253 Expression::TSAsExpression(ts_as) => {
1254 return resolve_wrapped_config_object(program, &ts_as.expression);
1255 }
1256 _ => return None,
1257 };
1258 for arg in &call.arguments {
1259 let Some(arg_expr) = arg.as_expression() else {
1260 continue;
1261 };
1262 if let Some(name) = unwrap_to_identifier_name(arg_expr)
1263 && let Some(obj) = find_variable_init_object(program, name)
1264 {
1265 return Some(obj);
1266 }
1267 if let Some(obj) = resolve_wrapped_config_object(program, arg_expr) {
1268 return Some(obj);
1269 }
1270 }
1271 None
1272}
1273
1274pub(crate) fn find_property<'a>(
1276 obj: &'a ObjectExpression<'a>,
1277 key: &str,
1278) -> Option<&'a ObjectProperty<'a>> {
1279 for prop in &obj.properties {
1280 if let ObjectPropertyKind::ObjectProperty(p) = prop
1281 && property_key_matches(&p.key, key)
1282 {
1283 return Some(p);
1284 }
1285 }
1286 None
1287}
1288
1289pub(crate) fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
1291 match key {
1292 PropertyKey::StaticIdentifier(id) => id.name == name,
1293 PropertyKey::StringLiteral(s) => s.value == name,
1294 _ => false,
1295 }
1296}
1297
1298fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
1300 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
1301}
1302
1303fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
1305 find_property(obj, key)
1306 .map(|p| expression_to_string_array(&p.value))
1307 .unwrap_or_default()
1308}
1309
1310fn get_nested_string_array_from_object(
1312 obj: &ObjectExpression,
1313 path: &[&str],
1314) -> Option<Vec<String>> {
1315 if path.is_empty() {
1316 return None;
1317 }
1318 if path.len() == 1 {
1319 return Some(get_object_string_array_property(obj, path[0]));
1320 }
1321 let prop = find_property(obj, path[0])?;
1322 if let Expression::ObjectExpression(nested) = &prop.value {
1323 get_nested_string_array_from_object(nested, &path[1..])
1324 } else {
1325 None
1326 }
1327}
1328
1329fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
1331 if path.is_empty() {
1332 return None;
1333 }
1334 if path.len() == 1 {
1335 return get_object_string_property(obj, path[0]);
1336 }
1337 let prop = find_property(obj, path[0])?;
1338 if let Expression::ObjectExpression(nested) = &prop.value {
1339 get_nested_string_from_object(nested, &path[1..])
1340 } else {
1341 None
1342 }
1343}
1344
1345fn get_nested_command_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
1347 if path.is_empty() {
1348 return None;
1349 }
1350 if path.len() == 1 {
1351 return find_property(obj, path[0]).and_then(|prop| expression_to_command(&prop.value));
1352 }
1353 let prop = find_property(obj, path[0])?;
1354 if let Expression::ObjectExpression(nested) = &prop.value {
1355 get_nested_command_from_object(nested, &path[1..])
1356 } else {
1357 None
1358 }
1359}
1360
1361pub(crate) fn expression_to_string(expr: &Expression) -> Option<String> {
1363 match expr {
1364 Expression::StringLiteral(s) => Some(s.value.to_string()),
1365 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
1366 t.quasis.first().map(|q| q.value.raw.to_string())
1367 }
1368 _ => None,
1369 }
1370}
1371
1372fn expression_to_command(expr: &Expression) -> Option<String> {
1374 match expr {
1375 Expression::StringLiteral(s) => Some(s.value.to_string()),
1376 Expression::TemplateLiteral(template) => template_literal_to_command(template),
1377 Expression::ParenthesizedExpression(paren) => expression_to_command(&paren.expression),
1378 Expression::TSAsExpression(ts_as) => expression_to_command(&ts_as.expression),
1379 Expression::TSSatisfiesExpression(ts_sat) => expression_to_command(&ts_sat.expression),
1380 _ => None,
1381 }
1382}
1383
1384fn template_literal_to_command(template: &TemplateLiteral<'_>) -> Option<String> {
1385 let first = template.quasis.first()?.value.raw.as_str();
1386 if first.trim_start().is_empty() {
1387 return None;
1388 }
1389
1390 let mut command = String::new();
1391 for (idx, quasi) in template.quasis.iter().enumerate() {
1392 command.push_str(quasi.value.raw.as_str());
1393 if idx < template.expressions.len() {
1394 let next = template
1395 .quasis
1396 .get(idx + 1)
1397 .map_or("", |next| next.value.raw.as_str());
1398 if dynamic_template_boundary_splits_static_token(quasi.value.raw.as_str(), next) {
1399 return None;
1400 }
1401 command.push(' ');
1402 }
1403 }
1404
1405 Some(command)
1406}
1407
1408fn dynamic_template_boundary_splits_static_token(before: &str, after: &str) -> bool {
1409 before
1410 .chars()
1411 .next_back()
1412 .is_some_and(is_command_token_char)
1413 && after.chars().next().is_some_and(is_command_token_char)
1414}
1415
1416fn is_command_token_char(ch: char) -> bool {
1417 !ch.is_whitespace() && !matches!(ch, '&' | '|' | ';' | '"' | '\'')
1418}
1419
1420pub(crate) fn expression_to_path_string(expr: &Expression) -> Option<String> {
1422 match expr {
1423 Expression::ParenthesizedExpression(paren) => expression_to_path_string(&paren.expression),
1424 Expression::TSAsExpression(ts_as) => expression_to_path_string(&ts_as.expression),
1425 Expression::TSSatisfiesExpression(ts_sat) => expression_to_path_string(&ts_sat.expression),
1426 Expression::StaticMemberExpression(member) if member.property.name == "pathname" => {
1427 expression_to_path_string(&member.object)
1428 }
1429 Expression::CallExpression(call) => call_expression_to_path_string(call),
1430 Expression::NewExpression(new_expr) => new_expression_to_path_string(new_expr),
1431 _ => expression_to_string(expr),
1432 }
1433}
1434
1435fn call_expression_to_path_string(call: &CallExpression) -> Option<String> {
1436 if matches!(&call.callee, Expression::Identifier(id) if id.name == "fileURLToPath") {
1437 return call
1438 .arguments
1439 .first()
1440 .and_then(Argument::as_expression)
1441 .and_then(expression_to_path_string);
1442 }
1443
1444 let callee_name = match &call.callee {
1445 Expression::Identifier(id) => Some(id.name.as_str()),
1446 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
1447 _ => None,
1448 }?;
1449
1450 if !matches!(callee_name, "resolve" | "join") {
1451 return None;
1452 }
1453
1454 let mut segments = Vec::new();
1455 for (index, arg) in call.arguments.iter().enumerate() {
1456 let expr = arg.as_expression()?;
1457
1458 if is_dirname_anchor(expr) {
1459 if index == 0 {
1460 continue;
1461 }
1462 return None;
1463 }
1464
1465 segments.push(expression_to_string(expr)?);
1466 }
1467
1468 (!segments.is_empty()).then(|| join_path_segments(&segments))
1469}
1470
1471fn is_dirname_anchor(expr: &Expression) -> bool {
1476 match expr {
1477 Expression::Identifier(id) => id.name == "__dirname",
1478 Expression::StaticMemberExpression(member) => {
1479 member.property.name == "dirname" && is_import_meta_expression(&member.object)
1480 }
1481 _ => false,
1482 }
1483}
1484
1485fn is_import_meta_expression(expr: &Expression) -> bool {
1487 matches!(
1488 expr,
1489 Expression::MetaProperty(meta) if meta.meta.name == "import" && meta.property.name == "meta"
1490 )
1491}
1492
1493fn new_expression_to_path_string(new_expr: &NewExpression) -> Option<String> {
1494 if !matches!(&new_expr.callee, Expression::Identifier(id) if id.name == "URL") {
1495 return None;
1496 }
1497
1498 let source = new_expr
1499 .arguments
1500 .first()
1501 .and_then(Argument::as_expression)
1502 .and_then(expression_to_string)?;
1503
1504 let base = new_expr
1505 .arguments
1506 .get(1)
1507 .and_then(Argument::as_expression)?;
1508 is_import_meta_url_expression(base).then_some(source)
1509}
1510
1511fn is_import_meta_url_expression(expr: &Expression) -> bool {
1512 if let Expression::StaticMemberExpression(member) = expr {
1513 member.property.name == "url" && matches!(member.object, Expression::MetaProperty(_))
1514 } else {
1515 false
1516 }
1517}
1518
1519fn join_path_segments(segments: &[String]) -> String {
1520 let mut joined = PathBuf::new();
1521 for segment in segments {
1522 joined.push(segment);
1523 }
1524 joined.to_string_lossy().replace('\\', "/")
1525}
1526
1527fn expression_to_alias_pairs(expr: &Expression) -> Vec<(String, String)> {
1528 match expr {
1529 Expression::ObjectExpression(obj) => obj
1530 .properties
1531 .iter()
1532 .filter_map(|prop| {
1533 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
1534 return None;
1535 };
1536 let find = property_key_to_string(&prop.key)?;
1537 let replacement = expression_to_path_values(&prop.value)
1538 .into_iter()
1539 .next()
1540 .map(|path| path_to_config_string(&path))?;
1541 Some((find, replacement))
1542 })
1543 .collect(),
1544 Expression::ArrayExpression(arr) => arr
1545 .elements
1546 .iter()
1547 .filter_map(|element| {
1548 let Expression::ObjectExpression(obj) = element.as_expression()? else {
1549 return None;
1550 };
1551 let find = find_property(obj, "find")
1552 .and_then(|prop| expression_to_string(&prop.value))?;
1553 let replacement = find_property(obj, "replacement")
1554 .and_then(|prop| expression_to_path_string(&prop.value))?;
1555 Some((find, replacement))
1556 })
1557 .collect(),
1558 _ => Vec::new(),
1559 }
1560}
1561
1562fn expression_to_alias_pairs_kinded(expr: &Expression) -> Vec<(String, String, bool)> {
1566 match expr {
1567 Expression::ObjectExpression(obj) => obj
1568 .properties
1569 .iter()
1570 .filter_map(|prop| {
1571 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
1572 return None;
1573 };
1574 let find = property_key_to_string(&prop.key)?;
1575 let (replacement, is_bare) = alias_replacement_kinded(&prop.value)?;
1576 Some((find, replacement, is_bare))
1577 })
1578 .collect(),
1579 Expression::ArrayExpression(arr) => arr
1580 .elements
1581 .iter()
1582 .filter_map(|element| {
1583 let Expression::ObjectExpression(obj) = element.as_expression()? else {
1584 return None;
1585 };
1586 let find = find_property(obj, "find")
1587 .and_then(|prop| expression_to_string(&prop.value))?;
1588 let (replacement, is_bare) = find_property(obj, "replacement")
1589 .and_then(|prop| alias_replacement_kinded(&prop.value))?;
1590 Some((find, replacement, is_bare))
1591 })
1592 .collect(),
1593 _ => Vec::new(),
1594 }
1595}
1596
1597fn alias_replacement_kinded(expr: &Expression) -> Option<(String, bool)> {
1604 match expr {
1605 Expression::ParenthesizedExpression(paren) => alias_replacement_kinded(&paren.expression),
1606 Expression::TSAsExpression(ts_as) => alias_replacement_kinded(&ts_as.expression),
1607 Expression::TSSatisfiesExpression(ts_sat) => alias_replacement_kinded(&ts_sat.expression),
1608 Expression::StringLiteral(s) => {
1609 let value = s.value.to_string();
1610 let is_bare =
1611 !value.starts_with("./") && !value.starts_with("../") && !value.starts_with('/');
1612 Some((value, is_bare))
1613 }
1614 Expression::ArrayExpression(arr) => arr
1618 .elements
1619 .iter()
1620 .find_map(ArrayExpressionElement::as_expression)
1621 .and_then(alias_replacement_kinded),
1622 _ => expression_to_path_string(expr).map(|value| (value, false)),
1623 }
1624}
1625
1626const MAX_ALIAS_RESOLVE_DEPTH: usize = 8;
1632
1633const ALIAS_SIBLING_EXTS: [&str; 6] = ["js", "mjs", "cjs", "ts", "mts", "cts"];
1640
1641fn resolve_alias_pairs_kinded(
1658 program: &Program,
1659 config_path: &Path,
1660 expr: &Expression,
1661 visited: &mut FxHashSet<PathBuf>,
1662 depth: usize,
1663) -> Vec<(String, String, bool)> {
1664 match expr {
1665 Expression::ParenthesizedExpression(paren) => {
1666 resolve_alias_pairs_kinded(program, config_path, &paren.expression, visited, depth)
1667 }
1668 Expression::TSAsExpression(ts_as) => {
1669 resolve_alias_pairs_kinded(program, config_path, &ts_as.expression, visited, depth)
1670 }
1671 Expression::TSSatisfiesExpression(ts_sat) => {
1672 resolve_alias_pairs_kinded(program, config_path, &ts_sat.expression, visited, depth)
1673 }
1674 Expression::ObjectExpression(obj) => {
1675 resolve_object_alias_pairs_kinded(program, config_path, obj, visited, depth)
1676 }
1677 Expression::ArrayExpression(arr) => {
1678 resolve_array_alias_pairs_kinded(program, config_path, arr, visited, depth)
1679 }
1680 Expression::Identifier(id) => {
1681 resolve_identifier_alias_pairs(program, config_path, id.name.as_str(), visited, depth)
1682 }
1683 _ => Vec::new(),
1684 }
1685}
1686
1687fn resolve_object_alias_pairs_kinded(
1690 program: &Program,
1691 config_path: &Path,
1692 obj: &ObjectExpression,
1693 visited: &mut FxHashSet<PathBuf>,
1694 depth: usize,
1695) -> Vec<(String, String, bool)> {
1696 let mut pairs = Vec::new();
1697 for prop in &obj.properties {
1698 match prop {
1699 ObjectPropertyKind::ObjectProperty(prop) => {
1700 if let Some(find) = property_key_to_string(&prop.key)
1701 && let Some((replacement, is_bare)) = alias_replacement_kinded(&prop.value)
1702 {
1703 pairs.push((find, replacement, is_bare));
1704 }
1705 }
1706 ObjectPropertyKind::SpreadProperty(spread) => {
1708 pairs.extend(resolve_alias_pairs_kinded(
1709 program,
1710 config_path,
1711 &spread.argument,
1712 visited,
1713 depth,
1714 ));
1715 }
1716 }
1717 }
1718 pairs
1719}
1720
1721fn resolve_array_alias_pairs_kinded(
1724 program: &Program,
1725 config_path: &Path,
1726 arr: &ArrayExpression,
1727 visited: &mut FxHashSet<PathBuf>,
1728 depth: usize,
1729) -> Vec<(String, String, bool)> {
1730 let mut pairs = Vec::new();
1731 for element in &arr.elements {
1732 match element {
1733 ArrayExpressionElement::SpreadElement(spread) => {
1735 pairs.extend(resolve_alias_pairs_kinded(
1736 program,
1737 config_path,
1738 &spread.argument,
1739 visited,
1740 depth,
1741 ));
1742 }
1743 _ => {
1744 if let Some(Expression::ObjectExpression(obj)) = element.as_expression()
1745 && let Some(find) = find_property(obj, "find")
1746 .and_then(|prop| expression_to_string(&prop.value))
1747 && let Some((replacement, is_bare)) = find_property(obj, "replacement")
1748 .and_then(|prop| alias_replacement_kinded(&prop.value))
1749 {
1750 pairs.push((find, replacement, is_bare));
1751 }
1752 }
1753 }
1754 }
1755 pairs
1756}
1757
1758fn resolve_identifier_alias_pairs(
1761 program: &Program,
1762 config_path: &Path,
1763 name: &str,
1764 visited: &mut FxHashSet<PathBuf>,
1765 depth: usize,
1766) -> Vec<(String, String, bool)> {
1767 if depth >= MAX_ALIAS_RESOLVE_DEPTH {
1768 return Vec::new();
1769 }
1770 if let Some(init) = find_variable_init_expression(program, name) {
1772 return resolve_alias_pairs_kinded(program, config_path, init, visited, depth + 1);
1773 }
1774 let Some((specifier, imported_name)) = find_relative_import_binding(program, name) else {
1776 return Vec::new();
1777 };
1778 resolve_imported_alias_pairs(
1779 config_path,
1780 &specifier,
1781 imported_name.as_deref(),
1782 visited,
1783 depth + 1,
1784 )
1785}
1786
1787fn resolve_imported_alias_pairs(
1790 config_path: &Path,
1791 specifier: &str,
1792 imported_name: Option<&str>,
1793 visited: &mut FxHashSet<PathBuf>,
1794 depth: usize,
1795) -> Vec<(String, String, bool)> {
1796 let Some((sibling_path, sibling_source)) = resolve_sibling_module(config_path, specifier)
1797 else {
1798 return Vec::new();
1799 };
1800 if !visited.insert(sibling_path.clone()) {
1801 return Vec::new();
1802 }
1803 extract_from_source(&sibling_source, &sibling_path, |program| {
1804 let init = find_exported_init(program, imported_name)?;
1805 let pairs = resolve_alias_pairs_kinded(program, &sibling_path, init, visited, depth);
1806 (!pairs.is_empty()).then_some(pairs)
1807 })
1808 .unwrap_or_default()
1809}
1810
1811fn find_variable_init_expression<'a>(
1816 program: &'a Program<'a>,
1817 name: &str,
1818) -> Option<&'a Expression<'a>> {
1819 for stmt in &program.body {
1820 let decl = match stmt {
1821 Statement::VariableDeclaration(decl) => decl,
1822 Statement::ExportNamedDeclaration(export) => match &export.declaration {
1823 Some(Declaration::VariableDeclaration(decl)) => decl,
1824 _ => continue,
1825 },
1826 _ => continue,
1827 };
1828 for declarator in &decl.declarations {
1829 if let BindingPattern::BindingIdentifier(id) = &declarator.id
1830 && id.name == name
1831 && let Some(init) = &declarator.init
1832 {
1833 return Some(init);
1834 }
1835 }
1836 }
1837 None
1838}
1839
1840fn find_exported_init<'a>(
1845 program: &'a Program<'a>,
1846 name: Option<&str>,
1847) -> Option<&'a Expression<'a>> {
1848 match name {
1849 Some(name) => find_variable_init_expression(program, name),
1850 None => program.body.iter().find_map(|stmt| {
1851 if let Statement::ExportDefaultDeclaration(decl) = stmt {
1852 decl.declaration.as_expression()
1853 } else {
1854 None
1855 }
1856 }),
1857 }
1858}
1859
1860fn find_relative_import_binding(program: &Program, name: &str) -> Option<(String, Option<String>)> {
1865 for stmt in &program.body {
1866 let Statement::ImportDeclaration(decl) = stmt else {
1867 continue;
1868 };
1869 let specifier = decl.source.value.as_str();
1870 if !is_relative_specifier(specifier) {
1871 continue;
1872 }
1873 let Some(specifiers) = &decl.specifiers else {
1874 continue;
1875 };
1876 for spec in specifiers {
1877 match spec {
1878 ImportDeclarationSpecifier::ImportSpecifier(spec) if spec.local.name == name => {
1879 return Some((
1880 specifier.to_string(),
1881 Some(spec.imported.name().to_string()),
1882 ));
1883 }
1884 ImportDeclarationSpecifier::ImportDefaultSpecifier(spec)
1885 if spec.local.name == name =>
1886 {
1887 return Some((specifier.to_string(), None));
1888 }
1889 _ => {}
1890 }
1891 }
1892 }
1893 None
1894}
1895
1896fn is_relative_specifier(specifier: &str) -> bool {
1899 specifier.starts_with("./") || specifier.starts_with("../") || specifier.starts_with('/')
1900}
1901
1902fn resolve_sibling_module(config_path: &Path, specifier: &str) -> Option<(PathBuf, String)> {
1908 let parent = config_path.parent().unwrap_or(config_path);
1909 let direct = parent.join(specifier);
1910 if let Ok(source) = std::fs::read_to_string(&direct) {
1911 return Some((direct, source));
1912 }
1913 for ext in ALIAS_SIBLING_EXTS {
1914 let candidate = parent.join(format!("{specifier}.{ext}"));
1915 if let Ok(source) = std::fs::read_to_string(&candidate) {
1916 return Some((candidate, source));
1917 }
1918 }
1919 for ext in ALIAS_SIBLING_EXTS {
1920 let candidate = direct.join(format!("index.{ext}"));
1921 if let Ok(source) = std::fs::read_to_string(&candidate) {
1922 return Some((candidate, source));
1923 }
1924 }
1925 None
1926}
1927
1928fn find_default_export_array<'a>(program: &'a Program<'a>) -> Option<&'a ArrayExpression<'a>> {
1933 for stmt in &program.body {
1934 if let Statement::ExportDefaultDeclaration(decl) = stmt
1935 && let Some(expr) = decl.declaration.as_expression()
1936 {
1937 return array_from_expression(expr);
1938 }
1939 }
1940 None
1941}
1942
1943fn array_from_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
1944 match expr {
1945 Expression::ArrayExpression(arr) => Some(arr),
1946 Expression::ParenthesizedExpression(paren) => array_from_expression(&paren.expression),
1947 Expression::TSAsExpression(ts_as) => array_from_expression(&ts_as.expression),
1948 Expression::TSSatisfiesExpression(ts_sat) => array_from_expression(&ts_sat.expression),
1949 Expression::CallExpression(call) => call
1950 .arguments
1951 .first()
1952 .and_then(Argument::as_expression)
1953 .and_then(array_from_expression),
1954 _ => None,
1955 }
1956}
1957
1958pub(crate) fn lexical_normalize(path: &Path) -> PathBuf {
1959 let mut normalized = PathBuf::new();
1960
1961 for component in path.components() {
1962 match component {
1963 std::path::Component::CurDir => {}
1964 std::path::Component::ParentDir => {
1965 normalized.pop();
1966 }
1967 _ => normalized.push(component.as_os_str()),
1968 }
1969 }
1970
1971 normalized
1972}
1973
1974fn expression_to_string_array(expr: &Expression) -> Vec<String> {
1976 match expr {
1977 Expression::ArrayExpression(arr) => arr
1978 .elements
1979 .iter()
1980 .filter_map(|el| match el {
1981 ArrayExpressionElement::SpreadElement(_) => None,
1982 _ => el.as_expression().and_then(expression_to_string),
1983 })
1984 .collect(),
1985 _ => vec![],
1986 }
1987}
1988
1989fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
1994 let mut values = Vec::new();
1995 match expr {
1996 Expression::StringLiteral(s) => {
1997 values.push(s.value.to_string());
1998 }
1999 Expression::ArrayExpression(arr) => {
2000 for el in &arr.elements {
2001 if let Some(inner) = el.as_expression() {
2002 match inner {
2003 Expression::StringLiteral(s) => {
2004 values.push(s.value.to_string());
2005 }
2006 Expression::ArrayExpression(sub_arr) => {
2007 if let Some(first) = sub_arr.elements.first()
2008 && let Some(first_expr) = first.as_expression()
2009 && let Some(s) = expression_to_string(first_expr)
2010 {
2011 values.push(s);
2012 }
2013 }
2014 _ => {}
2015 }
2016 }
2017 }
2018 }
2019 Expression::ObjectExpression(obj) => {
2020 for prop in &obj.properties {
2021 if let ObjectPropertyKind::ObjectProperty(p) = prop {
2022 match &p.value {
2023 Expression::StringLiteral(s) => {
2024 values.push(s.value.to_string());
2025 }
2026 Expression::ArrayExpression(sub_arr) => {
2027 if let Some(first) = sub_arr.elements.first()
2028 && let Some(first_expr) = first.as_expression()
2029 && let Some(s) = expression_to_string(first_expr)
2030 {
2031 values.push(s);
2032 }
2033 }
2034 _ => {}
2035 }
2036 }
2037 }
2038 }
2039 _ => {}
2040 }
2041 values
2042}
2043
2044fn collect_shallow_string_or_object_property_values(
2046 expr: &Expression,
2047 object_property: &str,
2048) -> Vec<String> {
2049 match expr {
2050 Expression::ArrayExpression(arr) => arr
2051 .elements
2052 .iter()
2053 .filter_map(|element| {
2054 element
2055 .as_expression()
2056 .and_then(|expr| shallow_string_or_object_property(expr, object_property))
2057 })
2058 .collect(),
2059 _ => shallow_string_or_object_property(expr, object_property)
2060 .into_iter()
2061 .collect(),
2062 }
2063}
2064
2065fn shallow_string_or_object_property(expr: &Expression, object_property: &str) -> Option<String> {
2066 match expr {
2067 Expression::ParenthesizedExpression(paren) => {
2068 shallow_string_or_object_property(&paren.expression, object_property)
2069 }
2070 Expression::TSSatisfiesExpression(ts_sat) => {
2071 shallow_string_or_object_property(&ts_sat.expression, object_property)
2072 }
2073 Expression::TSAsExpression(ts_as) => {
2074 shallow_string_or_object_property(&ts_as.expression, object_property)
2075 }
2076 Expression::ArrayExpression(sub_arr) => sub_arr
2077 .elements
2078 .first()
2079 .and_then(ArrayExpressionElement::as_expression)
2080 .and_then(expression_to_string),
2081 Expression::ObjectExpression(obj) => {
2082 find_property(obj, object_property).and_then(|prop| expression_to_string(&prop.value))
2083 }
2084 _ => expression_to_string(expr),
2085 }
2086}
2087
2088fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
2090 match expr {
2091 Expression::StringLiteral(s) => {
2092 values.push(s.value.to_string());
2093 }
2094 Expression::ArrayExpression(arr) => {
2095 for el in &arr.elements {
2096 if let Some(expr) = el.as_expression() {
2097 collect_all_string_values(expr, values);
2098 }
2099 }
2100 }
2101 Expression::ObjectExpression(obj) => {
2102 for prop in &obj.properties {
2103 if let ObjectPropertyKind::ObjectProperty(p) = prop {
2104 collect_all_string_values(&p.value, values);
2105 }
2106 }
2107 }
2108 _ => {}
2109 }
2110}
2111
2112fn property_key_to_string(key: &PropertyKey) -> Option<String> {
2114 match key {
2115 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
2116 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
2117 _ => None,
2118 }
2119}
2120
2121fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
2123 if path.is_empty() {
2124 return None;
2125 }
2126 let prop = find_property(obj, path[0])?;
2127 if path.len() == 1 {
2128 if let Expression::ObjectExpression(nested) = &prop.value {
2129 let keys = nested
2130 .properties
2131 .iter()
2132 .filter_map(|p| {
2133 if let ObjectPropertyKind::ObjectProperty(p) = p {
2134 property_key_to_string(&p.key)
2135 } else {
2136 None
2137 }
2138 })
2139 .collect();
2140 return Some(keys);
2141 }
2142 return None;
2143 }
2144 if let Expression::ObjectExpression(nested) = &prop.value {
2145 get_nested_object_keys(nested, &path[1..])
2146 } else {
2147 None
2148 }
2149}
2150
2151fn get_nested_expression<'a>(
2153 obj: &'a ObjectExpression<'a>,
2154 path: &[&str],
2155) -> Option<&'a Expression<'a>> {
2156 if path.is_empty() {
2157 return None;
2158 }
2159 let prop = find_property(obj, path[0])?;
2160 if path.len() == 1 {
2161 return Some(&prop.value);
2162 }
2163 if let Expression::ObjectExpression(nested) = &prop.value {
2164 get_nested_expression(nested, &path[1..])
2165 } else {
2166 None
2167 }
2168}
2169
2170fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
2172 if path.is_empty() {
2173 return None;
2174 }
2175 if path.len() == 1 {
2176 let prop = find_property(obj, path[0])?;
2177 return Some(expression_to_string_or_array(&prop.value));
2178 }
2179 let prop = find_property(obj, path[0])?;
2180 if let Expression::ObjectExpression(nested) = &prop.value {
2181 get_nested_string_or_array(nested, &path[1..])
2182 } else {
2183 None
2184 }
2185}
2186
2187fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
2195 match expr {
2196 Expression::StringLiteral(s) => vec![s.value.to_string()],
2197 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
2198 .quasis
2199 .first()
2200 .map(|q| vec![q.value.raw.to_string()])
2201 .unwrap_or_default(),
2202 Expression::ArrayExpression(arr) => arr
2203 .elements
2204 .iter()
2205 .filter_map(|el| el.as_expression())
2206 .flat_map(|e| match e {
2207 Expression::ObjectExpression(obj) => find_property(obj, "input")
2208 .map(|p| expression_to_string_or_array(&p.value))
2209 .unwrap_or_default(),
2210 _ => expression_to_path_string(e).into_iter().collect(),
2211 })
2212 .collect(),
2213 Expression::ObjectExpression(obj) => obj
2214 .properties
2215 .iter()
2216 .flat_map(|p| {
2217 if let ObjectPropertyKind::ObjectProperty(p) = p {
2218 match &p.value {
2219 Expression::ArrayExpression(_) => expression_to_string_or_array(&p.value),
2220 Expression::ObjectExpression(value_obj) => {
2221 find_property(value_obj, "import")
2222 .map(|import_prop| {
2223 expression_to_string_or_array(&import_prop.value)
2224 })
2225 .unwrap_or_default()
2226 }
2227 _ => expression_to_path_string(&p.value).into_iter().collect(),
2228 }
2229 } else {
2230 Vec::new()
2231 }
2232 })
2233 .collect(),
2234 _ => expression_to_path_string(expr).into_iter().collect(),
2235 }
2236}
2237
2238fn collect_require_sources(expr: &Expression) -> Vec<String> {
2240 let mut sources = Vec::new();
2241 match expr {
2242 Expression::CallExpression(call) if is_require_call(call) => {
2243 if let Some(s) = get_require_source(call) {
2244 sources.push(s);
2245 }
2246 }
2247 Expression::ArrayExpression(arr) => {
2248 for el in &arr.elements {
2249 if let Some(inner) = el.as_expression() {
2250 match inner {
2251 Expression::CallExpression(call) if is_require_call(call) => {
2252 if let Some(s) = get_require_source(call) {
2253 sources.push(s);
2254 }
2255 }
2256 Expression::ArrayExpression(sub_arr) => {
2257 if let Some(first) = sub_arr.elements.first()
2258 && let Some(Expression::CallExpression(call)) =
2259 first.as_expression()
2260 && is_require_call(call)
2261 && let Some(s) = get_require_source(call)
2262 {
2263 sources.push(s);
2264 }
2265 }
2266 _ => {}
2267 }
2268 }
2269 }
2270 }
2271 _ => {}
2272 }
2273 sources
2274}
2275
2276fn is_require_call(call: &CallExpression) -> bool {
2278 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
2279}
2280
2281fn get_require_source(call: &CallExpression) -> Option<String> {
2283 call.arguments.first().and_then(|arg| {
2284 if let Argument::StringLiteral(s) = arg {
2285 Some(s.value.to_string())
2286 } else {
2287 None
2288 }
2289 })
2290}
2291
2292#[cfg(test)]
2293mod tests {
2294 use super::*;
2295 use std::path::PathBuf;
2296
2297 fn js_path() -> PathBuf {
2298 PathBuf::from("config.js")
2299 }
2300
2301 fn ts_path() -> PathBuf {
2302 PathBuf::from("config.ts")
2303 }
2304
2305 #[test]
2306 fn extract_lazy_imports_bare_arrows() {
2307 let source = r"
2308 import { defineConfig } from '@adonisjs/core/app'
2309 export default defineConfig({
2310 preloads: [
2311 () => import('#start/routes'),
2312 () => import('#start/kernel'),
2313 ],
2314 })
2315 ";
2316 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["preloads"]);
2317 assert_eq!(specs, vec!["#start/routes", "#start/kernel"]);
2318 }
2319
2320 #[test]
2321 fn extract_lazy_imports_object_form_with_file_key() {
2322 let source = r"
2323 export default defineConfig({
2324 providers: [
2325 () => import('@adonisjs/core/providers/app_provider'),
2326 {
2327 file: () => import('@adonisjs/core/providers/repl_provider'),
2328 environment: ['repl', 'test'],
2329 },
2330 ],
2331 })
2332 ";
2333 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
2334 assert_eq!(
2335 specs,
2336 vec![
2337 "@adonisjs/core/providers/app_provider",
2338 "@adonisjs/core/providers/repl_provider",
2339 ]
2340 );
2341 }
2342
2343 #[test]
2344 fn extract_lazy_imports_block_body_with_return() {
2345 let source = r"
2346 export default defineConfig({
2347 commands: [
2348 () => { return import('@adonisjs/core/commands') },
2349 ],
2350 })
2351 ";
2352 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
2353 assert_eq!(specs, vec!["@adonisjs/core/commands"]);
2354 }
2355
2356 #[test]
2357 fn extract_lazy_imports_skips_unknown_element_shapes() {
2358 let source = r"
2359 export default defineConfig({
2360 commands: [
2361 'string-entry',
2362 42,
2363 { other: 'value' },
2364 () => import('@adonisjs/lucid/commands'),
2365 ],
2366 })
2367 ";
2368 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
2369 assert_eq!(specs, vec!["@adonisjs/lucid/commands"]);
2370 }
2371
2372 #[test]
2373 fn extract_lazy_imports_missing_property_returns_empty() {
2374 let source = r"
2375 export default defineConfig({
2376 preloads: [() => import('#start/routes')],
2377 })
2378 ";
2379 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
2380 assert!(specs.is_empty());
2381 }
2382
2383 #[test]
2384 fn extract_imports_basic() {
2385 let source = r"
2386 import foo from 'foo-pkg';
2387 import { bar } from '@scope/bar';
2388 export default {};
2389 ";
2390 let imports = extract_imports(source, &js_path());
2391 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
2392 }
2393
2394 #[test]
2395 fn extract_default_export_object_property() {
2396 let source = r#"export default { testDir: "./tests" };"#;
2397 let val = extract_config_string(source, &js_path(), &["testDir"]);
2398 assert_eq!(val, Some("./tests".to_string()));
2399 }
2400
2401 #[test]
2402 fn extract_define_config_property() {
2403 let source = r#"
2404 import { defineConfig } from 'vitest/config';
2405 export default defineConfig({
2406 test: {
2407 include: ["**/*.test.ts", "**/*.spec.ts"],
2408 setupFiles: ["./test/setup.ts"]
2409 }
2410 });
2411 "#;
2412 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
2413 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
2414
2415 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
2416 assert_eq!(setup, vec!["./test/setup.ts"]);
2417 }
2418
2419 #[test]
2420 fn extract_module_exports_property() {
2421 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
2422 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
2423 assert_eq!(val, Some("jsdom".to_string()));
2424 }
2425
2426 #[test]
2427 fn extract_nested_string_array() {
2428 let source = r#"
2429 export default {
2430 resolve: {
2431 alias: {
2432 "@": "./src"
2433 }
2434 },
2435 test: {
2436 include: ["src/**/*.test.ts"]
2437 }
2438 };
2439 "#;
2440 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
2441 assert_eq!(include, vec!["src/**/*.test.ts"]);
2442 }
2443
2444 #[test]
2445 fn extract_addons_array() {
2446 let source = r#"
2447 export default {
2448 addons: [
2449 "@storybook/addon-a11y",
2450 "@storybook/addon-docs",
2451 "@storybook/addon-links"
2452 ]
2453 };
2454 "#;
2455 let addons = extract_config_property_strings(source, &ts_path(), "addons");
2456 assert_eq!(
2457 addons,
2458 vec![
2459 "@storybook/addon-a11y",
2460 "@storybook/addon-docs",
2461 "@storybook/addon-links"
2462 ]
2463 );
2464 }
2465
2466 #[test]
2467 fn handle_empty_config() {
2468 let source = "";
2469 let result = extract_config_string(source, &js_path(), &["key"]);
2470 assert_eq!(result, None);
2471 }
2472
2473 #[test]
2474 fn object_keys_postcss_plugins() {
2475 let source = r"
2476 module.exports = {
2477 plugins: {
2478 autoprefixer: {},
2479 tailwindcss: {},
2480 'postcss-import': {}
2481 }
2482 };
2483 ";
2484 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2485 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
2486 }
2487
2488 #[test]
2489 fn object_keys_nested_path() {
2490 let source = r"
2491 export default {
2492 build: {
2493 plugins: {
2494 minify: {},
2495 compress: {}
2496 }
2497 }
2498 };
2499 ";
2500 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
2501 assert_eq!(keys, vec!["minify", "compress"]);
2502 }
2503
2504 #[test]
2505 fn object_keys_empty_object() {
2506 let source = r"export default { plugins: {} };";
2507 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2508 assert!(keys.is_empty());
2509 }
2510
2511 #[test]
2512 fn object_keys_non_object_returns_empty() {
2513 let source = r#"export default { plugins: ["a", "b"] };"#;
2514 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2515 assert!(keys.is_empty());
2516 }
2517
2518 #[test]
2519 fn string_or_array_single_string() {
2520 let source = r#"export default { entry: "./src/index.js" };"#;
2521 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2522 assert_eq!(result, vec!["./src/index.js"]);
2523 }
2524
2525 #[test]
2526 fn string_or_array_array() {
2527 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
2528 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2529 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
2530 }
2531
2532 #[test]
2533 fn string_or_array_object_values() {
2534 let source =
2535 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
2536 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2537 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
2538 }
2539
2540 #[test]
2541 fn string_or_array_object_array_values() {
2542 let source = r#"export default { entry: { app: ["./src/polyfill.js", "./src/app.js"] } };"#;
2543 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2544 assert_eq!(result, vec!["./src/polyfill.js", "./src/app.js"]);
2545 }
2546
2547 #[test]
2548 fn string_or_array_webpack_entry_descriptors() {
2549 let source = r#"
2550 export default {
2551 entry: {
2552 app: {
2553 import: "./src/app.js",
2554 filename: "pages/app.js",
2555 dependOn: "shared",
2556 },
2557 admin: {
2558 import: ["./src/admin-polyfill.js", "./src/admin.js"],
2559 runtime: "runtime",
2560 },
2561 shared: ["react", "react-dom"],
2562 },
2563 };
2564 "#;
2565 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2566 assert_eq!(
2567 result,
2568 vec![
2569 "./src/app.js",
2570 "./src/admin-polyfill.js",
2571 "./src/admin.js",
2572 "react",
2573 "react-dom"
2574 ]
2575 );
2576 }
2577
2578 #[test]
2579 fn string_or_array_nested_path() {
2580 let source = r#"
2581 export default {
2582 build: {
2583 rollupOptions: {
2584 input: ["./index.html", "./about.html"]
2585 }
2586 }
2587 };
2588 "#;
2589 let result = extract_config_string_or_array(
2590 source,
2591 &js_path(),
2592 &["build", "rollupOptions", "input"],
2593 );
2594 assert_eq!(result, vec!["./index.html", "./about.html"]);
2595 }
2596
2597 #[test]
2598 fn string_or_array_template_literal() {
2599 let source = r"export default { entry: `./src/index.js` };";
2600 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2601 assert_eq!(result, vec!["./src/index.js"]);
2602 }
2603
2604 #[test]
2605 fn string_or_array_object_path_helper_values() {
2606 let source = r#"
2607 import { resolve, join } from "node:path";
2608 import path from "node:path";
2609 export default {
2610 build: {
2611 rollupOptions: {
2612 input: {
2613 app: resolve(__dirname, "src/app.ts"),
2614 modal: path.resolve(__dirname, "src/modal.ts"),
2615 tabs: join(__dirname, "src/tabs.ts"),
2616 styles: resolve(__dirname, "src/index.css"),
2617 },
2618 },
2619 },
2620 };
2621 "#;
2622 let result = extract_config_string_or_array(
2623 source,
2624 &js_path(),
2625 &["build", "rollupOptions", "input"],
2626 );
2627 assert_eq!(
2628 result,
2629 vec!["src/app.ts", "src/modal.ts", "src/tabs.ts", "src/index.css"]
2630 );
2631 }
2632
2633 #[test]
2634 fn string_or_array_array_path_helper_values() {
2635 let source = r#"
2636 import { resolve } from "node:path";
2637 export default {
2638 build: {
2639 rollupOptions: {
2640 input: [resolve(__dirname, "src/a.ts"), "./src/b.ts"],
2641 },
2642 },
2643 };
2644 "#;
2645 let result = extract_config_string_or_array(
2646 source,
2647 &js_path(),
2648 &["build", "rollupOptions", "input"],
2649 );
2650 assert_eq!(result, vec!["src/a.ts", "./src/b.ts"]);
2651 }
2652
2653 #[test]
2654 fn string_or_array_top_level_path_helper_call() {
2655 let source = r#"
2656 import { resolve } from "node:path";
2657 export default { build: { lib: { entry: resolve(__dirname, "src/index.ts") } } };
2658 "#;
2659 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
2660 assert_eq!(result, vec!["src/index.ts"]);
2661 }
2662
2663 #[test]
2664 fn string_or_array_import_meta_dirname_anchor() {
2665 let source = r#"
2666 import { resolve } from "node:path";
2667 export default {
2668 build: { lib: { entry: resolve(import.meta.dirname, "src/index.ts") } },
2669 };
2670 "#;
2671 let result = extract_config_string_or_array(source, &ts_path(), &["build", "lib", "entry"]);
2672 assert_eq!(result, vec!["src/index.ts"]);
2673 }
2674
2675 #[test]
2676 fn string_or_array_non_literal_path_helper_args_dropped() {
2677 let source = r#"
2678 import { resolve } from "node:path";
2679 export default { build: { lib: { entry: resolve(baseDir, "src/index.ts") } } };
2680 "#;
2681 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
2682 assert!(
2683 result.is_empty(),
2684 "non-literal path-helper args must be dropped: {result:?}"
2685 );
2686 }
2687
2688 #[test]
2689 fn require_strings_array() {
2690 let source = r"
2691 module.exports = {
2692 plugins: [
2693 require('autoprefixer'),
2694 require('postcss-import')
2695 ]
2696 };
2697 ";
2698 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2699 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
2700 }
2701
2702 #[test]
2703 fn require_strings_with_tuples() {
2704 let source = r"
2705 module.exports = {
2706 plugins: [
2707 require('autoprefixer'),
2708 [require('postcss-preset-env'), { stage: 3 }]
2709 ]
2710 };
2711 ";
2712 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2713 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
2714 }
2715
2716 #[test]
2717 fn require_strings_empty_array() {
2718 let source = r"module.exports = { plugins: [] };";
2719 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2720 assert!(deps.is_empty());
2721 }
2722
2723 #[test]
2724 fn require_strings_no_require_calls() {
2725 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
2726 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2727 assert!(deps.is_empty());
2728 }
2729
2730 #[test]
2731 fn extract_aliases_from_object_with_file_url_to_path() {
2732 let source = r#"
2733 import { defineConfig } from 'vite';
2734 import { fileURLToPath, URL } from 'node:url';
2735
2736 export default defineConfig({
2737 resolve: {
2738 alias: {
2739 "@": fileURLToPath(new URL("./src", import.meta.url))
2740 }
2741 }
2742 });
2743 "#;
2744
2745 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
2746 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
2747 }
2748
2749 #[test]
2750 fn extract_aliases_from_array_form() {
2751 let source = r#"
2752 export default {
2753 resolve: {
2754 alias: [
2755 { find: "@", replacement: "./src" },
2756 { find: "$utils", replacement: "src/lib/utils" }
2757 ]
2758 }
2759 };
2760 "#;
2761
2762 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
2763 assert_eq!(
2764 aliases,
2765 vec![
2766 ("@".to_string(), "./src".to_string()),
2767 ("$utils".to_string(), "src/lib/utils".to_string())
2768 ]
2769 );
2770 }
2771
2772 #[test]
2773 fn extract_aliases_from_object_with_array_values() {
2774 let source = r#"
2775 ({
2776 compilerOptions: {
2777 paths: {
2778 "@/*": ["./src/*"],
2779 "@shared/*": ["./shared/*", "./fallback/*"]
2780 }
2781 }
2782 })
2783 "#;
2784
2785 let aliases = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
2786 assert_eq!(
2787 aliases,
2788 vec![
2789 ("@/*".to_string(), "./src/*".to_string()),
2790 ("@shared/*".to_string(), "./shared/*".to_string())
2791 ]
2792 );
2793 }
2794
2795 #[test]
2796 fn extract_array_object_strings_mixed_forms() {
2797 let source = r#"
2798 export default {
2799 components: [
2800 "~/components",
2801 { path: "@/feature-components" }
2802 ]
2803 };
2804 "#;
2805
2806 let values =
2807 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
2808 assert_eq!(
2809 values,
2810 vec![
2811 "~/components".to_string(),
2812 "@/feature-components".to_string()
2813 ]
2814 );
2815 }
2816
2817 #[test]
2818 fn extract_array_object_string_pairs_with_and_without_secondary() {
2819 let source = r#"
2820 export default {
2821 webServer: [
2822 { command: "tsx scripts/api.ts", cwd: "packages/api" },
2823 { command: "tsx scripts/web.ts" }
2824 ]
2825 };
2826 "#;
2827
2828 let pairs = extract_config_array_object_string_pairs(
2829 source,
2830 &ts_path(),
2831 &["webServer"],
2832 "command",
2833 "cwd",
2834 );
2835 assert_eq!(
2836 pairs,
2837 vec![
2838 (
2839 "tsx scripts/api.ts".to_string(),
2840 Some("packages/api".to_string())
2841 ),
2842 ("tsx scripts/web.ts".to_string(), None),
2843 ]
2844 );
2845 }
2846
2847 #[test]
2848 fn extract_array_object_string_pairs_skips_elements_missing_primary() {
2849 let source = r#"
2850 export default {
2851 webServer: [
2852 { cwd: "packages/api" },
2853 { command: "srvx --port 3000" }
2854 ]
2855 };
2856 "#;
2857
2858 let pairs = extract_config_array_object_string_pairs(
2859 source,
2860 &ts_path(),
2861 &["webServer"],
2862 "command",
2863 "cwd",
2864 );
2865 assert_eq!(pairs, vec![("srvx --port 3000".to_string(), None)]);
2866 }
2867
2868 #[test]
2869 fn extract_array_object_string_pairs_empty_for_object_form() {
2870 let source = r#"
2871 export default {
2872 webServer: { command: "srvx --port 3000" }
2873 };
2874 "#;
2875
2876 let pairs = extract_config_array_object_string_pairs(
2877 source,
2878 &ts_path(),
2879 &["webServer"],
2880 "command",
2881 "cwd",
2882 );
2883 assert!(pairs.is_empty());
2884 }
2885
2886 #[test]
2887 fn extract_config_plugin_option_string_from_json() {
2888 let source = r#"{
2889 "expo": {
2890 "plugins": [
2891 ["expo-router", { "root": "src/app" }]
2892 ]
2893 }
2894 }"#;
2895
2896 let value = extract_config_plugin_option_string(
2897 source,
2898 &json_path(),
2899 &["expo", "plugins"],
2900 "expo-router",
2901 "root",
2902 );
2903
2904 assert_eq!(value, Some("src/app".to_string()));
2905 }
2906
2907 #[test]
2908 fn extract_config_plugin_option_string_from_top_level_plugins() {
2909 let source = r#"{
2910 "plugins": [
2911 ["expo-router", { "root": "./src/routes" }]
2912 ]
2913 }"#;
2914
2915 let value = extract_config_plugin_option_string_from_paths(
2916 source,
2917 &json_path(),
2918 &[&["plugins"], &["expo", "plugins"]],
2919 "expo-router",
2920 "root",
2921 );
2922
2923 assert_eq!(value, Some("./src/routes".to_string()));
2924 }
2925
2926 #[test]
2927 fn extract_config_plugin_option_string_from_ts_config() {
2928 let source = r"
2929 export default {
2930 expo: {
2931 plugins: [
2932 ['expo-router', { root: './src/app' }]
2933 ]
2934 }
2935 };
2936 ";
2937
2938 let value = extract_config_plugin_option_string(
2939 source,
2940 &ts_path(),
2941 &["expo", "plugins"],
2942 "expo-router",
2943 "root",
2944 );
2945
2946 assert_eq!(value, Some("./src/app".to_string()));
2947 }
2948
2949 #[test]
2950 fn extract_config_plugin_option_string_returns_none_when_plugin_missing() {
2951 let source = r#"{
2952 "expo": {
2953 "plugins": [
2954 ["expo-font", {}]
2955 ]
2956 }
2957 }"#;
2958
2959 let value = extract_config_plugin_option_string(
2960 source,
2961 &json_path(),
2962 &["expo", "plugins"],
2963 "expo-router",
2964 "root",
2965 );
2966
2967 assert_eq!(value, None);
2968 }
2969
2970 #[test]
2971 fn vite_react_babel_dependencies_extract_plain_tuple_and_prefixed_entries() {
2972 let source = r#"
2973 import react from "@vitejs/plugin-react";
2974
2975 export default defineConfig({
2976 plugins: [
2977 react({
2978 babel: {
2979 plugins: [
2980 "babel-plugin-plain",
2981 ["module:@preact/signals-react-transform", { mode: "auto" }],
2982 ],
2983 presets: [["@babel/preset-react", { runtime: "automatic" }]],
2984 },
2985 }),
2986 ],
2987 });
2988 "#;
2989
2990 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
2991
2992 assert_eq!(
2993 deps,
2994 vec![
2995 "babel-plugin-plain".to_string(),
2996 "@preact/signals-react-transform".to_string(),
2997 "@babel/preset-react".to_string(),
2998 ]
2999 );
3000 }
3001
3002 #[test]
3003 fn vite_react_babel_dependencies_support_default_alias_import() {
3004 let source = r#"
3005 import { default as viteReact } from "@vitejs/plugin-react";
3006
3007 export default {
3008 plugins: [
3009 viteReact({
3010 babel: {
3011 plugins: [["module:@scope/pkg/plugin", {}]],
3012 },
3013 }),
3014 ],
3015 };
3016 "#;
3017
3018 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3019
3020 assert_eq!(deps, vec!["@scope/pkg".to_string()]);
3021 }
3022
3023 #[test]
3024 fn vite_react_babel_dependencies_ignore_unrelated_plugin_calls() {
3025 let source = r#"
3026 import vue from "@vitejs/plugin-vue";
3027
3028 export default {
3029 plugins: [
3030 vue({
3031 babel: {
3032 plugins: ["@preact/signals-react-transform"],
3033 },
3034 }),
3035 ],
3036 };
3037 "#;
3038
3039 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3040
3041 assert!(deps.is_empty());
3042 }
3043
3044 #[test]
3045 fn vite_react_babel_dependencies_skip_relative_and_protocol_entries() {
3046 let source = r#"
3047 import react from "@vitejs/plugin-react";
3048
3049 export default {
3050 plugins: [
3051 react({
3052 babel: {
3053 plugins: ["./local-plugin", "module:./local-prefixed", "http://example.com/plugin"],
3054 },
3055 }),
3056 ],
3057 };
3058 "#;
3059
3060 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3061
3062 assert!(deps.is_empty());
3063 }
3064
3065 #[test]
3066 fn normalize_config_path_relative_to_root() {
3067 let config_path = PathBuf::from("/project/vite.config.ts");
3068 let root = PathBuf::from("/project");
3069
3070 assert_eq!(
3071 normalize_config_path("./src/lib", &config_path, &root),
3072 Some("src/lib".to_string())
3073 );
3074 assert_eq!(
3075 normalize_config_path("/src/lib", &config_path, &root),
3076 Some("src/lib".to_string())
3077 );
3078 }
3079
3080 #[test]
3081 fn normalize_config_path_mixed_separators_and_parent_dirs() {
3082 let config_path = PathBuf::from("/project/config/vite.config.ts");
3083 let root = PathBuf::from("/project");
3084
3085 assert_eq!(
3086 normalize_config_path(".\\src\\..\\app\\lib", &config_path, &root),
3087 Some("config/app/lib".to_string())
3088 );
3089 }
3090
3091 #[test]
3092 fn normalize_config_path_leading_slash_stays_project_relative() {
3093 let config_path = PathBuf::from("/project/vite.config.ts");
3094 let root = PathBuf::from("/project");
3095
3096 assert_eq!(
3097 normalize_config_path("/src\\lib", &config_path, &root),
3098 Some("src/lib".to_string())
3099 );
3100 }
3101
3102 #[test]
3103 fn json_wrapped_in_parens_string() {
3104 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
3105 let val = extract_config_string(source, &js_path(), &["extends"]);
3106 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
3107 }
3108
3109 #[test]
3110 fn json_wrapped_in_parens_nested_array() {
3111 let source =
3112 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
3113 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
3114 assert_eq!(types, vec!["node", "jest"]);
3115
3116 let include = extract_config_string_array(source, &js_path(), &["include"]);
3117 assert_eq!(include, vec!["src/**/*"]);
3118 }
3119
3120 #[test]
3121 fn json_wrapped_in_parens_object_keys() {
3122 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
3123 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3124 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
3125 }
3126
3127 fn json_path() -> PathBuf {
3128 PathBuf::from("config.json")
3129 }
3130
3131 #[test]
3132 fn json_file_parsed_correctly() {
3133 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
3134 let val = extract_config_string(source, &json_path(), &["key"]);
3135 assert_eq!(val, Some("value".to_string()));
3136
3137 let list = extract_config_string_array(source, &json_path(), &["list"]);
3138 assert_eq!(list, vec!["a", "b"]);
3139 }
3140
3141 #[test]
3142 fn jsonc_file_parsed_correctly() {
3143 let source = r#"{"key": "value"}"#;
3144 let path = PathBuf::from("tsconfig.jsonc");
3145 let val = extract_config_string(source, &path, &["key"]);
3146 assert_eq!(val, Some("value".to_string()));
3147 }
3148
3149 #[test]
3150 fn extract_define_config_arrow_function() {
3151 let source = r#"
3152 import { defineConfig } from 'vite';
3153 export default defineConfig(() => ({
3154 test: {
3155 include: ["**/*.test.ts"]
3156 }
3157 }));
3158 "#;
3159 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
3160 assert_eq!(include, vec!["**/*.test.ts"]);
3161 }
3162
3163 #[test]
3164 fn extract_config_from_default_export_function_declaration() {
3165 let source = r#"
3166 export default function createConfig() {
3167 return {
3168 clientModules: ["./src/client/global.js"]
3169 };
3170 }
3171 "#;
3172
3173 let client_modules = extract_config_string_array(source, &ts_path(), &["clientModules"]);
3174 assert_eq!(client_modules, vec!["./src/client/global.js"]);
3175 }
3176
3177 #[test]
3178 fn extract_config_from_default_export_async_function_declaration() {
3179 let source = r#"
3180 export default async function createConfigAsync() {
3181 return {
3182 docs: {
3183 path: "knowledge"
3184 }
3185 };
3186 }
3187 "#;
3188
3189 let docs_path = extract_config_string(source, &ts_path(), &["docs", "path"]);
3190 assert_eq!(docs_path, Some("knowledge".to_string()));
3191 }
3192
3193 #[test]
3194 fn extract_config_from_exported_arrow_function_identifier() {
3195 let source = r#"
3196 const config = async () => {
3197 return {
3198 themes: ["classic"]
3199 };
3200 };
3201
3202 export default config;
3203 "#;
3204
3205 let themes = extract_config_shallow_strings(source, &ts_path(), "themes");
3206 assert_eq!(themes, vec!["classic"]);
3207 }
3208
3209 #[test]
3210 fn module_exports_nested_string() {
3211 let source = r#"
3212 module.exports = {
3213 resolve: {
3214 alias: {
3215 "@": "./src"
3216 }
3217 }
3218 };
3219 "#;
3220 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
3221 assert_eq!(val, Some("./src".to_string()));
3222 }
3223
3224 #[test]
3225 fn property_strings_nested_objects() {
3226 let source = r#"
3227 export default {
3228 plugins: {
3229 group1: { a: "val-a" },
3230 group2: { b: "val-b" }
3231 }
3232 };
3233 "#;
3234 let values = extract_config_property_strings(source, &js_path(), "plugins");
3235 assert!(values.contains(&"val-a".to_string()));
3236 assert!(values.contains(&"val-b".to_string()));
3237 }
3238
3239 #[test]
3240 fn property_strings_missing_key_returns_empty() {
3241 let source = r#"export default { other: "value" };"#;
3242 let values = extract_config_property_strings(source, &js_path(), "missing");
3243 assert!(values.is_empty());
3244 }
3245
3246 #[test]
3247 fn shallow_strings_tuple_array() {
3248 let source = r#"
3249 module.exports = {
3250 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
3251 };
3252 "#;
3253 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
3254 assert_eq!(values, vec!["default", "jest-junit"]);
3255 assert!(!values.contains(&"reports".to_string()));
3256 }
3257
3258 #[test]
3259 fn shallow_strings_single_string() {
3260 let source = r#"export default { preset: "ts-jest" };"#;
3261 let values = extract_config_shallow_strings(source, &js_path(), "preset");
3262 assert_eq!(values, vec!["ts-jest"]);
3263 }
3264
3265 #[test]
3266 fn shallow_strings_missing_key() {
3267 let source = r#"export default { other: "val" };"#;
3268 let values = extract_config_shallow_strings(source, &js_path(), "missing");
3269 assert!(values.is_empty());
3270 }
3271
3272 #[test]
3273 fn shallow_strings_or_object_property_alias_objects() {
3274 let source = r#"
3275 export default {
3276 jsPlugins: [
3277 "eslint-plugin-playwright",
3278 ["eslint-plugin-regexp", { rules: {} }],
3279 { name: "short", specifier: "eslint-plugin-with-long-name" }
3280 ]
3281 };
3282 "#;
3283 let values = extract_config_shallow_strings_or_object_property(
3284 source,
3285 &ts_path(),
3286 "jsPlugins",
3287 "specifier",
3288 );
3289 assert_eq!(
3290 values,
3291 vec![
3292 "eslint-plugin-playwright",
3293 "eslint-plugin-regexp",
3294 "eslint-plugin-with-long-name"
3295 ]
3296 );
3297 }
3298
3299 #[test]
3300 fn nested_shallow_strings_vitest_reporters() {
3301 let source = r#"
3302 export default {
3303 test: {
3304 reporters: ["default", "vitest-sonar-reporter"]
3305 }
3306 };
3307 "#;
3308 let values =
3309 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3310 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
3311 }
3312
3313 #[test]
3314 fn nested_shallow_strings_tuple_format() {
3315 let source = r#"
3316 export default {
3317 test: {
3318 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
3319 }
3320 };
3321 "#;
3322 let values =
3323 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3324 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
3325 }
3326
3327 #[test]
3328 fn nested_shallow_strings_missing_outer() {
3329 let source = r"export default { other: {} };";
3330 let values =
3331 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3332 assert!(values.is_empty());
3333 }
3334
3335 #[test]
3336 fn nested_shallow_strings_missing_inner() {
3337 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
3338 let values =
3339 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3340 assert!(values.is_empty());
3341 }
3342
3343 #[test]
3344 fn string_or_array_missing_path() {
3345 let source = r"export default {};";
3346 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3347 assert!(result.is_empty());
3348 }
3349
3350 #[test]
3351 fn string_or_array_non_string_values() {
3352 let source = r"export default { entry: [42, true] };";
3353 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3354 assert!(result.is_empty());
3355 }
3356
3357 #[test]
3358 fn array_nested_extraction() {
3359 let source = r#"
3360 export default defineConfig({
3361 test: {
3362 projects: [
3363 {
3364 test: {
3365 setupFiles: ["./test/setup-a.ts"]
3366 }
3367 },
3368 {
3369 test: {
3370 setupFiles: "./test/setup-b.ts"
3371 }
3372 }
3373 ]
3374 }
3375 });
3376 "#;
3377 let results = extract_config_array_nested_string_or_array(
3378 source,
3379 &ts_path(),
3380 &["test", "projects"],
3381 &["test", "setupFiles"],
3382 );
3383 assert!(results.contains(&"./test/setup-a.ts".to_string()));
3384 assert!(results.contains(&"./test/setup-b.ts".to_string()));
3385 }
3386
3387 #[test]
3388 fn array_nested_empty_when_no_array() {
3389 let source = r#"export default { test: { projects: "not-an-array" } };"#;
3390 let results = extract_config_array_nested_string_or_array(
3391 source,
3392 &js_path(),
3393 &["test", "projects"],
3394 &["test", "setupFiles"],
3395 );
3396 assert!(results.is_empty());
3397 }
3398
3399 #[test]
3400 fn object_nested_extraction() {
3401 let source = r#"{
3402 "projects": {
3403 "app-one": {
3404 "architect": {
3405 "build": {
3406 "options": {
3407 "styles": ["src/styles.css"]
3408 }
3409 }
3410 }
3411 }
3412 }
3413 }"#;
3414 let results = extract_config_object_nested_string_or_array(
3415 source,
3416 &json_path(),
3417 &["projects"],
3418 &["architect", "build", "options", "styles"],
3419 );
3420 assert_eq!(results, vec!["src/styles.css"]);
3421 }
3422
3423 #[test]
3424 fn array_with_object_input_form_extracted() {
3425 let source = r#"{
3426 "projects": {
3427 "app": {
3428 "architect": {
3429 "build": {
3430 "options": {
3431 "styles": [
3432 "src/styles.scss",
3433 { "input": "src/theme.scss", "bundleName": "theme", "inject": false },
3434 { "bundleName": "lazy-only" }
3435 ]
3436 }
3437 }
3438 }
3439 }
3440 }
3441 }"#;
3442 let results = extract_config_object_nested_string_or_array(
3443 source,
3444 &json_path(),
3445 &["projects"],
3446 &["architect", "build", "options", "styles"],
3447 );
3448 assert!(
3449 results.contains(&"src/styles.scss".to_string()),
3450 "string form must still work: {results:?}"
3451 );
3452 assert!(
3453 results.contains(&"src/theme.scss".to_string()),
3454 "object form with `input` must be extracted: {results:?}"
3455 );
3456 assert!(
3457 !results.contains(&"lazy-only".to_string()),
3458 "bundleName must not be misinterpreted as a path: {results:?}"
3459 );
3460 assert!(
3461 !results.contains(&"theme".to_string()),
3462 "bundleName from full object must not leak: {results:?}"
3463 );
3464 }
3465
3466 #[test]
3467 fn object_nested_strings_extraction() {
3468 let source = r#"{
3469 "targets": {
3470 "build": {
3471 "executor": "@angular/build:application"
3472 },
3473 "test": {
3474 "executor": "@nx/vite:test"
3475 }
3476 }
3477 }"#;
3478 let results =
3479 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
3480 assert!(results.contains(&"@angular/build:application".to_string()));
3481 assert!(results.contains(&"@nx/vite:test".to_string()));
3482 }
3483
3484 #[test]
3485 fn require_strings_direct_call() {
3486 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
3487 let deps = extract_config_require_strings(source, &js_path(), "adapter");
3488 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
3489 }
3490
3491 #[test]
3492 fn require_strings_no_matching_key() {
3493 let source = r"module.exports = { other: require('something') };";
3494 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3495 assert!(deps.is_empty());
3496 }
3497
3498 #[test]
3499 fn extract_imports_no_imports() {
3500 let source = r"export default {};";
3501 let imports = extract_imports(source, &js_path());
3502 assert!(imports.is_empty());
3503 }
3504
3505 #[test]
3506 fn extract_imports_side_effect_import() {
3507 let source = r"
3508 import 'polyfill';
3509 import './local-setup';
3510 export default {};
3511 ";
3512 let imports = extract_imports(source, &js_path());
3513 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
3514 }
3515
3516 #[test]
3517 fn extract_imports_mixed_specifiers() {
3518 let source = r"
3519 import defaultExport from 'module-a';
3520 import { named } from 'module-b';
3521 import * as ns from 'module-c';
3522 export default {};
3523 ";
3524 let imports = extract_imports(source, &js_path());
3525 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
3526 }
3527
3528 #[test]
3529 fn template_literal_in_string_or_array() {
3530 let source = r"export default { entry: `./src/index.ts` };";
3531 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
3532 assert_eq!(result, vec!["./src/index.ts"]);
3533 }
3534
3535 #[test]
3536 fn template_literal_in_config_string() {
3537 let source = r"export default { testDir: `./tests` };";
3538 let val = extract_config_string(source, &js_path(), &["testDir"]);
3539 assert_eq!(val, Some("./tests".to_string()));
3540 }
3541
3542 #[test]
3543 fn template_literal_command_recovers_static_command_tokens() {
3544 let source = r"
3545 const PORT = 3000;
3546 export default {
3547 webServer: {
3548 command: `pnpm exec srvx --port ${PORT} --hostname 127.0.0.1`
3549 }
3550 };
3551 ";
3552 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
3553 assert_eq!(
3554 val,
3555 Some("pnpm exec srvx --port --hostname 127.0.0.1".to_string())
3556 );
3557 }
3558
3559 #[test]
3560 fn template_literal_command_skips_dynamic_prefix() {
3561 let source = r"
3562 export default {
3563 webServer: { command: `${serverCommand} && pnpm exec srvx` }
3564 };
3565 ";
3566 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
3567 assert!(val.is_none());
3568 }
3569
3570 #[test]
3571 fn template_literal_command_skips_split_static_token() {
3572 let source = r"
3573 export default {
3574 webServer: { command: `pnpm exec sr${part}vx --port 3000` }
3575 };
3576 ";
3577 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
3578 assert!(val.is_none());
3579 }
3580
3581 #[test]
3582 fn array_object_command_pairs_recover_template_command() {
3583 let source = r"
3584 const PORT = 3000;
3585 export default {
3586 webServer: [
3587 {
3588 command: `pnpm exec srvx --port ${PORT}`,
3589 cwd: 'apps/web'
3590 }
3591 ]
3592 };
3593 ";
3594 let pairs = extract_config_array_object_command_pairs(
3595 source,
3596 &ts_path(),
3597 &["webServer"],
3598 "command",
3599 "cwd",
3600 );
3601 assert_eq!(
3602 pairs,
3603 vec![(
3604 "pnpm exec srvx --port ".to_string(),
3605 Some("apps/web".to_string())
3606 )]
3607 );
3608 }
3609
3610 #[test]
3611 fn nested_string_array_empty_path() {
3612 let source = r#"export default { items: ["a", "b"] };"#;
3613 let result = extract_config_string_array(source, &js_path(), &[]);
3614 assert!(result.is_empty());
3615 }
3616
3617 #[test]
3618 fn nested_string_empty_path() {
3619 let source = r#"export default { key: "val" };"#;
3620 let result = extract_config_string(source, &js_path(), &[]);
3621 assert!(result.is_none());
3622 }
3623
3624 #[test]
3625 fn object_keys_empty_path() {
3626 let source = r"export default { plugins: {} };";
3627 let result = extract_config_object_keys(source, &js_path(), &[]);
3628 assert!(result.is_empty());
3629 }
3630
3631 #[test]
3632 fn no_config_object_returns_empty() {
3633 let source = r"const x = 42;";
3634 let result = extract_config_string(source, &js_path(), &["key"]);
3635 assert!(result.is_none());
3636
3637 let arr = extract_config_string_array(source, &js_path(), &["items"]);
3638 assert!(arr.is_empty());
3639
3640 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3641 assert!(keys.is_empty());
3642 }
3643
3644 #[test]
3645 fn property_with_string_key() {
3646 let source = r#"export default { "string-key": "value" };"#;
3647 let val = extract_config_string(source, &js_path(), &["string-key"]);
3648 assert_eq!(val, Some("value".to_string()));
3649 }
3650
3651 #[test]
3652 fn nested_navigation_through_non_object() {
3653 let source = r#"export default { level1: "not-an-object" };"#;
3654 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
3655 assert!(val.is_none());
3656 }
3657
3658 #[test]
3659 fn variable_reference_untyped() {
3660 let source = r#"
3661 const config = {
3662 testDir: "./tests"
3663 };
3664 export default config;
3665 "#;
3666 let val = extract_config_string(source, &js_path(), &["testDir"]);
3667 assert_eq!(val, Some("./tests".to_string()));
3668 }
3669
3670 #[test]
3671 fn variable_reference_with_type_annotation() {
3672 let source = r#"
3673 import type { StorybookConfig } from '@storybook/react-vite';
3674 const config: StorybookConfig = {
3675 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
3676 framework: "@storybook/react-vite"
3677 };
3678 export default config;
3679 "#;
3680 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
3681 assert_eq!(
3682 addons,
3683 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
3684 );
3685
3686 let framework = extract_config_string(source, &ts_path(), &["framework"]);
3687 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
3688 }
3689
3690 #[test]
3691 fn variable_reference_with_define_config() {
3692 let source = r#"
3693 import { defineConfig } from 'vitest/config';
3694 const config = defineConfig({
3695 test: {
3696 include: ["**/*.test.ts"]
3697 }
3698 });
3699 export default config;
3700 "#;
3701 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
3702 assert_eq!(include, vec!["**/*.test.ts"]);
3703 }
3704
3705 #[test]
3706 fn ts_satisfies_direct_export() {
3707 let source = r#"
3708 export default {
3709 testDir: "./tests"
3710 } satisfies PlaywrightTestConfig;
3711 "#;
3712 let val = extract_config_string(source, &ts_path(), &["testDir"]);
3713 assert_eq!(val, Some("./tests".to_string()));
3714 }
3715
3716 #[test]
3717 fn ts_as_direct_export() {
3718 let source = r#"
3719 export default {
3720 testDir: "./tests"
3721 } as const;
3722 "#;
3723 let val = extract_config_string(source, &ts_path(), &["testDir"]);
3724 assert_eq!(val, Some("./tests".to_string()));
3725 }
3726
3727 fn aliases(source: &str) -> Vec<(String, String)> {
3730 extract_config_aliases(source, &js_path(), &["resolve", "alias"])
3731 }
3732
3733 #[test]
3734 fn aliases_inline_object_still_extracted() {
3735 let source = r#"
3737 export default defineConfig({
3738 resolve: { alias: { "@": "./src", utils: "../../utils" } }
3739 });
3740 "#;
3741 let mut got = aliases(source);
3742 got.sort();
3743 assert_eq!(
3744 got,
3745 vec![
3746 ("@".to_string(), "./src".to_string()),
3747 ("utils".to_string(), "../../utils".to_string()),
3748 ]
3749 );
3750 }
3751
3752 #[test]
3753 fn aliases_inline_array_still_extracted() {
3754 let source = r#"
3755 export default defineConfig({
3756 resolve: { alias: [{ find: "@", replacement: "./src" }] }
3757 });
3758 "#;
3759 assert_eq!(
3760 aliases(source),
3761 vec![("@".to_string(), "./src".to_string())]
3762 );
3763 }
3764
3765 #[test]
3766 fn aliases_local_const_array_identifier() {
3767 let source = r#"
3768 const sharedAliases = [{ find: "@", replacement: "./src" }];
3769 export default defineConfig({ resolve: { alias: sharedAliases } });
3770 "#;
3771 assert_eq!(
3772 aliases(source),
3773 vec![("@".to_string(), "./src".to_string())]
3774 );
3775 }
3776
3777 #[test]
3778 fn aliases_local_const_object_identifier() {
3779 let source = r#"
3780 const sharedAliases = { "@": "./src" };
3781 export default defineConfig({ resolve: { alias: sharedAliases } });
3782 "#;
3783 assert_eq!(
3784 aliases(source),
3785 vec![("@".to_string(), "./src".to_string())]
3786 );
3787 }
3788
3789 #[test]
3790 fn aliases_array_spread_of_identifiers_and_inline() {
3791 let source = r##"
3792 const a = [{ find: "@", replacement: "./src" }];
3793 const b = [{ find: "~", replacement: "./lib" }];
3794 export default defineConfig({
3795 resolve: { alias: [...a, ...b, { find: "#", replacement: "./test" }] }
3796 });
3797 "##;
3798 let mut got = aliases(source);
3799 got.sort();
3800 assert_eq!(
3801 got,
3802 vec![
3803 ("#".to_string(), "./test".to_string()),
3804 ("@".to_string(), "./src".to_string()),
3805 ("~".to_string(), "./lib".to_string()),
3806 ]
3807 );
3808 }
3809
3810 #[test]
3811 fn aliases_object_spread_of_identifier_and_inline() {
3812 let source = r#"
3813 const base = { "@": "./src" };
3814 export default defineConfig({
3815 resolve: { alias: { ...base, "~": "./lib" } }
3816 });
3817 "#;
3818 let mut got = aliases(source);
3819 got.sort();
3820 assert_eq!(
3821 got,
3822 vec![
3823 ("@".to_string(), "./src".to_string()),
3824 ("~".to_string(), "./lib".to_string()),
3825 ]
3826 );
3827 }
3828
3829 #[test]
3830 fn aliases_local_const_chained_identifier() {
3831 let source = r#"
3833 const real = [{ find: "@", replacement: "./src" }];
3834 const alias2 = real;
3835 export default defineConfig({ resolve: { alias: alias2 } });
3836 "#;
3837 assert_eq!(
3838 aliases(source),
3839 vec![("@".to_string(), "./src".to_string())]
3840 );
3841 }
3842
3843 #[test]
3844 fn aliases_imported_named_identifier_from_sibling() {
3845 let dir = tempfile::tempdir().unwrap();
3846 std::fs::write(
3847 dir.path().join("vite.shared.js"),
3848 r#"export const sharedAliases = [
3849 { find: "@", replacement: new URL("./src", import.meta.url).pathname },
3850 ];"#,
3851 )
3852 .unwrap();
3853 let config = dir.path().join("vite.config.js");
3854 let source = r#"
3855 import { defineConfig } from "vite";
3856 import { sharedAliases } from "./vite.shared.js";
3857 export default defineConfig({ resolve: { alias: sharedAliases } });
3858 "#;
3859 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
3860 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
3861 }
3862
3863 #[test]
3864 fn aliases_imported_extensionless_specifier_probed() {
3865 let dir = tempfile::tempdir().unwrap();
3866 std::fs::write(
3867 dir.path().join("aliases.mjs"),
3868 r#"export const sharedAliases = { "@": "./src" };"#,
3869 )
3870 .unwrap();
3871 let config = dir.path().join("vite.config.ts");
3872 let source = r#"
3873 import { sharedAliases } from "./aliases";
3874 export default defineConfig({ resolve: { alias: sharedAliases } });
3875 "#;
3876 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
3877 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
3878 }
3879
3880 #[test]
3881 fn aliases_imported_default_export_from_sibling() {
3882 let dir = tempfile::tempdir().unwrap();
3883 std::fs::write(
3884 dir.path().join("aliases.js"),
3885 r#"export default [{ find: "@", replacement: "./src" }];"#,
3886 )
3887 .unwrap();
3888 let config = dir.path().join("vite.config.js");
3889 let source = r#"
3890 import sharedAliases from "./aliases.js";
3891 export default defineConfig({ resolve: { alias: sharedAliases } });
3892 "#;
3893 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
3894 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
3895 }
3896
3897 #[test]
3898 fn aliases_imported_spread_from_two_siblings() {
3899 let dir = tempfile::tempdir().unwrap();
3900 std::fs::write(
3901 dir.path().join("a.js"),
3902 r#"export const a = [{ find: "@", replacement: "./src" }];"#,
3903 )
3904 .unwrap();
3905 std::fs::write(
3906 dir.path().join("b.js"),
3907 r#"export const b = [{ find: "~", replacement: "./lib" }];"#,
3908 )
3909 .unwrap();
3910 let config = dir.path().join("vite.config.js");
3911 let source = r#"
3912 import { a } from "./a.js";
3913 import { b } from "./b.js";
3914 export default defineConfig({ resolve: { alias: [...a, ...b] } });
3915 "#;
3916 let mut got = extract_config_aliases(source, &config, &["resolve", "alias"]);
3917 got.sort();
3918 assert_eq!(
3919 got,
3920 vec![
3921 ("@".to_string(), "./src".to_string()),
3922 ("~".to_string(), "./lib".to_string()),
3923 ]
3924 );
3925 }
3926
3927 #[test]
3928 fn aliases_import_cycle_terminates() {
3929 let dir = tempfile::tempdir().unwrap();
3932 std::fs::write(
3933 dir.path().join("a.js"),
3934 r#"import { b } from "./b.js";
3935 export const a = [{ find: "@", replacement: "./src" }, ...b];"#,
3936 )
3937 .unwrap();
3938 std::fs::write(
3939 dir.path().join("b.js"),
3940 r#"import { a } from "./a.js";
3941 export const b = [...a];"#,
3942 )
3943 .unwrap();
3944 let config = dir.path().join("vite.config.js");
3945 let source = r#"
3946 import { a } from "./a.js";
3947 export default defineConfig({ resolve: { alias: a } });
3948 "#;
3949 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
3950 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
3951 }
3952
3953 #[test]
3954 fn aliases_non_relative_import_not_followed() {
3955 let source = r#"
3958 import { sharedAliases } from "some-pkg";
3959 export default defineConfig({ resolve: { alias: sharedAliases } });
3960 "#;
3961 let dir = tempfile::tempdir().unwrap();
3962 let config = dir.path().join("vite.config.js");
3963 assert!(extract_config_aliases(source, &config, &["resolve", "alias"]).is_empty());
3964 }
3965
3966 #[test]
3967 fn aliases_object_array_value_takes_first_entry() {
3968 let source = r#"
3973 export default {
3974 compilerOptions: { paths: { "@/*": ["./src/*"], "~/*": ["./lib/*", "./vendor/*"] } }
3975 };
3976 "#;
3977 let mut got = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
3978 got.sort();
3979 assert_eq!(
3980 got,
3981 vec![
3982 ("@/*".to_string(), "./src/*".to_string()),
3983 ("~/*".to_string(), "./lib/*".to_string()),
3984 ]
3985 );
3986 }
3987
3988 #[test]
3989 fn aliases_kinded_preserves_is_bare_through_resolution() {
3990 let source = r#"
3993 const a = [{ find: "lodash-es", replacement: "lodash" }];
3994 export default defineConfig({
3995 resolve: { alias: [...a, { find: "@", replacement: "./src" }] }
3996 });
3997 "#;
3998 let mut got = extract_config_aliases_kinded(source, &js_path(), &["resolve", "alias"]);
3999 got.sort();
4000 assert_eq!(
4001 got,
4002 vec![
4003 ("@".to_string(), "./src".to_string(), false),
4004 ("lodash-es".to_string(), "lodash".to_string(), true),
4005 ]
4006 );
4007 }
4008
4009 #[test]
4010 fn aliases_kinded_preserves_is_bare_through_imported_spread() {
4011 let dir = tempfile::tempdir().unwrap();
4012 std::fs::write(
4013 dir.path().join("aliases.js"),
4014 r#"export const packageAliases = [{ find: "lodash-es", replacement: "lodash" }];"#,
4015 )
4016 .unwrap();
4017 let config = dir.path().join("vite.config.js");
4018 let source = r#"
4019 import { packageAliases } from "./aliases.js";
4020 export default defineConfig({
4021 resolve: { alias: [...packageAliases, { find: "@", replacement: "./src" }] }
4022 });
4023 "#;
4024 let mut got = extract_config_aliases_kinded(source, &config, &["resolve", "alias"]);
4025 got.sort();
4026 assert_eq!(
4027 got,
4028 vec![
4029 ("@".to_string(), "./src".to_string(), false),
4030 ("lodash-es".to_string(), "lodash".to_string(), true),
4031 ]
4032 );
4033 }
4034
4035 #[test]
4038 fn extract_command_string_literal() {
4039 let source = r#"export default { start: "node server.js" };"#;
4040 let val = extract_config_command(source, &js_path(), &["start"]);
4041 assert_eq!(val, Some("node server.js".to_string()));
4042 }
4043
4044 #[test]
4045 fn extract_command_nested_path() {
4046 let source = r#"
4047 export default {
4048 scripts: {
4049 dev: "vite dev"
4050 }
4051 };
4052 "#;
4053 let val = extract_config_command(source, &js_path(), &["scripts", "dev"]);
4054 assert_eq!(val, Some("vite dev".to_string()));
4055 }
4056
4057 #[test]
4058 fn extract_command_missing_key_returns_none() {
4059 let source = r#"export default { other: "val" };"#;
4060 let val = extract_config_command(source, &js_path(), &["start"]);
4061 assert!(val.is_none());
4062 }
4063
4064 #[test]
4065 fn extract_command_ts_as_expression() {
4066 let source = r#"export default { start: "node server.js" as string };"#;
4067 let val = extract_config_command(source, &ts_path(), &["start"]);
4068 assert_eq!(val, Some("node server.js".to_string()));
4069 }
4070
4071 #[test]
4072 fn extract_command_ts_satisfies_expression() {
4073 let source = r#"export default { start: "node server.js" satisfies string };"#;
4074 let val = extract_config_command(source, &ts_path(), &["start"]);
4075 assert_eq!(val, Some("node server.js".to_string()));
4076 }
4077
4078 #[test]
4079 fn extract_command_parenthesized_expression() {
4080 let source = r#"export default { start: ("node server.js") };"#;
4081 let val = extract_config_command(source, &js_path(), &["start"]);
4082 assert_eq!(val, Some("node server.js".to_string()));
4083 }
4084
4085 #[test]
4086 fn extract_command_empty_path_returns_none() {
4087 let source = r#"export default { start: "node server.js" };"#;
4088 let val = extract_config_command(source, &js_path(), &[]);
4089 assert!(val.is_none());
4090 }
4091
4092 #[test]
4095 fn truthy_bool_or_object_with_true_value() {
4096 let source = r"export default { typescript: true };";
4097 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4098 assert!(result);
4099 }
4100
4101 #[test]
4102 fn truthy_bool_or_object_with_false_value() {
4103 let source = r"export default { typescript: false };";
4104 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4105 assert!(!result);
4106 }
4107
4108 #[test]
4109 fn truthy_bool_or_object_with_object_value() {
4110 let source = r#"export default { typescript: { reactDocgen: "react-docgen" } };"#;
4111 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4112 assert!(result);
4113 }
4114
4115 #[test]
4116 fn truthy_bool_or_object_missing_key_returns_false() {
4117 let source = r"export default { other: true };";
4118 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4119 assert!(!result);
4120 }
4121
4122 #[test]
4123 fn truthy_bool_or_object_with_string_value_returns_false() {
4124 let source = r#"export default { typescript: "yes" };"#;
4126 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4127 assert!(!result);
4128 }
4129
4130 #[test]
4131 fn truthy_bool_or_object_ts_satisfies_wrapper() {
4132 let source = r"export default { typescript: (true satisfies boolean) };";
4133 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4134 assert!(result);
4135 }
4136
4137 #[test]
4138 fn truthy_bool_or_object_ts_as_wrapper() {
4139 let source = r"export default { typescript: (true as boolean) };";
4140 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4141 assert!(result);
4142 }
4143
4144 #[test]
4145 fn truthy_bool_or_object_parenthesized_wrapper() {
4146 let source = r"export default { typescript: (true) };";
4147 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4148 assert!(result);
4149 }
4150
4151 #[test]
4157 fn static_dir_entries_object_form_exercises_property_string() {
4158 let source = r#"
4161 export default {
4162 staticDirs: [
4163 { from: "./media", to: "/assets" }
4164 ]
4165 };
4166 "#;
4167 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
4168 assert_eq!(
4169 entries,
4170 vec![("./media".to_string(), Some("/assets".to_string()))]
4171 );
4172 }
4173
4174 #[test]
4177 fn expression_to_path_values_array_form_via_config_path() {
4178 let source = r#"export default { entries: ["./src/a.ts", "./src/b.ts"] };"#;
4181 let result = extract_config_string_or_array(source, &js_path(), &["entries"]);
4182 assert_eq!(result, vec!["./src/a.ts", "./src/b.ts"]);
4183 }
4184
4185 #[test]
4188 fn array_nested_aliases_object_form() {
4189 let source = r#"
4190 export default {
4191 test: {
4192 projects: [
4193 {
4194 resolve: {
4195 alias: { "@": "./src" }
4196 }
4197 }
4198 ]
4199 }
4200 };
4201 "#;
4202 let aliases = extract_config_array_nested_aliases(
4203 source,
4204 &ts_path(),
4205 &["test", "projects"],
4206 &["resolve", "alias"],
4207 );
4208 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
4209 }
4210
4211 #[test]
4212 fn array_nested_aliases_array_form_find_replacement() {
4213 let source = r#"
4214 export default {
4215 projects: [
4216 {
4217 resolve: {
4218 alias: [
4219 { find: "@", replacement: "./src" },
4220 { find: "~", replacement: "./lib" }
4221 ]
4222 }
4223 }
4224 ]
4225 };
4226 "#;
4227 let aliases = extract_config_array_nested_aliases(
4228 source,
4229 &ts_path(),
4230 &["projects"],
4231 &["resolve", "alias"],
4232 );
4233 assert_eq!(
4234 aliases,
4235 vec![
4236 ("@".to_string(), "./src".to_string()),
4237 ("~".to_string(), "./lib".to_string()),
4238 ]
4239 );
4240 }
4241
4242 #[test]
4243 fn array_nested_aliases_empty_when_path_is_not_array() {
4244 let source = r#"export default { test: { projects: "not-an-array" } };"#;
4245 let aliases = extract_config_array_nested_aliases(
4246 source,
4247 &ts_path(),
4248 &["test", "projects"],
4249 &["resolve", "alias"],
4250 );
4251 assert!(aliases.is_empty());
4252 }
4253
4254 #[test]
4255 fn array_nested_aliases_kinded_tracks_is_bare() {
4256 let source = r#"
4257 export default {
4258 projects: [
4259 {
4260 resolve: {
4261 alias: [
4262 { find: "lodash-es", replacement: "lodash" },
4263 { find: "@", replacement: "./src" }
4264 ]
4265 }
4266 }
4267 ]
4268 };
4269 "#;
4270 let mut aliases = extract_config_array_nested_aliases_kinded(
4271 source,
4272 &ts_path(),
4273 &["projects"],
4274 &["resolve", "alias"],
4275 );
4276 aliases.sort();
4277 assert_eq!(
4278 aliases,
4279 vec![
4280 ("@".to_string(), "./src".to_string(), false),
4281 ("lodash-es".to_string(), "lodash".to_string(), true),
4282 ]
4283 );
4284 }
4285
4286 #[test]
4289 fn default_export_array_aliases_kinded_extracts_from_workspace_config() {
4290 let source = r#"
4291 export default [
4292 {
4293 resolve: {
4294 alias: { "@": "./src" }
4295 }
4296 },
4297 {
4298 resolve: {
4299 alias: [{ find: "~", replacement: "./lib" }]
4300 }
4301 }
4302 ];
4303 "#;
4304 let mut aliases =
4305 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4306 aliases.sort();
4307 assert_eq!(
4308 aliases,
4309 vec![
4310 ("@".to_string(), "./src".to_string(), false),
4311 ("~".to_string(), "./lib".to_string(), false),
4312 ]
4313 );
4314 }
4315
4316 #[test]
4317 fn default_export_array_aliases_kinded_define_workspace_wrapper() {
4318 let source = r#"
4319 export default defineWorkspace([
4320 {
4321 resolve: { alias: { "@": "./src" } }
4322 }
4323 ]);
4324 "#;
4325 let aliases =
4326 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4327 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
4328 }
4329
4330 #[test]
4331 fn default_export_array_aliases_kinded_empty_when_no_alias_path() {
4332 let source = r#"
4333 export default [
4334 { test: { include: ["**/*.test.ts"] } }
4335 ];
4336 "#;
4337 let aliases =
4338 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4339 assert!(aliases.is_empty());
4340 }
4341
4342 #[test]
4345 fn config_default_export_unreachable_when_no_export() {
4346 let source = r"const x = 42;";
4347 assert!(config_default_export_unreachable(source, &js_path()));
4348 }
4349
4350 #[test]
4351 fn config_default_export_unreachable_false_for_object_export() {
4352 let source = r#"export default { key: "value" };"#;
4353 assert!(!config_default_export_unreachable(source, &js_path()));
4354 }
4355
4356 #[test]
4357 fn config_default_export_unreachable_false_for_array_export() {
4358 let source = r#"export default ["a", "b"];"#;
4359 assert!(!config_default_export_unreachable(source, &js_path()));
4360 }
4361
4362 #[test]
4363 fn config_default_export_unreachable_true_for_function_without_return_object() {
4364 let source = r"export default function config() { return 42; }";
4366 assert!(config_default_export_unreachable(source, &js_path()));
4367 }
4368
4369 #[test]
4372 fn static_dir_entries_string_and_object_form() {
4373 let source = r#"
4374 export default {
4375 staticDirs: [
4376 "./public",
4377 { from: "../assets", to: "/static" }
4378 ]
4379 };
4380 "#;
4381 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
4382 assert_eq!(
4383 entries,
4384 vec![
4385 ("./public".to_string(), None),
4386 ("../assets".to_string(), Some("/static".to_string())),
4387 ]
4388 );
4389 }
4390
4391 #[test]
4392 fn static_dir_entries_object_without_to() {
4393 let source = r#"
4394 export default {
4395 staticDirs: [
4396 { from: "./media" }
4397 ]
4398 };
4399 "#;
4400 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
4401 assert_eq!(entries, vec![("./media".to_string(), None)]);
4402 }
4403
4404 #[test]
4405 fn static_dir_entries_object_missing_from_skipped() {
4406 let source = r#"
4408 export default {
4409 staticDirs: [
4410 { to: "/target" },
4411 "./public"
4412 ]
4413 };
4414 "#;
4415 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
4416 assert_eq!(entries, vec![("./public".to_string(), None)]);
4417 }
4418
4419 #[test]
4420 fn static_dir_entries_empty_when_not_array() {
4421 let source = r#"export default { staticDirs: "./public" };"#;
4422 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
4423 assert!(entries.is_empty());
4424 }
4425
4426 #[test]
4429 fn aliases_array_form_missing_find_or_replacement_skipped() {
4430 let source = r#"
4432 export default {
4433 resolve: {
4434 alias: [
4435 { replacement: "./src" },
4436 { find: "@" },
4437 { find: "~", replacement: "./lib" }
4438 ]
4439 }
4440 };
4441 "#;
4442 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
4443 assert_eq!(aliases, vec![("~".to_string(), "./lib".to_string())]);
4444 }
4445
4446 #[test]
4447 fn aliases_object_form_computed_key_skipped() {
4448 let source = r#"
4450 const k = "@";
4451 export default {
4452 resolve: {
4453 alias: {
4454 [k]: "./src",
4455 "~": "./lib"
4456 }
4457 }
4458 };
4459 "#;
4460 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
4461 assert_eq!(aliases, vec![("~".to_string(), "./lib".to_string())]);
4463 }
4464
4465 #[test]
4466 fn aliases_kinded_array_form_path_replacement_is_not_bare() {
4467 let source = r#"
4468 export default {
4469 resolve: {
4470 alias: [{ find: "@", replacement: "./src" }]
4471 }
4472 };
4473 "#;
4474 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4475 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
4476 }
4477
4478 #[test]
4479 fn aliases_kinded_object_form_bare_and_path_discrimination() {
4480 let source = r#"
4481 export default {
4482 resolve: {
4483 alias: {
4484 "lodash-es": "lodash",
4485 "@": "./src"
4486 }
4487 }
4488 };
4489 "#;
4490 let mut aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4491 aliases.sort();
4492 assert_eq!(
4493 aliases,
4494 vec![
4495 ("@".to_string(), "./src".to_string(), false),
4496 ("lodash-es".to_string(), "lodash".to_string(), true),
4497 ]
4498 );
4499 }
4500
4501 #[test]
4502 fn aliases_kinded_parent_relative_replacement_is_not_bare() {
4503 let source = r#"
4504 export default {
4505 resolve: { alias: { "@": "../shared/src" } }
4506 };
4507 "#;
4508 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4509 assert_eq!(
4510 aliases,
4511 vec![("@".to_string(), "../shared/src".to_string(), false)]
4512 );
4513 }
4514
4515 #[test]
4516 fn aliases_kinded_absolute_replacement_is_not_bare() {
4517 let source = r#"
4518 export default {
4519 resolve: { alias: { "@": "/absolute/path" } }
4520 };
4521 "#;
4522 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4523 assert_eq!(
4524 aliases,
4525 vec![("@".to_string(), "/absolute/path".to_string(), false)]
4526 );
4527 }
4528
4529 #[test]
4532 fn default_export_array_ts_as_wrapper() {
4533 let source = r"export default [] as string[];";
4535 assert!(!config_default_export_unreachable(source, &js_path()));
4536 }
4537
4538 #[test]
4539 fn default_export_array_ts_satisfies_wrapper() {
4540 let source = r"export default [] satisfies string[];";
4541 assert!(!config_default_export_unreachable(source, &ts_path()));
4542 }
4543
4544 #[test]
4545 fn default_export_array_define_config_call_wrapper() {
4546 let source = r#"export default defineConfig(["**/*.test.ts"]);"#;
4547 assert!(!config_default_export_unreachable(source, &ts_path()));
4548 }
4549
4550 #[test]
4553 fn shallow_strings_object_with_string_values() {
4554 let source = r#"
4556 export default {
4557 plugins: {
4558 autoprefixer: "autoprefixer",
4559 tailwindcss: "tailwindcss"
4560 }
4561 };
4562 "#;
4563 let vals = extract_config_shallow_strings(source, &js_path(), "plugins");
4564 assert!(vals.contains(&"autoprefixer".to_string()));
4565 assert!(vals.contains(&"tailwindcss".to_string()));
4566 }
4567
4568 #[test]
4569 fn shallow_strings_object_with_sub_array_first_element() {
4570 let source = r#"
4572 export default {
4573 reporters: {
4574 main: ["jest-junit", { outputFile: "report.xml" }],
4575 alt: ["html-reporter"]
4576 }
4577 };
4578 "#;
4579 let vals = extract_config_shallow_strings(source, &js_path(), "reporters");
4580 assert!(vals.contains(&"jest-junit".to_string()));
4581 assert!(vals.contains(&"html-reporter".to_string()));
4582 }
4583
4584 #[test]
4587 fn shallow_strings_or_object_property_non_array_single_string() {
4588 let source = r#"export default { jsPlugins: "eslint-plugin-foo" };"#;
4590 let vals = extract_config_shallow_strings_or_object_property(
4591 source,
4592 &ts_path(),
4593 "jsPlugins",
4594 "specifier",
4595 );
4596 assert_eq!(vals, vec!["eslint-plugin-foo"]);
4597 }
4598
4599 #[test]
4600 fn shallow_strings_or_object_property_ts_satisfies_array_element() {
4601 let source = r#"
4603 export default {
4604 jsPlugins: [
4605 ("eslint-plugin-a" satisfies string)
4606 ]
4607 };
4608 "#;
4609 let vals = extract_config_shallow_strings_or_object_property(
4610 source,
4611 &ts_path(),
4612 "jsPlugins",
4613 "specifier",
4614 );
4615 assert_eq!(vals, vec!["eslint-plugin-a"]);
4616 }
4617
4618 #[test]
4619 fn shallow_strings_or_object_property_ts_as_array_element() {
4620 let source = r#"
4621 export default {
4622 jsPlugins: [
4623 ("eslint-plugin-b" as string)
4624 ]
4625 };
4626 "#;
4627 let vals = extract_config_shallow_strings_or_object_property(
4628 source,
4629 &ts_path(),
4630 "jsPlugins",
4631 "specifier",
4632 );
4633 assert_eq!(vals, vec!["eslint-plugin-b"]);
4634 }
4635
4636 #[test]
4637 fn shallow_strings_or_object_property_sub_array_first_element_string() {
4638 let source = r#"
4640 export default {
4641 jsPlugins: [
4642 ["eslint-plugin-tuple-pkg", { options: true }]
4643 ]
4644 };
4645 "#;
4646 let vals = extract_config_shallow_strings_or_object_property(
4647 source,
4648 &ts_path(),
4649 "jsPlugins",
4650 "specifier",
4651 );
4652 assert_eq!(vals, vec!["eslint-plugin-tuple-pkg"]);
4653 }
4654
4655 #[test]
4658 fn array_object_command_pairs_basic() {
4659 let source = r#"
4660 export default {
4661 webServer: [
4662 { command: "node server.js", cwd: "packages/api" },
4663 { command: "vite dev" }
4664 ]
4665 };
4666 "#;
4667 let pairs = extract_config_array_object_command_pairs(
4668 source,
4669 &ts_path(),
4670 &["webServer"],
4671 "command",
4672 "cwd",
4673 );
4674 assert_eq!(
4675 pairs,
4676 vec![
4677 (
4678 "node server.js".to_string(),
4679 Some("packages/api".to_string())
4680 ),
4681 ("vite dev".to_string(), None),
4682 ]
4683 );
4684 }
4685
4686 #[test]
4687 fn array_object_command_pairs_skips_missing_command() {
4688 let source = r#"
4689 export default {
4690 webServer: [
4691 { cwd: "packages/api" },
4692 { command: "vite dev", cwd: "apps/web" }
4693 ]
4694 };
4695 "#;
4696 let pairs = extract_config_array_object_command_pairs(
4697 source,
4698 &ts_path(),
4699 &["webServer"],
4700 "command",
4701 "cwd",
4702 );
4703 assert_eq!(
4704 pairs,
4705 vec![("vite dev".to_string(), Some("apps/web".to_string()))]
4706 );
4707 }
4708
4709 #[test]
4710 fn array_object_command_pairs_empty_when_not_array() {
4711 let source = r#"export default { webServer: { command: "vite dev" } };"#;
4712 let pairs = extract_config_array_object_command_pairs(
4713 source,
4714 &ts_path(),
4715 &["webServer"],
4716 "command",
4717 "cwd",
4718 );
4719 assert!(pairs.is_empty());
4720 }
4721
4722 #[test]
4725 fn normalize_config_path_empty_string_returns_none() {
4726 let config_path = PathBuf::from("/project/vite.config.ts");
4727 let root = PathBuf::from("/project");
4728 assert_eq!(normalize_config_path("", &config_path, &root), None);
4729 }
4730
4731 #[test]
4732 fn normalize_config_path_escapes_to_above_root_returns_none() {
4733 let config_path = PathBuf::from("/project/vite.config.ts");
4734 let root = PathBuf::from("/project");
4735 assert_eq!(
4737 normalize_config_path("../../etc", &config_path, &root),
4738 None
4739 );
4740 }
4741
4742 #[test]
4743 fn normalize_config_path_dot_slash_resolves_relative_to_config_dir() {
4744 let config_path = PathBuf::from("/project/packages/app/vite.config.ts");
4745 let root = PathBuf::from("/project");
4746 assert_eq!(
4747 normalize_config_path("./src", &config_path, &root),
4748 Some("packages/app/src".to_string())
4749 );
4750 }
4751
4752 #[test]
4755 fn json_config_array_of_arrays_via_shallow_strings() {
4756 let source = r#"{"reporters": ["default", ["jest-junit", {}]]}"#;
4758 let vals = extract_config_shallow_strings(source, &json_path(), "reporters");
4759 assert_eq!(vals, vec!["default", "jest-junit"]);
4760 }
4761
4762 #[test]
4765 fn extract_config_path_string_literal() {
4766 let source = r#"export default { outDir: "./dist" };"#;
4767 let path = extract_config_path(source, &js_path(), &["outDir"]);
4768 assert_eq!(
4769 path.map(|p| p.to_string_lossy().replace('\\', "/")),
4770 Some("./dist".to_string())
4771 );
4772 }
4773
4774 #[test]
4775 fn extract_config_path_with_resolve_call() {
4776 let source = r#"
4777 import { resolve } from "node:path";
4778 export default { outDir: resolve(__dirname, "dist") };
4779 "#;
4780 let path = extract_config_path(source, &js_path(), &["outDir"]);
4781 assert_eq!(
4782 path.map(|p| p.to_string_lossy().replace('\\', "/")),
4783 Some("dist".to_string())
4784 );
4785 }
4786
4787 #[test]
4788 fn extract_config_path_missing_key_returns_none() {
4789 let source = r#"export default { other: "val" };"#;
4790 let path = extract_config_path(source, &js_path(), &["outDir"]);
4791 assert!(path.is_none());
4792 }
4793
4794 #[test]
4797 fn extract_imports_and_requires_both_forms() {
4798 let source = r"
4799 import foo from 'foo-pkg';
4800 require('bar-pkg');
4801 export default {};
4802 ";
4803 let sources = extract_imports_and_requires(source, &js_path());
4804 assert!(sources.contains(&"foo-pkg".to_string()));
4805 assert!(sources.contains(&"bar-pkg".to_string()));
4806 }
4807
4808 #[test]
4809 fn extract_imports_and_requires_skips_non_require_calls() {
4810 let source = r"
4811 import foo from 'foo-pkg';
4812 someOtherCall('bar-pkg');
4813 export default {};
4814 ";
4815 let sources = extract_imports_and_requires(source, &js_path());
4816 assert_eq!(sources, vec!["foo-pkg"]);
4817 }
4818
4819 #[test]
4822 fn nested_shallow_strings_non_object_nested_returns_empty() {
4823 let source = r#"export default { test: "not-an-object" };"#;
4825 let vals =
4826 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4827 assert!(vals.is_empty());
4828 }
4829
4830 #[test]
4833 fn vite_react_babel_dependencies_namespace_import() {
4834 let source = r#"
4835 import * as react from "@vitejs/plugin-react";
4836
4837 export default defineConfig({
4838 plugins: [
4839 react.default({
4840 babel: {
4841 plugins: ["babel-plugin-ns"],
4842 },
4843 }),
4844 ],
4845 });
4846 "#;
4847 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
4848 assert_eq!(deps, vec!["babel-plugin-ns".to_string()]);
4849 }
4850
4851 #[test]
4854 fn property_strings_deeply_nested_object_values() {
4855 let source = r#"
4857 export default {
4858 settings: {
4859 a: "val-a",
4860 b: {
4861 c: "val-c",
4862 d: ["val-d1", "val-d2"]
4863 }
4864 }
4865 };
4866 "#;
4867 let values = extract_config_property_strings(source, &js_path(), "settings");
4868 assert!(values.contains(&"val-a".to_string()));
4869 assert!(values.contains(&"val-c".to_string()));
4870 assert!(values.contains(&"val-d1".to_string()));
4871 assert!(values.contains(&"val-d2".to_string()));
4872 }
4873
4874 #[test]
4877 fn aliases_exported_const_form_resolves() {
4878 let source = r#"
4880 export const sharedAliases = { "@": "./src" };
4881 export default defineConfig({ resolve: { alias: sharedAliases } });
4882 "#;
4883 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
4884 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
4885 }
4886
4887 #[test]
4890 fn aliases_imported_from_sibling_directory_index_file() {
4891 let dir = tempfile::tempdir().unwrap();
4894 let aliases_dir = dir.path().join("aliases");
4895 std::fs::create_dir_all(&aliases_dir).unwrap();
4896 std::fs::write(
4897 aliases_dir.join("index.js"),
4898 r#"export const aliases = [{ find: "@", replacement: "./src" }];"#,
4899 )
4900 .unwrap();
4901 let config = dir.path().join("vite.config.js");
4902 let source = r#"
4903 import { aliases } from "./aliases";
4904 export default defineConfig({ resolve: { alias: aliases } });
4905 "#;
4906 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4907 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4908 }
4909
4910 #[test]
4913 fn aliases_depth_limit_terminates_deep_chain() {
4914 let source = r#"
4917 const a9 = [{ find: "@", replacement: "./src" }];
4918 const a8 = a9;
4919 const a7 = a8;
4920 const a6 = a7;
4921 const a5 = a6;
4922 const a4 = a5;
4923 const a3 = a4;
4924 const a2 = a3;
4925 const a1 = a2;
4926 export default defineConfig({ resolve: { alias: a1 } });
4927 "#;
4928 let got = extract_config_aliases(source, &js_path(), &["resolve", "alias"]);
4930 let _ = got; }
4932
4933 #[test]
4936 fn extract_aliases_file_url_to_path_new_url() {
4937 let source = r#"
4939 import { fileURLToPath, URL } from 'node:url';
4940 export default {
4941 resolve: {
4942 alias: {
4943 "@": fileURLToPath(new URL("./src", import.meta.url))
4944 }
4945 }
4946 };
4947 "#;
4948 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
4949 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
4950 }
4951
4952 #[test]
4953 fn extract_path_via_new_url_pathname_member() {
4954 let source = r#"
4956 export default {
4957 resolve: {
4958 alias: {
4959 "@": new URL("./src", import.meta.url).pathname
4960 }
4961 }
4962 };
4963 "#;
4964 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
4965 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
4966 }
4967
4968 #[test]
4971 fn truthy_bool_or_object_null_literal_returns_false() {
4972 let source = r"export default { typescript: null };";
4974 let result = extract_config_truthy_bool_or_object(source, &js_path(), &["typescript"]);
4975 assert!(!result);
4976 }
4977
4978 #[test]
4981 fn string_array_non_array_value_returns_empty() {
4982 let source = r#"export default { items: "not-an-array" };"#;
4983 let result = extract_config_string_array(source, &js_path(), &["items"]);
4984 assert!(result.is_empty());
4985 }
4986
4987 #[test]
4990 fn object_nested_empty_when_inner_value_is_not_object() {
4991 let source = r#"export default { targets: { build: "not-an-object" } };"#;
4993 let results =
4994 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
4995 assert!(results.is_empty());
4996 }
4997
4998 #[test]
5001 fn array_nested_string_or_array_missing_inner_path_returns_empty() {
5002 let source = r#"
5003 export default {
5004 test: {
5005 projects: [
5006 { test: { include: ["**/*.test.ts"] } }
5007 ]
5008 }
5009 };
5010 "#;
5011 let results = extract_config_array_nested_string_or_array(
5012 source,
5013 &ts_path(),
5014 &["test", "projects"],
5015 &["test", "setupFiles"],
5016 );
5017 assert!(results.is_empty());
5018 }
5019
5020 #[test]
5021 fn wrapped_named_const_default_export_resolves() {
5022 let source = r#"
5025 import createMDX from "@next/mdx";
5026 const nextConfig = { pageExtensions: ["ts", "tsx", "md", "mdx"] };
5027 const withMDX = createMDX({});
5028 export default withMDX(nextConfig);
5029 "#;
5030 let exts = extract_config_string_array(source, &ts_path(), &["pageExtensions"]);
5031 assert_eq!(exts, vec!["ts", "tsx", "md", "mdx"]);
5032 }
5033
5034 #[test]
5035 fn wrapped_named_const_module_exports_resolves() {
5036 let source = r#"
5038 const nextJest = require("next/jest");
5039 const createJestConfig = nextJest();
5040 const customConfig = { testMatch: ["**/*.test.ts"] };
5041 module.exports = createJestConfig(customConfig);
5042 "#;
5043 let matches = extract_config_string_array(source, &js_path(), &["testMatch"]);
5044 assert_eq!(matches, vec!["**/*.test.ts"]);
5045 }
5046
5047 #[test]
5048 fn wrapped_named_const_nested_and_curried_resolve() {
5049 let nested = r#"
5050 const nextConfig = { pageExtensions: ["mdx"] };
5051 const withMDX = (c) => c;
5052 const withFoo = (c) => c;
5053 export default withMDX(withFoo(nextConfig));
5054 "#;
5055 assert_eq!(
5056 extract_config_string_array(nested, &js_path(), &["pageExtensions"]),
5057 vec!["mdx"]
5058 );
5059
5060 let curried = r#"
5061 const nextConfig = { pageExtensions: ["md"] };
5062 const compose = (..._p) => (c) => c;
5063 export default compose(a, b)(nextConfig);
5064 "#;
5065 assert_eq!(
5066 extract_config_string_array(curried, &js_path(), &["pageExtensions"]),
5067 vec!["md"]
5068 );
5069 }
5070
5071 #[test]
5072 fn wrapped_inline_object_still_resolves() {
5073 let source = r#"
5075 const withMDX = createMDX({});
5076 export default withMDX({ pageExtensions: ["mdx"] });
5077 "#;
5078 assert_eq!(
5079 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
5080 vec!["mdx"]
5081 );
5082 }
5083}