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