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) => {
1150 if arrow.expression
1151 && !arrow.body.statements.is_empty()
1152 && let Statement::ExpressionStatement(expr_stmt) =
1153 &arrow.body.statements[0]
1154 {
1155 return extract_object_from_expression(&expr_stmt.expression);
1156 }
1157 }
1158 _ => {}
1159 }
1160 }
1161 None
1162 }
1163 Expression::ParenthesizedExpression(paren) => {
1164 extract_object_from_expression(&paren.expression)
1165 }
1166 Expression::TSSatisfiesExpression(ts_sat) => {
1167 extract_object_from_expression(&ts_sat.expression)
1168 }
1169 Expression::TSAsExpression(ts_as) => extract_object_from_expression(&ts_as.expression),
1170 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
1171 Expression::FunctionExpression(func) => extract_object_from_function(func),
1172 _ => None,
1173 }
1174}
1175
1176fn extract_object_from_arrow_function<'a>(
1177 arrow: &'a ArrowFunctionExpression<'a>,
1178) -> Option<&'a ObjectExpression<'a>> {
1179 if arrow.expression {
1180 arrow.body.statements.first().and_then(|stmt| {
1181 if let Statement::ExpressionStatement(expr_stmt) = stmt {
1182 extract_object_from_expression(&expr_stmt.expression)
1183 } else {
1184 None
1185 }
1186 })
1187 } else {
1188 extract_object_from_function_body(&arrow.body)
1189 }
1190}
1191
1192fn extract_object_from_function<'a>(func: &'a Function<'a>) -> Option<&'a ObjectExpression<'a>> {
1193 func.body
1194 .as_ref()
1195 .and_then(|body| extract_object_from_function_body(body))
1196}
1197
1198fn extract_object_from_function_body<'a>(
1199 body: &'a FunctionBody<'a>,
1200) -> Option<&'a ObjectExpression<'a>> {
1201 for stmt in &body.statements {
1202 if let Statement::ReturnStatement(ret) = stmt
1203 && let Some(argument) = &ret.argument
1204 && let Some(obj) = extract_object_from_expression(argument)
1205 {
1206 return Some(obj);
1207 }
1208 }
1209 None
1210}
1211
1212fn is_module_exports_target(target: &AssignmentTarget) -> bool {
1214 if let AssignmentTarget::StaticMemberExpression(member) = target
1215 && let Expression::Identifier(obj) = &member.object
1216 {
1217 return obj.name == "module" && member.property.name == "exports";
1218 }
1219 false
1220}
1221
1222fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
1226 match expr {
1227 Expression::Identifier(id) => Some(&id.name),
1228 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
1229 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
1230 _ => None,
1231 }
1232}
1233
1234fn find_variable_init_object<'a>(
1239 program: &'a Program,
1240 name: &str,
1241) -> Option<&'a ObjectExpression<'a>> {
1242 for stmt in &program.body {
1243 if let Statement::VariableDeclaration(decl) = stmt {
1244 for declarator in &decl.declarations {
1245 if let BindingPattern::BindingIdentifier(id) = &declarator.id
1246 && id.name == name
1247 && let Some(init) = &declarator.init
1248 {
1249 return extract_object_from_expression(init);
1250 }
1251 }
1252 }
1253 }
1254 None
1255}
1256
1257fn resolve_wrapped_config_object<'a>(
1270 program: &'a Program,
1271 expr: &'a Expression<'a>,
1272) -> Option<&'a ObjectExpression<'a>> {
1273 let call = match expr {
1274 Expression::CallExpression(call) => call,
1275 Expression::ParenthesizedExpression(paren) => {
1276 return resolve_wrapped_config_object(program, &paren.expression);
1277 }
1278 Expression::TSSatisfiesExpression(ts_sat) => {
1279 return resolve_wrapped_config_object(program, &ts_sat.expression);
1280 }
1281 Expression::TSAsExpression(ts_as) => {
1282 return resolve_wrapped_config_object(program, &ts_as.expression);
1283 }
1284 _ => return None,
1285 };
1286 for arg in &call.arguments {
1287 let Some(arg_expr) = arg.as_expression() else {
1288 continue;
1289 };
1290 if let Some(name) = unwrap_to_identifier_name(arg_expr)
1291 && let Some(obj) = find_variable_init_object(program, name)
1292 {
1293 return Some(obj);
1294 }
1295 if let Some(obj) = resolve_wrapped_config_object(program, arg_expr) {
1296 return Some(obj);
1297 }
1298 }
1299 None
1300}
1301
1302pub(crate) fn find_property<'a>(
1304 obj: &'a ObjectExpression<'a>,
1305 key: &str,
1306) -> Option<&'a ObjectProperty<'a>> {
1307 for prop in &obj.properties {
1308 if let ObjectPropertyKind::ObjectProperty(p) = prop
1309 && property_key_matches(&p.key, key)
1310 {
1311 return Some(p);
1312 }
1313 }
1314 None
1315}
1316
1317fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
1319 match key {
1320 PropertyKey::StaticIdentifier(id) => id.name == name,
1321 PropertyKey::StringLiteral(s) => s.value == name,
1322 _ => false,
1323 }
1324}
1325
1326fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
1328 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
1329}
1330
1331fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
1333 find_property(obj, key)
1334 .map(|p| expression_to_string_array(&p.value))
1335 .unwrap_or_default()
1336}
1337
1338fn get_nested_string_array_from_object(
1340 obj: &ObjectExpression,
1341 path: &[&str],
1342) -> Option<Vec<String>> {
1343 if path.is_empty() {
1344 return None;
1345 }
1346 if path.len() == 1 {
1347 return Some(get_object_string_array_property(obj, path[0]));
1348 }
1349 let prop = find_property(obj, path[0])?;
1350 if let Expression::ObjectExpression(nested) = &prop.value {
1351 get_nested_string_array_from_object(nested, &path[1..])
1352 } else {
1353 None
1354 }
1355}
1356
1357fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
1359 if path.is_empty() {
1360 return None;
1361 }
1362 if path.len() == 1 {
1363 return get_object_string_property(obj, path[0]);
1364 }
1365 let prop = find_property(obj, path[0])?;
1366 if let Expression::ObjectExpression(nested) = &prop.value {
1367 get_nested_string_from_object(nested, &path[1..])
1368 } else {
1369 None
1370 }
1371}
1372
1373fn get_nested_command_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
1375 if path.is_empty() {
1376 return None;
1377 }
1378 if path.len() == 1 {
1379 return find_property(obj, path[0]).and_then(|prop| expression_to_command(&prop.value));
1380 }
1381 let prop = find_property(obj, path[0])?;
1382 if let Expression::ObjectExpression(nested) = &prop.value {
1383 get_nested_command_from_object(nested, &path[1..])
1384 } else {
1385 None
1386 }
1387}
1388
1389pub(crate) fn expression_to_string(expr: &Expression) -> Option<String> {
1391 match expr {
1392 Expression::StringLiteral(s) => Some(s.value.to_string()),
1393 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
1394 t.quasis.first().map(|q| q.value.raw.to_string())
1395 }
1396 _ => None,
1397 }
1398}
1399
1400fn expression_to_command(expr: &Expression) -> Option<String> {
1402 match expr {
1403 Expression::StringLiteral(s) => Some(s.value.to_string()),
1404 Expression::TemplateLiteral(template) => template_literal_to_command(template),
1405 Expression::ParenthesizedExpression(paren) => expression_to_command(&paren.expression),
1406 Expression::TSAsExpression(ts_as) => expression_to_command(&ts_as.expression),
1407 Expression::TSSatisfiesExpression(ts_sat) => expression_to_command(&ts_sat.expression),
1408 _ => None,
1409 }
1410}
1411
1412fn template_literal_to_command(template: &TemplateLiteral<'_>) -> Option<String> {
1413 let first = template.quasis.first()?.value.raw.as_str();
1414 if first.trim_start().is_empty() {
1415 return None;
1416 }
1417
1418 let mut command = String::new();
1419 for (idx, quasi) in template.quasis.iter().enumerate() {
1420 command.push_str(quasi.value.raw.as_str());
1421 if idx < template.expressions.len() {
1422 let next = template
1423 .quasis
1424 .get(idx + 1)
1425 .map_or("", |next| next.value.raw.as_str());
1426 if dynamic_template_boundary_splits_static_token(quasi.value.raw.as_str(), next) {
1427 return None;
1428 }
1429 command.push(' ');
1430 }
1431 }
1432
1433 Some(command)
1434}
1435
1436fn dynamic_template_boundary_splits_static_token(before: &str, after: &str) -> bool {
1437 before
1438 .chars()
1439 .next_back()
1440 .is_some_and(is_command_token_char)
1441 && after.chars().next().is_some_and(is_command_token_char)
1442}
1443
1444fn is_command_token_char(ch: char) -> bool {
1445 !ch.is_whitespace() && !matches!(ch, '&' | '|' | ';' | '"' | '\'')
1446}
1447
1448pub(crate) fn expression_to_path_string(expr: &Expression) -> Option<String> {
1450 match expr {
1451 Expression::ParenthesizedExpression(paren) => expression_to_path_string(&paren.expression),
1452 Expression::TSAsExpression(ts_as) => expression_to_path_string(&ts_as.expression),
1453 Expression::TSSatisfiesExpression(ts_sat) => expression_to_path_string(&ts_sat.expression),
1454 Expression::StaticMemberExpression(member) if member.property.name == "pathname" => {
1455 expression_to_path_string(&member.object)
1456 }
1457 Expression::CallExpression(call) => call_expression_to_path_string(call),
1458 Expression::NewExpression(new_expr) => new_expression_to_path_string(new_expr),
1459 _ => expression_to_string(expr),
1460 }
1461}
1462
1463fn call_expression_to_path_string(call: &CallExpression) -> Option<String> {
1464 if matches!(&call.callee, Expression::Identifier(id) if id.name == "fileURLToPath") {
1465 return call
1466 .arguments
1467 .first()
1468 .and_then(Argument::as_expression)
1469 .and_then(expression_to_path_string);
1470 }
1471
1472 let callee_name = match &call.callee {
1473 Expression::Identifier(id) => Some(id.name.as_str()),
1474 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
1475 _ => None,
1476 }?;
1477
1478 if !matches!(callee_name, "resolve" | "join") {
1479 return None;
1480 }
1481
1482 let mut segments = Vec::new();
1483 for (index, arg) in call.arguments.iter().enumerate() {
1484 let expr = arg.as_expression()?;
1485
1486 if is_dirname_anchor(expr) {
1487 if index == 0 {
1488 continue;
1489 }
1490 return None;
1491 }
1492
1493 segments.push(expression_to_string(expr)?);
1494 }
1495
1496 (!segments.is_empty()).then(|| join_path_segments(&segments))
1497}
1498
1499fn is_dirname_anchor(expr: &Expression) -> bool {
1504 match expr {
1505 Expression::Identifier(id) => id.name == "__dirname",
1506 Expression::StaticMemberExpression(member) => {
1507 member.property.name == "dirname" && is_import_meta_expression(&member.object)
1508 }
1509 _ => false,
1510 }
1511}
1512
1513fn is_import_meta_expression(expr: &Expression) -> bool {
1515 matches!(
1516 expr,
1517 Expression::MetaProperty(meta) if meta.meta.name == "import" && meta.property.name == "meta"
1518 )
1519}
1520
1521fn new_expression_to_path_string(new_expr: &NewExpression) -> Option<String> {
1522 if !matches!(&new_expr.callee, Expression::Identifier(id) if id.name == "URL") {
1523 return None;
1524 }
1525
1526 let source = new_expr
1527 .arguments
1528 .first()
1529 .and_then(Argument::as_expression)
1530 .and_then(expression_to_string)?;
1531
1532 let base = new_expr
1533 .arguments
1534 .get(1)
1535 .and_then(Argument::as_expression)?;
1536 is_import_meta_url_expression(base).then_some(source)
1537}
1538
1539fn is_import_meta_url_expression(expr: &Expression) -> bool {
1540 if let Expression::StaticMemberExpression(member) = expr {
1541 member.property.name == "url" && matches!(member.object, Expression::MetaProperty(_))
1542 } else {
1543 false
1544 }
1545}
1546
1547fn join_path_segments(segments: &[String]) -> String {
1548 let mut joined = PathBuf::new();
1549 for segment in segments {
1550 joined.push(segment);
1551 }
1552 joined.to_string_lossy().replace('\\', "/")
1553}
1554
1555fn expression_to_alias_pairs(expr: &Expression) -> Vec<(String, String)> {
1556 match expr {
1557 Expression::ObjectExpression(obj) => obj
1558 .properties
1559 .iter()
1560 .filter_map(|prop| {
1561 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
1562 return None;
1563 };
1564 let find = property_key_to_string(&prop.key)?;
1565 let replacement = expression_to_path_values(&prop.value)
1566 .into_iter()
1567 .next()
1568 .map(|path| path_to_config_string(&path))?;
1569 Some((find, replacement))
1570 })
1571 .collect(),
1572 Expression::ArrayExpression(arr) => arr
1573 .elements
1574 .iter()
1575 .filter_map(|element| {
1576 let Expression::ObjectExpression(obj) = element.as_expression()? else {
1577 return None;
1578 };
1579 let find = find_property(obj, "find")
1580 .and_then(|prop| expression_to_string(&prop.value))?;
1581 let replacement = find_property(obj, "replacement")
1582 .and_then(|prop| expression_to_path_string(&prop.value))?;
1583 Some((find, replacement))
1584 })
1585 .collect(),
1586 _ => Vec::new(),
1587 }
1588}
1589
1590fn expression_to_alias_pairs_kinded(expr: &Expression) -> Vec<(String, String, bool)> {
1594 match expr {
1595 Expression::ObjectExpression(obj) => obj
1596 .properties
1597 .iter()
1598 .filter_map(|prop| {
1599 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
1600 return None;
1601 };
1602 let find = property_key_to_string(&prop.key)?;
1603 let (replacement, is_bare) = alias_replacement_kinded(&prop.value)?;
1604 Some((find, replacement, is_bare))
1605 })
1606 .collect(),
1607 Expression::ArrayExpression(arr) => arr
1608 .elements
1609 .iter()
1610 .filter_map(|element| {
1611 let Expression::ObjectExpression(obj) = element.as_expression()? else {
1612 return None;
1613 };
1614 let find = find_property(obj, "find")
1615 .and_then(|prop| expression_to_string(&prop.value))?;
1616 let (replacement, is_bare) = find_property(obj, "replacement")
1617 .and_then(|prop| alias_replacement_kinded(&prop.value))?;
1618 Some((find, replacement, is_bare))
1619 })
1620 .collect(),
1621 _ => Vec::new(),
1622 }
1623}
1624
1625fn alias_replacement_kinded(expr: &Expression) -> Option<(String, bool)> {
1632 match expr {
1633 Expression::ParenthesizedExpression(paren) => alias_replacement_kinded(&paren.expression),
1634 Expression::TSAsExpression(ts_as) => alias_replacement_kinded(&ts_as.expression),
1635 Expression::TSSatisfiesExpression(ts_sat) => alias_replacement_kinded(&ts_sat.expression),
1636 Expression::StringLiteral(s) => {
1637 let value = s.value.to_string();
1638 let is_bare =
1639 !value.starts_with("./") && !value.starts_with("../") && !value.starts_with('/');
1640 Some((value, is_bare))
1641 }
1642 Expression::ArrayExpression(arr) => arr
1646 .elements
1647 .iter()
1648 .find_map(ArrayExpressionElement::as_expression)
1649 .and_then(alias_replacement_kinded),
1650 _ => expression_to_path_string(expr).map(|value| (value, false)),
1651 }
1652}
1653
1654const MAX_ALIAS_RESOLVE_DEPTH: usize = 8;
1660
1661const ALIAS_SIBLING_EXTS: [&str; 6] = ["js", "mjs", "cjs", "ts", "mts", "cts"];
1668
1669fn resolve_alias_pairs_kinded(
1686 program: &Program,
1687 config_path: &Path,
1688 expr: &Expression,
1689 visited: &mut FxHashSet<PathBuf>,
1690 depth: usize,
1691) -> Vec<(String, String, bool)> {
1692 match expr {
1693 Expression::ParenthesizedExpression(paren) => {
1694 resolve_alias_pairs_kinded(program, config_path, &paren.expression, visited, depth)
1695 }
1696 Expression::TSAsExpression(ts_as) => {
1697 resolve_alias_pairs_kinded(program, config_path, &ts_as.expression, visited, depth)
1698 }
1699 Expression::TSSatisfiesExpression(ts_sat) => {
1700 resolve_alias_pairs_kinded(program, config_path, &ts_sat.expression, visited, depth)
1701 }
1702 Expression::ObjectExpression(obj) => {
1703 resolve_object_alias_pairs_kinded(program, config_path, obj, visited, depth)
1704 }
1705 Expression::ArrayExpression(arr) => {
1706 resolve_array_alias_pairs_kinded(program, config_path, arr, visited, depth)
1707 }
1708 Expression::Identifier(id) => {
1709 resolve_identifier_alias_pairs(program, config_path, id.name.as_str(), visited, depth)
1710 }
1711 _ => Vec::new(),
1712 }
1713}
1714
1715fn resolve_object_alias_pairs_kinded(
1718 program: &Program,
1719 config_path: &Path,
1720 obj: &ObjectExpression,
1721 visited: &mut FxHashSet<PathBuf>,
1722 depth: usize,
1723) -> Vec<(String, String, bool)> {
1724 let mut pairs = Vec::new();
1725 for prop in &obj.properties {
1726 match prop {
1727 ObjectPropertyKind::ObjectProperty(prop) => {
1728 if let Some(find) = property_key_to_string(&prop.key)
1729 && let Some((replacement, is_bare)) = alias_replacement_kinded(&prop.value)
1730 {
1731 pairs.push((find, replacement, is_bare));
1732 }
1733 }
1734 ObjectPropertyKind::SpreadProperty(spread) => {
1736 pairs.extend(resolve_alias_pairs_kinded(
1737 program,
1738 config_path,
1739 &spread.argument,
1740 visited,
1741 depth,
1742 ));
1743 }
1744 }
1745 }
1746 pairs
1747}
1748
1749fn resolve_array_alias_pairs_kinded(
1752 program: &Program,
1753 config_path: &Path,
1754 arr: &ArrayExpression,
1755 visited: &mut FxHashSet<PathBuf>,
1756 depth: usize,
1757) -> Vec<(String, String, bool)> {
1758 let mut pairs = Vec::new();
1759 for element in &arr.elements {
1760 match element {
1761 ArrayExpressionElement::SpreadElement(spread) => {
1763 pairs.extend(resolve_alias_pairs_kinded(
1764 program,
1765 config_path,
1766 &spread.argument,
1767 visited,
1768 depth,
1769 ));
1770 }
1771 _ => {
1772 if let Some(Expression::ObjectExpression(obj)) = element.as_expression()
1773 && let Some(find) = find_property(obj, "find")
1774 .and_then(|prop| expression_to_string(&prop.value))
1775 && let Some((replacement, is_bare)) = find_property(obj, "replacement")
1776 .and_then(|prop| alias_replacement_kinded(&prop.value))
1777 {
1778 pairs.push((find, replacement, is_bare));
1779 }
1780 }
1781 }
1782 }
1783 pairs
1784}
1785
1786fn resolve_identifier_alias_pairs(
1789 program: &Program,
1790 config_path: &Path,
1791 name: &str,
1792 visited: &mut FxHashSet<PathBuf>,
1793 depth: usize,
1794) -> Vec<(String, String, bool)> {
1795 if depth >= MAX_ALIAS_RESOLVE_DEPTH {
1796 return Vec::new();
1797 }
1798 if let Some(init) = find_variable_init_expression(program, name) {
1800 return resolve_alias_pairs_kinded(program, config_path, init, visited, depth + 1);
1801 }
1802 let Some((specifier, imported_name)) = find_relative_import_binding(program, name) else {
1804 return Vec::new();
1805 };
1806 resolve_imported_alias_pairs(
1807 config_path,
1808 &specifier,
1809 imported_name.as_deref(),
1810 visited,
1811 depth + 1,
1812 )
1813}
1814
1815fn resolve_imported_alias_pairs(
1818 config_path: &Path,
1819 specifier: &str,
1820 imported_name: Option<&str>,
1821 visited: &mut FxHashSet<PathBuf>,
1822 depth: usize,
1823) -> Vec<(String, String, bool)> {
1824 let Some((sibling_path, sibling_source)) = resolve_sibling_module(config_path, specifier)
1825 else {
1826 return Vec::new();
1827 };
1828 if !visited.insert(sibling_path.clone()) {
1829 return Vec::new();
1830 }
1831 extract_from_source(&sibling_source, &sibling_path, |program| {
1832 let init = find_exported_init(program, imported_name)?;
1833 let pairs = resolve_alias_pairs_kinded(program, &sibling_path, init, visited, depth);
1834 (!pairs.is_empty()).then_some(pairs)
1835 })
1836 .unwrap_or_default()
1837}
1838
1839fn find_variable_init_expression<'a>(
1844 program: &'a Program<'a>,
1845 name: &str,
1846) -> Option<&'a Expression<'a>> {
1847 for stmt in &program.body {
1848 let decl = match stmt {
1849 Statement::VariableDeclaration(decl) => decl,
1850 Statement::ExportNamedDeclaration(export) => match &export.declaration {
1851 Some(Declaration::VariableDeclaration(decl)) => decl,
1852 _ => continue,
1853 },
1854 _ => continue,
1855 };
1856 for declarator in &decl.declarations {
1857 if let BindingPattern::BindingIdentifier(id) = &declarator.id
1858 && id.name == name
1859 && let Some(init) = &declarator.init
1860 {
1861 return Some(init);
1862 }
1863 }
1864 }
1865 None
1866}
1867
1868fn find_exported_init<'a>(
1873 program: &'a Program<'a>,
1874 name: Option<&str>,
1875) -> Option<&'a Expression<'a>> {
1876 match name {
1877 Some(name) => find_variable_init_expression(program, name),
1878 None => program.body.iter().find_map(|stmt| {
1879 if let Statement::ExportDefaultDeclaration(decl) = stmt {
1880 decl.declaration.as_expression()
1881 } else {
1882 None
1883 }
1884 }),
1885 }
1886}
1887
1888fn find_relative_import_binding(program: &Program, name: &str) -> Option<(String, Option<String>)> {
1893 for stmt in &program.body {
1894 let Statement::ImportDeclaration(decl) = stmt else {
1895 continue;
1896 };
1897 let specifier = decl.source.value.as_str();
1898 if !is_relative_specifier(specifier) {
1899 continue;
1900 }
1901 let Some(specifiers) = &decl.specifiers else {
1902 continue;
1903 };
1904 for spec in specifiers {
1905 match spec {
1906 ImportDeclarationSpecifier::ImportSpecifier(spec) if spec.local.name == name => {
1907 return Some((
1908 specifier.to_string(),
1909 Some(spec.imported.name().to_string()),
1910 ));
1911 }
1912 ImportDeclarationSpecifier::ImportDefaultSpecifier(spec)
1913 if spec.local.name == name =>
1914 {
1915 return Some((specifier.to_string(), None));
1916 }
1917 _ => {}
1918 }
1919 }
1920 }
1921 None
1922}
1923
1924fn is_relative_specifier(specifier: &str) -> bool {
1927 specifier.starts_with("./") || specifier.starts_with("../") || specifier.starts_with('/')
1928}
1929
1930fn resolve_sibling_module(config_path: &Path, specifier: &str) -> Option<(PathBuf, String)> {
1936 let parent = config_path.parent().unwrap_or(config_path);
1937 let direct = parent.join(specifier);
1938 if let Ok(source) = std::fs::read_to_string(&direct) {
1939 return Some((direct, source));
1940 }
1941 for ext in ALIAS_SIBLING_EXTS {
1942 let candidate = parent.join(format!("{specifier}.{ext}"));
1943 if let Ok(source) = std::fs::read_to_string(&candidate) {
1944 return Some((candidate, source));
1945 }
1946 }
1947 for ext in ALIAS_SIBLING_EXTS {
1948 let candidate = direct.join(format!("index.{ext}"));
1949 if let Ok(source) = std::fs::read_to_string(&candidate) {
1950 return Some((candidate, source));
1951 }
1952 }
1953 None
1954}
1955
1956fn find_default_export_array<'a>(program: &'a Program<'a>) -> Option<&'a ArrayExpression<'a>> {
1961 for stmt in &program.body {
1962 if let Statement::ExportDefaultDeclaration(decl) = stmt
1963 && let Some(expr) = decl.declaration.as_expression()
1964 {
1965 return array_from_expression(expr);
1966 }
1967 }
1968 None
1969}
1970
1971fn array_from_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
1972 match expr {
1973 Expression::ArrayExpression(arr) => Some(arr),
1974 Expression::ParenthesizedExpression(paren) => array_from_expression(&paren.expression),
1975 Expression::TSAsExpression(ts_as) => array_from_expression(&ts_as.expression),
1976 Expression::TSSatisfiesExpression(ts_sat) => array_from_expression(&ts_sat.expression),
1977 Expression::CallExpression(call) => call
1978 .arguments
1979 .first()
1980 .and_then(Argument::as_expression)
1981 .and_then(array_from_expression),
1982 _ => None,
1983 }
1984}
1985
1986pub(crate) fn lexical_normalize(path: &Path) -> PathBuf {
1987 let mut normalized = PathBuf::new();
1988
1989 for component in path.components() {
1990 match component {
1991 std::path::Component::CurDir => {}
1992 std::path::Component::ParentDir => {
1993 normalized.pop();
1994 }
1995 _ => normalized.push(component.as_os_str()),
1996 }
1997 }
1998
1999 normalized
2000}
2001
2002fn expression_to_string_array(expr: &Expression) -> Vec<String> {
2004 match expr {
2005 Expression::ArrayExpression(arr) => arr
2006 .elements
2007 .iter()
2008 .filter_map(|el| match el {
2009 ArrayExpressionElement::SpreadElement(_) => None,
2010 _ => el.as_expression().and_then(expression_to_string),
2011 })
2012 .collect(),
2013 _ => vec![],
2014 }
2015}
2016
2017fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
2022 let mut values = Vec::new();
2023 match expr {
2024 Expression::StringLiteral(s) => {
2025 values.push(s.value.to_string());
2026 }
2027 Expression::ArrayExpression(arr) => {
2028 for el in &arr.elements {
2029 if let Some(inner) = el.as_expression() {
2030 match inner {
2031 Expression::StringLiteral(s) => {
2032 values.push(s.value.to_string());
2033 }
2034 Expression::ArrayExpression(sub_arr) => {
2035 if let Some(first) = sub_arr.elements.first()
2036 && let Some(first_expr) = first.as_expression()
2037 && let Some(s) = expression_to_string(first_expr)
2038 {
2039 values.push(s);
2040 }
2041 }
2042 _ => {}
2043 }
2044 }
2045 }
2046 }
2047 Expression::ObjectExpression(obj) => {
2048 for prop in &obj.properties {
2049 if let ObjectPropertyKind::ObjectProperty(p) = prop {
2050 match &p.value {
2051 Expression::StringLiteral(s) => {
2052 values.push(s.value.to_string());
2053 }
2054 Expression::ArrayExpression(sub_arr) => {
2055 if let Some(first) = sub_arr.elements.first()
2056 && let Some(first_expr) = first.as_expression()
2057 && let Some(s) = expression_to_string(first_expr)
2058 {
2059 values.push(s);
2060 }
2061 }
2062 _ => {}
2063 }
2064 }
2065 }
2066 }
2067 _ => {}
2068 }
2069 values
2070}
2071
2072fn collect_shallow_string_or_object_property_values(
2074 expr: &Expression,
2075 object_property: &str,
2076) -> Vec<String> {
2077 match expr {
2078 Expression::ArrayExpression(arr) => arr
2079 .elements
2080 .iter()
2081 .filter_map(|element| {
2082 element
2083 .as_expression()
2084 .and_then(|expr| shallow_string_or_object_property(expr, object_property))
2085 })
2086 .collect(),
2087 _ => shallow_string_or_object_property(expr, object_property)
2088 .into_iter()
2089 .collect(),
2090 }
2091}
2092
2093fn shallow_string_or_object_property(expr: &Expression, object_property: &str) -> Option<String> {
2094 match expr {
2095 Expression::ParenthesizedExpression(paren) => {
2096 shallow_string_or_object_property(&paren.expression, object_property)
2097 }
2098 Expression::TSSatisfiesExpression(ts_sat) => {
2099 shallow_string_or_object_property(&ts_sat.expression, object_property)
2100 }
2101 Expression::TSAsExpression(ts_as) => {
2102 shallow_string_or_object_property(&ts_as.expression, object_property)
2103 }
2104 Expression::ArrayExpression(sub_arr) => sub_arr
2105 .elements
2106 .first()
2107 .and_then(ArrayExpressionElement::as_expression)
2108 .and_then(expression_to_string),
2109 Expression::ObjectExpression(obj) => {
2110 find_property(obj, object_property).and_then(|prop| expression_to_string(&prop.value))
2111 }
2112 _ => expression_to_string(expr),
2113 }
2114}
2115
2116fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
2118 match expr {
2119 Expression::StringLiteral(s) => {
2120 values.push(s.value.to_string());
2121 }
2122 Expression::ArrayExpression(arr) => {
2123 for el in &arr.elements {
2124 if let Some(expr) = el.as_expression() {
2125 collect_all_string_values(expr, values);
2126 }
2127 }
2128 }
2129 Expression::ObjectExpression(obj) => {
2130 for prop in &obj.properties {
2131 if let ObjectPropertyKind::ObjectProperty(p) = prop {
2132 collect_all_string_values(&p.value, values);
2133 }
2134 }
2135 }
2136 _ => {}
2137 }
2138}
2139
2140fn property_key_to_string(key: &PropertyKey) -> Option<String> {
2142 match key {
2143 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
2144 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
2145 _ => None,
2146 }
2147}
2148
2149fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
2151 if path.is_empty() {
2152 return None;
2153 }
2154 let prop = find_property(obj, path[0])?;
2155 if path.len() == 1 {
2156 if let Expression::ObjectExpression(nested) = &prop.value {
2157 let keys = nested
2158 .properties
2159 .iter()
2160 .filter_map(|p| {
2161 if let ObjectPropertyKind::ObjectProperty(p) = p {
2162 property_key_to_string(&p.key)
2163 } else {
2164 None
2165 }
2166 })
2167 .collect();
2168 return Some(keys);
2169 }
2170 return None;
2171 }
2172 if let Expression::ObjectExpression(nested) = &prop.value {
2173 get_nested_object_keys(nested, &path[1..])
2174 } else {
2175 None
2176 }
2177}
2178
2179fn get_nested_expression<'a>(
2181 obj: &'a ObjectExpression<'a>,
2182 path: &[&str],
2183) -> Option<&'a Expression<'a>> {
2184 if path.is_empty() {
2185 return None;
2186 }
2187 let prop = find_property(obj, path[0])?;
2188 if path.len() == 1 {
2189 return Some(&prop.value);
2190 }
2191 if let Expression::ObjectExpression(nested) = &prop.value {
2192 get_nested_expression(nested, &path[1..])
2193 } else {
2194 None
2195 }
2196}
2197
2198fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
2200 if path.is_empty() {
2201 return None;
2202 }
2203 if path.len() == 1 {
2204 let prop = find_property(obj, path[0])?;
2205 return Some(expression_to_string_or_array(&prop.value));
2206 }
2207 let prop = find_property(obj, path[0])?;
2208 if let Expression::ObjectExpression(nested) = &prop.value {
2209 get_nested_string_or_array(nested, &path[1..])
2210 } else {
2211 None
2212 }
2213}
2214
2215fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
2223 match expr {
2224 Expression::StringLiteral(s) => vec![s.value.to_string()],
2225 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
2226 .quasis
2227 .first()
2228 .map(|q| vec![q.value.raw.to_string()])
2229 .unwrap_or_default(),
2230 Expression::ArrayExpression(arr) => arr
2231 .elements
2232 .iter()
2233 .filter_map(|el| el.as_expression())
2234 .flat_map(|e| match e {
2235 Expression::ObjectExpression(obj) => find_property(obj, "input")
2236 .map(|p| expression_to_string_or_array(&p.value))
2237 .unwrap_or_default(),
2238 _ => expression_to_path_string(e).into_iter().collect(),
2239 })
2240 .collect(),
2241 Expression::ObjectExpression(obj) => obj
2242 .properties
2243 .iter()
2244 .flat_map(|p| {
2245 if let ObjectPropertyKind::ObjectProperty(p) = p {
2246 match &p.value {
2247 Expression::ArrayExpression(_) => expression_to_string_or_array(&p.value),
2248 Expression::ObjectExpression(value_obj) => {
2249 find_property(value_obj, "import")
2250 .map(|import_prop| {
2251 expression_to_string_or_array(&import_prop.value)
2252 })
2253 .unwrap_or_default()
2254 }
2255 _ => expression_to_path_string(&p.value).into_iter().collect(),
2256 }
2257 } else {
2258 Vec::new()
2259 }
2260 })
2261 .collect(),
2262 _ => expression_to_path_string(expr).into_iter().collect(),
2263 }
2264}
2265
2266fn collect_require_sources(expr: &Expression) -> Vec<String> {
2268 let mut sources = Vec::new();
2269 match expr {
2270 Expression::CallExpression(call) if is_require_call(call) => {
2271 if let Some(s) = get_require_source(call) {
2272 sources.push(s);
2273 }
2274 }
2275 Expression::ArrayExpression(arr) => {
2276 for el in &arr.elements {
2277 if let Some(inner) = el.as_expression() {
2278 match inner {
2279 Expression::CallExpression(call) if is_require_call(call) => {
2280 if let Some(s) = get_require_source(call) {
2281 sources.push(s);
2282 }
2283 }
2284 Expression::ArrayExpression(sub_arr) => {
2285 if let Some(first) = sub_arr.elements.first()
2286 && let Some(Expression::CallExpression(call)) =
2287 first.as_expression()
2288 && is_require_call(call)
2289 && let Some(s) = get_require_source(call)
2290 {
2291 sources.push(s);
2292 }
2293 }
2294 _ => {}
2295 }
2296 }
2297 }
2298 }
2299 _ => {}
2300 }
2301 sources
2302}
2303
2304fn is_require_call(call: &CallExpression) -> bool {
2306 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
2307}
2308
2309fn get_require_source(call: &CallExpression) -> Option<String> {
2311 call.arguments.first().and_then(|arg| {
2312 if let Argument::StringLiteral(s) = arg {
2313 Some(s.value.to_string())
2314 } else {
2315 None
2316 }
2317 })
2318}
2319
2320#[cfg(test)]
2321mod tests {
2322 use super::*;
2323 use std::path::PathBuf;
2324
2325 fn js_path() -> PathBuf {
2326 PathBuf::from("config.js")
2327 }
2328
2329 fn ts_path() -> PathBuf {
2330 PathBuf::from("config.ts")
2331 }
2332
2333 #[test]
2334 fn extract_lazy_imports_bare_arrows() {
2335 let source = r"
2336 import { defineConfig } from '@adonisjs/core/app'
2337 export default defineConfig({
2338 preloads: [
2339 () => import('#start/routes'),
2340 () => import('#start/kernel'),
2341 ],
2342 })
2343 ";
2344 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["preloads"]);
2345 assert_eq!(specs, vec!["#start/routes", "#start/kernel"]);
2346 }
2347
2348 #[test]
2349 fn extract_lazy_imports_object_form_with_file_key() {
2350 let source = r"
2351 export default defineConfig({
2352 providers: [
2353 () => import('@adonisjs/core/providers/app_provider'),
2354 {
2355 file: () => import('@adonisjs/core/providers/repl_provider'),
2356 environment: ['repl', 'test'],
2357 },
2358 ],
2359 })
2360 ";
2361 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
2362 assert_eq!(
2363 specs,
2364 vec![
2365 "@adonisjs/core/providers/app_provider",
2366 "@adonisjs/core/providers/repl_provider",
2367 ]
2368 );
2369 }
2370
2371 #[test]
2372 fn extract_lazy_imports_block_body_with_return() {
2373 let source = r"
2374 export default defineConfig({
2375 commands: [
2376 () => { return import('@adonisjs/core/commands') },
2377 ],
2378 })
2379 ";
2380 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
2381 assert_eq!(specs, vec!["@adonisjs/core/commands"]);
2382 }
2383
2384 #[test]
2385 fn extract_lazy_imports_skips_unknown_element_shapes() {
2386 let source = r"
2387 export default defineConfig({
2388 commands: [
2389 'string-entry',
2390 42,
2391 { other: 'value' },
2392 () => import('@adonisjs/lucid/commands'),
2393 ],
2394 })
2395 ";
2396 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
2397 assert_eq!(specs, vec!["@adonisjs/lucid/commands"]);
2398 }
2399
2400 #[test]
2401 fn extract_lazy_imports_missing_property_returns_empty() {
2402 let source = r"
2403 export default defineConfig({
2404 preloads: [() => import('#start/routes')],
2405 })
2406 ";
2407 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
2408 assert!(specs.is_empty());
2409 }
2410
2411 #[test]
2412 fn extract_imports_basic() {
2413 let source = r"
2414 import foo from 'foo-pkg';
2415 import { bar } from '@scope/bar';
2416 export default {};
2417 ";
2418 let imports = extract_imports(source, &js_path());
2419 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
2420 }
2421
2422 #[test]
2423 fn extract_default_export_object_property() {
2424 let source = r#"export default { testDir: "./tests" };"#;
2425 let val = extract_config_string(source, &js_path(), &["testDir"]);
2426 assert_eq!(val, Some("./tests".to_string()));
2427 }
2428
2429 #[test]
2430 fn extract_define_config_property() {
2431 let source = r#"
2432 import { defineConfig } from 'vitest/config';
2433 export default defineConfig({
2434 test: {
2435 include: ["**/*.test.ts", "**/*.spec.ts"],
2436 setupFiles: ["./test/setup.ts"]
2437 }
2438 });
2439 "#;
2440 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
2441 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
2442
2443 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
2444 assert_eq!(setup, vec!["./test/setup.ts"]);
2445 }
2446
2447 #[test]
2448 fn extract_module_exports_property() {
2449 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
2450 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
2451 assert_eq!(val, Some("jsdom".to_string()));
2452 }
2453
2454 #[test]
2455 fn extract_nested_string_array() {
2456 let source = r#"
2457 export default {
2458 resolve: {
2459 alias: {
2460 "@": "./src"
2461 }
2462 },
2463 test: {
2464 include: ["src/**/*.test.ts"]
2465 }
2466 };
2467 "#;
2468 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
2469 assert_eq!(include, vec!["src/**/*.test.ts"]);
2470 }
2471
2472 #[test]
2473 fn extract_addons_array() {
2474 let source = r#"
2475 export default {
2476 addons: [
2477 "@storybook/addon-a11y",
2478 "@storybook/addon-docs",
2479 "@storybook/addon-links"
2480 ]
2481 };
2482 "#;
2483 let addons = extract_config_property_strings(source, &ts_path(), "addons");
2484 assert_eq!(
2485 addons,
2486 vec![
2487 "@storybook/addon-a11y",
2488 "@storybook/addon-docs",
2489 "@storybook/addon-links"
2490 ]
2491 );
2492 }
2493
2494 #[test]
2495 fn handle_empty_config() {
2496 let source = "";
2497 let result = extract_config_string(source, &js_path(), &["key"]);
2498 assert_eq!(result, None);
2499 }
2500
2501 #[test]
2502 fn object_keys_postcss_plugins() {
2503 let source = r"
2504 module.exports = {
2505 plugins: {
2506 autoprefixer: {},
2507 tailwindcss: {},
2508 'postcss-import': {}
2509 }
2510 };
2511 ";
2512 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2513 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
2514 }
2515
2516 #[test]
2517 fn object_keys_nested_path() {
2518 let source = r"
2519 export default {
2520 build: {
2521 plugins: {
2522 minify: {},
2523 compress: {}
2524 }
2525 }
2526 };
2527 ";
2528 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
2529 assert_eq!(keys, vec!["minify", "compress"]);
2530 }
2531
2532 #[test]
2533 fn object_keys_empty_object() {
2534 let source = r"export default { plugins: {} };";
2535 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2536 assert!(keys.is_empty());
2537 }
2538
2539 #[test]
2540 fn object_keys_non_object_returns_empty() {
2541 let source = r#"export default { plugins: ["a", "b"] };"#;
2542 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2543 assert!(keys.is_empty());
2544 }
2545
2546 #[test]
2547 fn string_or_array_single_string() {
2548 let source = r#"export default { entry: "./src/index.js" };"#;
2549 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2550 assert_eq!(result, vec!["./src/index.js"]);
2551 }
2552
2553 #[test]
2554 fn string_or_array_array() {
2555 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
2556 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2557 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
2558 }
2559
2560 #[test]
2561 fn string_or_array_object_values() {
2562 let source =
2563 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
2564 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2565 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
2566 }
2567
2568 #[test]
2569 fn string_or_array_object_array_values() {
2570 let source = r#"export default { entry: { app: ["./src/polyfill.js", "./src/app.js"] } };"#;
2571 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2572 assert_eq!(result, vec!["./src/polyfill.js", "./src/app.js"]);
2573 }
2574
2575 #[test]
2576 fn string_or_array_webpack_entry_descriptors() {
2577 let source = r#"
2578 export default {
2579 entry: {
2580 app: {
2581 import: "./src/app.js",
2582 filename: "pages/app.js",
2583 dependOn: "shared",
2584 },
2585 admin: {
2586 import: ["./src/admin-polyfill.js", "./src/admin.js"],
2587 runtime: "runtime",
2588 },
2589 shared: ["react", "react-dom"],
2590 },
2591 };
2592 "#;
2593 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2594 assert_eq!(
2595 result,
2596 vec![
2597 "./src/app.js",
2598 "./src/admin-polyfill.js",
2599 "./src/admin.js",
2600 "react",
2601 "react-dom"
2602 ]
2603 );
2604 }
2605
2606 #[test]
2607 fn string_or_array_nested_path() {
2608 let source = r#"
2609 export default {
2610 build: {
2611 rollupOptions: {
2612 input: ["./index.html", "./about.html"]
2613 }
2614 }
2615 };
2616 "#;
2617 let result = extract_config_string_or_array(
2618 source,
2619 &js_path(),
2620 &["build", "rollupOptions", "input"],
2621 );
2622 assert_eq!(result, vec!["./index.html", "./about.html"]);
2623 }
2624
2625 #[test]
2626 fn string_or_array_template_literal() {
2627 let source = r"export default { entry: `./src/index.js` };";
2628 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2629 assert_eq!(result, vec!["./src/index.js"]);
2630 }
2631
2632 #[test]
2633 fn string_or_array_object_path_helper_values() {
2634 let source = r#"
2635 import { resolve, join } from "node:path";
2636 import path from "node:path";
2637 export default {
2638 build: {
2639 rollupOptions: {
2640 input: {
2641 app: resolve(__dirname, "src/app.ts"),
2642 modal: path.resolve(__dirname, "src/modal.ts"),
2643 tabs: join(__dirname, "src/tabs.ts"),
2644 styles: resolve(__dirname, "src/index.css"),
2645 },
2646 },
2647 },
2648 };
2649 "#;
2650 let result = extract_config_string_or_array(
2651 source,
2652 &js_path(),
2653 &["build", "rollupOptions", "input"],
2654 );
2655 assert_eq!(
2656 result,
2657 vec!["src/app.ts", "src/modal.ts", "src/tabs.ts", "src/index.css"]
2658 );
2659 }
2660
2661 #[test]
2662 fn string_or_array_array_path_helper_values() {
2663 let source = r#"
2664 import { resolve } from "node:path";
2665 export default {
2666 build: {
2667 rollupOptions: {
2668 input: [resolve(__dirname, "src/a.ts"), "./src/b.ts"],
2669 },
2670 },
2671 };
2672 "#;
2673 let result = extract_config_string_or_array(
2674 source,
2675 &js_path(),
2676 &["build", "rollupOptions", "input"],
2677 );
2678 assert_eq!(result, vec!["src/a.ts", "./src/b.ts"]);
2679 }
2680
2681 #[test]
2682 fn string_or_array_top_level_path_helper_call() {
2683 let source = r#"
2684 import { resolve } from "node:path";
2685 export default { build: { lib: { entry: resolve(__dirname, "src/index.ts") } } };
2686 "#;
2687 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
2688 assert_eq!(result, vec!["src/index.ts"]);
2689 }
2690
2691 #[test]
2692 fn string_or_array_import_meta_dirname_anchor() {
2693 let source = r#"
2694 import { resolve } from "node:path";
2695 export default {
2696 build: { lib: { entry: resolve(import.meta.dirname, "src/index.ts") } },
2697 };
2698 "#;
2699 let result = extract_config_string_or_array(source, &ts_path(), &["build", "lib", "entry"]);
2700 assert_eq!(result, vec!["src/index.ts"]);
2701 }
2702
2703 #[test]
2704 fn string_or_array_non_literal_path_helper_args_dropped() {
2705 let source = r#"
2706 import { resolve } from "node:path";
2707 export default { build: { lib: { entry: resolve(baseDir, "src/index.ts") } } };
2708 "#;
2709 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
2710 assert!(
2711 result.is_empty(),
2712 "non-literal path-helper args must be dropped: {result:?}"
2713 );
2714 }
2715
2716 #[test]
2717 fn require_strings_array() {
2718 let source = r"
2719 module.exports = {
2720 plugins: [
2721 require('autoprefixer'),
2722 require('postcss-import')
2723 ]
2724 };
2725 ";
2726 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2727 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
2728 }
2729
2730 #[test]
2731 fn require_strings_with_tuples() {
2732 let source = r"
2733 module.exports = {
2734 plugins: [
2735 require('autoprefixer'),
2736 [require('postcss-preset-env'), { stage: 3 }]
2737 ]
2738 };
2739 ";
2740 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2741 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
2742 }
2743
2744 #[test]
2745 fn require_strings_empty_array() {
2746 let source = r"module.exports = { plugins: [] };";
2747 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2748 assert!(deps.is_empty());
2749 }
2750
2751 #[test]
2752 fn require_strings_no_require_calls() {
2753 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
2754 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2755 assert!(deps.is_empty());
2756 }
2757
2758 #[test]
2759 fn extract_aliases_from_object_with_file_url_to_path() {
2760 let source = r#"
2761 import { defineConfig } from 'vite';
2762 import { fileURLToPath, URL } from 'node:url';
2763
2764 export default defineConfig({
2765 resolve: {
2766 alias: {
2767 "@": fileURLToPath(new URL("./src", import.meta.url))
2768 }
2769 }
2770 });
2771 "#;
2772
2773 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
2774 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
2775 }
2776
2777 #[test]
2778 fn extract_aliases_from_array_form() {
2779 let source = r#"
2780 export default {
2781 resolve: {
2782 alias: [
2783 { find: "@", replacement: "./src" },
2784 { find: "$utils", replacement: "src/lib/utils" }
2785 ]
2786 }
2787 };
2788 "#;
2789
2790 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
2791 assert_eq!(
2792 aliases,
2793 vec![
2794 ("@".to_string(), "./src".to_string()),
2795 ("$utils".to_string(), "src/lib/utils".to_string())
2796 ]
2797 );
2798 }
2799
2800 #[test]
2801 fn extract_aliases_from_object_with_array_values() {
2802 let source = r#"
2803 ({
2804 compilerOptions: {
2805 paths: {
2806 "@/*": ["./src/*"],
2807 "@shared/*": ["./shared/*", "./fallback/*"]
2808 }
2809 }
2810 })
2811 "#;
2812
2813 let aliases = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
2814 assert_eq!(
2815 aliases,
2816 vec![
2817 ("@/*".to_string(), "./src/*".to_string()),
2818 ("@shared/*".to_string(), "./shared/*".to_string())
2819 ]
2820 );
2821 }
2822
2823 #[test]
2824 fn extract_array_object_strings_mixed_forms() {
2825 let source = r#"
2826 export default {
2827 components: [
2828 "~/components",
2829 { path: "@/feature-components" }
2830 ]
2831 };
2832 "#;
2833
2834 let values =
2835 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
2836 assert_eq!(
2837 values,
2838 vec![
2839 "~/components".to_string(),
2840 "@/feature-components".to_string()
2841 ]
2842 );
2843 }
2844
2845 #[test]
2846 fn extract_array_object_string_pairs_with_and_without_secondary() {
2847 let source = r#"
2848 export default {
2849 webServer: [
2850 { command: "tsx scripts/api.ts", cwd: "packages/api" },
2851 { command: "tsx scripts/web.ts" }
2852 ]
2853 };
2854 "#;
2855
2856 let pairs = extract_config_array_object_string_pairs(
2857 source,
2858 &ts_path(),
2859 &["webServer"],
2860 "command",
2861 "cwd",
2862 );
2863 assert_eq!(
2864 pairs,
2865 vec![
2866 (
2867 "tsx scripts/api.ts".to_string(),
2868 Some("packages/api".to_string())
2869 ),
2870 ("tsx scripts/web.ts".to_string(), None),
2871 ]
2872 );
2873 }
2874
2875 #[test]
2876 fn extract_array_object_string_pairs_skips_elements_missing_primary() {
2877 let source = r#"
2878 export default {
2879 webServer: [
2880 { cwd: "packages/api" },
2881 { command: "srvx --port 3000" }
2882 ]
2883 };
2884 "#;
2885
2886 let pairs = extract_config_array_object_string_pairs(
2887 source,
2888 &ts_path(),
2889 &["webServer"],
2890 "command",
2891 "cwd",
2892 );
2893 assert_eq!(pairs, vec![("srvx --port 3000".to_string(), None)]);
2894 }
2895
2896 #[test]
2897 fn extract_array_object_string_pairs_empty_for_object_form() {
2898 let source = r#"
2899 export default {
2900 webServer: { command: "srvx --port 3000" }
2901 };
2902 "#;
2903
2904 let pairs = extract_config_array_object_string_pairs(
2905 source,
2906 &ts_path(),
2907 &["webServer"],
2908 "command",
2909 "cwd",
2910 );
2911 assert!(pairs.is_empty());
2912 }
2913
2914 #[test]
2915 fn extract_config_plugin_option_string_from_json() {
2916 let source = r#"{
2917 "expo": {
2918 "plugins": [
2919 ["expo-router", { "root": "src/app" }]
2920 ]
2921 }
2922 }"#;
2923
2924 let value = extract_config_plugin_option_string(
2925 source,
2926 &json_path(),
2927 &["expo", "plugins"],
2928 "expo-router",
2929 "root",
2930 );
2931
2932 assert_eq!(value, Some("src/app".to_string()));
2933 }
2934
2935 #[test]
2936 fn extract_config_plugin_option_string_from_top_level_plugins() {
2937 let source = r#"{
2938 "plugins": [
2939 ["expo-router", { "root": "./src/routes" }]
2940 ]
2941 }"#;
2942
2943 let value = extract_config_plugin_option_string_from_paths(
2944 source,
2945 &json_path(),
2946 &[&["plugins"], &["expo", "plugins"]],
2947 "expo-router",
2948 "root",
2949 );
2950
2951 assert_eq!(value, Some("./src/routes".to_string()));
2952 }
2953
2954 #[test]
2955 fn extract_config_plugin_option_string_from_ts_config() {
2956 let source = r"
2957 export default {
2958 expo: {
2959 plugins: [
2960 ['expo-router', { root: './src/app' }]
2961 ]
2962 }
2963 };
2964 ";
2965
2966 let value = extract_config_plugin_option_string(
2967 source,
2968 &ts_path(),
2969 &["expo", "plugins"],
2970 "expo-router",
2971 "root",
2972 );
2973
2974 assert_eq!(value, Some("./src/app".to_string()));
2975 }
2976
2977 #[test]
2978 fn extract_config_plugin_option_string_returns_none_when_plugin_missing() {
2979 let source = r#"{
2980 "expo": {
2981 "plugins": [
2982 ["expo-font", {}]
2983 ]
2984 }
2985 }"#;
2986
2987 let value = extract_config_plugin_option_string(
2988 source,
2989 &json_path(),
2990 &["expo", "plugins"],
2991 "expo-router",
2992 "root",
2993 );
2994
2995 assert_eq!(value, None);
2996 }
2997
2998 #[test]
2999 fn vite_react_babel_dependencies_extract_plain_tuple_and_prefixed_entries() {
3000 let source = r#"
3001 import react from "@vitejs/plugin-react";
3002
3003 export default defineConfig({
3004 plugins: [
3005 react({
3006 babel: {
3007 plugins: [
3008 "babel-plugin-plain",
3009 ["module:@preact/signals-react-transform", { mode: "auto" }],
3010 ],
3011 presets: [["@babel/preset-react", { runtime: "automatic" }]],
3012 },
3013 }),
3014 ],
3015 });
3016 "#;
3017
3018 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3019
3020 assert_eq!(
3021 deps,
3022 vec![
3023 "babel-plugin-plain".to_string(),
3024 "@preact/signals-react-transform".to_string(),
3025 "@babel/preset-react".to_string(),
3026 ]
3027 );
3028 }
3029
3030 #[test]
3031 fn vite_react_babel_dependencies_support_default_alias_import() {
3032 let source = r#"
3033 import { default as viteReact } from "@vitejs/plugin-react";
3034
3035 export default {
3036 plugins: [
3037 viteReact({
3038 babel: {
3039 plugins: [["module:@scope/pkg/plugin", {}]],
3040 },
3041 }),
3042 ],
3043 };
3044 "#;
3045
3046 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3047
3048 assert_eq!(deps, vec!["@scope/pkg".to_string()]);
3049 }
3050
3051 #[test]
3052 fn vite_react_babel_dependencies_ignore_unrelated_plugin_calls() {
3053 let source = r#"
3054 import vue from "@vitejs/plugin-vue";
3055
3056 export default {
3057 plugins: [
3058 vue({
3059 babel: {
3060 plugins: ["@preact/signals-react-transform"],
3061 },
3062 }),
3063 ],
3064 };
3065 "#;
3066
3067 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3068
3069 assert!(deps.is_empty());
3070 }
3071
3072 #[test]
3073 fn vite_react_babel_dependencies_skip_relative_and_protocol_entries() {
3074 let source = r#"
3075 import react from "@vitejs/plugin-react";
3076
3077 export default {
3078 plugins: [
3079 react({
3080 babel: {
3081 plugins: ["./local-plugin", "module:./local-prefixed", "http://example.com/plugin"],
3082 },
3083 }),
3084 ],
3085 };
3086 "#;
3087
3088 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3089
3090 assert!(deps.is_empty());
3091 }
3092
3093 #[test]
3094 fn normalize_config_path_relative_to_root() {
3095 let config_path = PathBuf::from("/project/vite.config.ts");
3096 let root = PathBuf::from("/project");
3097
3098 assert_eq!(
3099 normalize_config_path("./src/lib", &config_path, &root),
3100 Some("src/lib".to_string())
3101 );
3102 assert_eq!(
3103 normalize_config_path("/src/lib", &config_path, &root),
3104 Some("src/lib".to_string())
3105 );
3106 }
3107
3108 #[test]
3109 fn normalize_config_path_mixed_separators_and_parent_dirs() {
3110 let config_path = PathBuf::from("/project/config/vite.config.ts");
3111 let root = PathBuf::from("/project");
3112
3113 assert_eq!(
3114 normalize_config_path(".\\src\\..\\app\\lib", &config_path, &root),
3115 Some("config/app/lib".to_string())
3116 );
3117 }
3118
3119 #[test]
3120 fn normalize_config_path_leading_slash_stays_project_relative() {
3121 let config_path = PathBuf::from("/project/vite.config.ts");
3122 let root = PathBuf::from("/project");
3123
3124 assert_eq!(
3125 normalize_config_path("/src\\lib", &config_path, &root),
3126 Some("src/lib".to_string())
3127 );
3128 }
3129
3130 #[test]
3131 fn json_wrapped_in_parens_string() {
3132 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
3133 let val = extract_config_string(source, &js_path(), &["extends"]);
3134 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
3135 }
3136
3137 #[test]
3138 fn json_wrapped_in_parens_nested_array() {
3139 let source =
3140 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
3141 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
3142 assert_eq!(types, vec!["node", "jest"]);
3143
3144 let include = extract_config_string_array(source, &js_path(), &["include"]);
3145 assert_eq!(include, vec!["src/**/*"]);
3146 }
3147
3148 #[test]
3149 fn json_wrapped_in_parens_object_keys() {
3150 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
3151 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3152 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
3153 }
3154
3155 fn json_path() -> PathBuf {
3156 PathBuf::from("config.json")
3157 }
3158
3159 #[test]
3160 fn json_file_parsed_correctly() {
3161 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
3162 let val = extract_config_string(source, &json_path(), &["key"]);
3163 assert_eq!(val, Some("value".to_string()));
3164
3165 let list = extract_config_string_array(source, &json_path(), &["list"]);
3166 assert_eq!(list, vec!["a", "b"]);
3167 }
3168
3169 #[test]
3170 fn jsonc_file_parsed_correctly() {
3171 let source = r#"{"key": "value"}"#;
3172 let path = PathBuf::from("tsconfig.jsonc");
3173 let val = extract_config_string(source, &path, &["key"]);
3174 assert_eq!(val, Some("value".to_string()));
3175 }
3176
3177 #[test]
3178 fn extract_define_config_arrow_function() {
3179 let source = r#"
3180 import { defineConfig } from 'vite';
3181 export default defineConfig(() => ({
3182 test: {
3183 include: ["**/*.test.ts"]
3184 }
3185 }));
3186 "#;
3187 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
3188 assert_eq!(include, vec!["**/*.test.ts"]);
3189 }
3190
3191 #[test]
3192 fn extract_config_from_default_export_function_declaration() {
3193 let source = r#"
3194 export default function createConfig() {
3195 return {
3196 clientModules: ["./src/client/global.js"]
3197 };
3198 }
3199 "#;
3200
3201 let client_modules = extract_config_string_array(source, &ts_path(), &["clientModules"]);
3202 assert_eq!(client_modules, vec!["./src/client/global.js"]);
3203 }
3204
3205 #[test]
3206 fn extract_config_from_default_export_async_function_declaration() {
3207 let source = r#"
3208 export default async function createConfigAsync() {
3209 return {
3210 docs: {
3211 path: "knowledge"
3212 }
3213 };
3214 }
3215 "#;
3216
3217 let docs_path = extract_config_string(source, &ts_path(), &["docs", "path"]);
3218 assert_eq!(docs_path, Some("knowledge".to_string()));
3219 }
3220
3221 #[test]
3222 fn extract_config_from_exported_arrow_function_identifier() {
3223 let source = r#"
3224 const config = async () => {
3225 return {
3226 themes: ["classic"]
3227 };
3228 };
3229
3230 export default config;
3231 "#;
3232
3233 let themes = extract_config_shallow_strings(source, &ts_path(), "themes");
3234 assert_eq!(themes, vec!["classic"]);
3235 }
3236
3237 #[test]
3238 fn module_exports_nested_string() {
3239 let source = r#"
3240 module.exports = {
3241 resolve: {
3242 alias: {
3243 "@": "./src"
3244 }
3245 }
3246 };
3247 "#;
3248 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
3249 assert_eq!(val, Some("./src".to_string()));
3250 }
3251
3252 #[test]
3253 fn property_strings_nested_objects() {
3254 let source = r#"
3255 export default {
3256 plugins: {
3257 group1: { a: "val-a" },
3258 group2: { b: "val-b" }
3259 }
3260 };
3261 "#;
3262 let values = extract_config_property_strings(source, &js_path(), "plugins");
3263 assert!(values.contains(&"val-a".to_string()));
3264 assert!(values.contains(&"val-b".to_string()));
3265 }
3266
3267 #[test]
3268 fn property_strings_missing_key_returns_empty() {
3269 let source = r#"export default { other: "value" };"#;
3270 let values = extract_config_property_strings(source, &js_path(), "missing");
3271 assert!(values.is_empty());
3272 }
3273
3274 #[test]
3275 fn shallow_strings_tuple_array() {
3276 let source = r#"
3277 module.exports = {
3278 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
3279 };
3280 "#;
3281 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
3282 assert_eq!(values, vec!["default", "jest-junit"]);
3283 assert!(!values.contains(&"reports".to_string()));
3284 }
3285
3286 #[test]
3287 fn shallow_strings_single_string() {
3288 let source = r#"export default { preset: "ts-jest" };"#;
3289 let values = extract_config_shallow_strings(source, &js_path(), "preset");
3290 assert_eq!(values, vec!["ts-jest"]);
3291 }
3292
3293 #[test]
3294 fn shallow_strings_missing_key() {
3295 let source = r#"export default { other: "val" };"#;
3296 let values = extract_config_shallow_strings(source, &js_path(), "missing");
3297 assert!(values.is_empty());
3298 }
3299
3300 #[test]
3301 fn shallow_strings_or_object_property_alias_objects() {
3302 let source = r#"
3303 export default {
3304 jsPlugins: [
3305 "eslint-plugin-playwright",
3306 ["eslint-plugin-regexp", { rules: {} }],
3307 { name: "short", specifier: "eslint-plugin-with-long-name" }
3308 ]
3309 };
3310 "#;
3311 let values = extract_config_shallow_strings_or_object_property(
3312 source,
3313 &ts_path(),
3314 "jsPlugins",
3315 "specifier",
3316 );
3317 assert_eq!(
3318 values,
3319 vec![
3320 "eslint-plugin-playwright",
3321 "eslint-plugin-regexp",
3322 "eslint-plugin-with-long-name"
3323 ]
3324 );
3325 }
3326
3327 #[test]
3328 fn nested_shallow_strings_vitest_reporters() {
3329 let source = r#"
3330 export default {
3331 test: {
3332 reporters: ["default", "vitest-sonar-reporter"]
3333 }
3334 };
3335 "#;
3336 let values =
3337 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3338 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
3339 }
3340
3341 #[test]
3342 fn nested_shallow_strings_tuple_format() {
3343 let source = r#"
3344 export default {
3345 test: {
3346 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
3347 }
3348 };
3349 "#;
3350 let values =
3351 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3352 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
3353 }
3354
3355 #[test]
3356 fn nested_shallow_strings_missing_outer() {
3357 let source = r"export default { other: {} };";
3358 let values =
3359 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3360 assert!(values.is_empty());
3361 }
3362
3363 #[test]
3364 fn nested_shallow_strings_missing_inner() {
3365 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
3366 let values =
3367 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3368 assert!(values.is_empty());
3369 }
3370
3371 #[test]
3372 fn string_or_array_missing_path() {
3373 let source = r"export default {};";
3374 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3375 assert!(result.is_empty());
3376 }
3377
3378 #[test]
3379 fn string_or_array_non_string_values() {
3380 let source = r"export default { entry: [42, true] };";
3381 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3382 assert!(result.is_empty());
3383 }
3384
3385 #[test]
3386 fn array_nested_extraction() {
3387 let source = r#"
3388 export default defineConfig({
3389 test: {
3390 projects: [
3391 {
3392 test: {
3393 setupFiles: ["./test/setup-a.ts"]
3394 }
3395 },
3396 {
3397 test: {
3398 setupFiles: "./test/setup-b.ts"
3399 }
3400 }
3401 ]
3402 }
3403 });
3404 "#;
3405 let results = extract_config_array_nested_string_or_array(
3406 source,
3407 &ts_path(),
3408 &["test", "projects"],
3409 &["test", "setupFiles"],
3410 );
3411 assert!(results.contains(&"./test/setup-a.ts".to_string()));
3412 assert!(results.contains(&"./test/setup-b.ts".to_string()));
3413 }
3414
3415 #[test]
3416 fn array_nested_empty_when_no_array() {
3417 let source = r#"export default { test: { projects: "not-an-array" } };"#;
3418 let results = extract_config_array_nested_string_or_array(
3419 source,
3420 &js_path(),
3421 &["test", "projects"],
3422 &["test", "setupFiles"],
3423 );
3424 assert!(results.is_empty());
3425 }
3426
3427 #[test]
3428 fn object_nested_extraction() {
3429 let source = r#"{
3430 "projects": {
3431 "app-one": {
3432 "architect": {
3433 "build": {
3434 "options": {
3435 "styles": ["src/styles.css"]
3436 }
3437 }
3438 }
3439 }
3440 }
3441 }"#;
3442 let results = extract_config_object_nested_string_or_array(
3443 source,
3444 &json_path(),
3445 &["projects"],
3446 &["architect", "build", "options", "styles"],
3447 );
3448 assert_eq!(results, vec!["src/styles.css"]);
3449 }
3450
3451 #[test]
3452 fn array_with_object_input_form_extracted() {
3453 let source = r#"{
3454 "projects": {
3455 "app": {
3456 "architect": {
3457 "build": {
3458 "options": {
3459 "styles": [
3460 "src/styles.scss",
3461 { "input": "src/theme.scss", "bundleName": "theme", "inject": false },
3462 { "bundleName": "lazy-only" }
3463 ]
3464 }
3465 }
3466 }
3467 }
3468 }
3469 }"#;
3470 let results = extract_config_object_nested_string_or_array(
3471 source,
3472 &json_path(),
3473 &["projects"],
3474 &["architect", "build", "options", "styles"],
3475 );
3476 assert!(
3477 results.contains(&"src/styles.scss".to_string()),
3478 "string form must still work: {results:?}"
3479 );
3480 assert!(
3481 results.contains(&"src/theme.scss".to_string()),
3482 "object form with `input` must be extracted: {results:?}"
3483 );
3484 assert!(
3485 !results.contains(&"lazy-only".to_string()),
3486 "bundleName must not be misinterpreted as a path: {results:?}"
3487 );
3488 assert!(
3489 !results.contains(&"theme".to_string()),
3490 "bundleName from full object must not leak: {results:?}"
3491 );
3492 }
3493
3494 #[test]
3495 fn object_nested_strings_extraction() {
3496 let source = r#"{
3497 "targets": {
3498 "build": {
3499 "executor": "@angular/build:application"
3500 },
3501 "test": {
3502 "executor": "@nx/vite:test"
3503 }
3504 }
3505 }"#;
3506 let results =
3507 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
3508 assert!(results.contains(&"@angular/build:application".to_string()));
3509 assert!(results.contains(&"@nx/vite:test".to_string()));
3510 }
3511
3512 #[test]
3513 fn require_strings_direct_call() {
3514 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
3515 let deps = extract_config_require_strings(source, &js_path(), "adapter");
3516 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
3517 }
3518
3519 #[test]
3520 fn require_strings_no_matching_key() {
3521 let source = r"module.exports = { other: require('something') };";
3522 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3523 assert!(deps.is_empty());
3524 }
3525
3526 #[test]
3527 fn extract_imports_no_imports() {
3528 let source = r"export default {};";
3529 let imports = extract_imports(source, &js_path());
3530 assert!(imports.is_empty());
3531 }
3532
3533 #[test]
3534 fn extract_imports_side_effect_import() {
3535 let source = r"
3536 import 'polyfill';
3537 import './local-setup';
3538 export default {};
3539 ";
3540 let imports = extract_imports(source, &js_path());
3541 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
3542 }
3543
3544 #[test]
3545 fn extract_imports_mixed_specifiers() {
3546 let source = r"
3547 import defaultExport from 'module-a';
3548 import { named } from 'module-b';
3549 import * as ns from 'module-c';
3550 export default {};
3551 ";
3552 let imports = extract_imports(source, &js_path());
3553 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
3554 }
3555
3556 #[test]
3557 fn template_literal_in_string_or_array() {
3558 let source = r"export default { entry: `./src/index.ts` };";
3559 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
3560 assert_eq!(result, vec!["./src/index.ts"]);
3561 }
3562
3563 #[test]
3564 fn template_literal_in_config_string() {
3565 let source = r"export default { testDir: `./tests` };";
3566 let val = extract_config_string(source, &js_path(), &["testDir"]);
3567 assert_eq!(val, Some("./tests".to_string()));
3568 }
3569
3570 #[test]
3571 fn template_literal_command_recovers_static_command_tokens() {
3572 let source = r"
3573 const PORT = 3000;
3574 export default {
3575 webServer: {
3576 command: `pnpm exec srvx --port ${PORT} --hostname 127.0.0.1`
3577 }
3578 };
3579 ";
3580 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
3581 assert_eq!(
3582 val,
3583 Some("pnpm exec srvx --port --hostname 127.0.0.1".to_string())
3584 );
3585 }
3586
3587 #[test]
3588 fn template_literal_command_skips_dynamic_prefix() {
3589 let source = r"
3590 export default {
3591 webServer: { command: `${serverCommand} && pnpm exec srvx` }
3592 };
3593 ";
3594 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
3595 assert!(val.is_none());
3596 }
3597
3598 #[test]
3599 fn template_literal_command_skips_split_static_token() {
3600 let source = r"
3601 export default {
3602 webServer: { command: `pnpm exec sr${part}vx --port 3000` }
3603 };
3604 ";
3605 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
3606 assert!(val.is_none());
3607 }
3608
3609 #[test]
3610 fn array_object_command_pairs_recover_template_command() {
3611 let source = r"
3612 const PORT = 3000;
3613 export default {
3614 webServer: [
3615 {
3616 command: `pnpm exec srvx --port ${PORT}`,
3617 cwd: 'apps/web'
3618 }
3619 ]
3620 };
3621 ";
3622 let pairs = extract_config_array_object_command_pairs(
3623 source,
3624 &ts_path(),
3625 &["webServer"],
3626 "command",
3627 "cwd",
3628 );
3629 assert_eq!(
3630 pairs,
3631 vec![(
3632 "pnpm exec srvx --port ".to_string(),
3633 Some("apps/web".to_string())
3634 )]
3635 );
3636 }
3637
3638 #[test]
3639 fn nested_string_array_empty_path() {
3640 let source = r#"export default { items: ["a", "b"] };"#;
3641 let result = extract_config_string_array(source, &js_path(), &[]);
3642 assert!(result.is_empty());
3643 }
3644
3645 #[test]
3646 fn nested_string_empty_path() {
3647 let source = r#"export default { key: "val" };"#;
3648 let result = extract_config_string(source, &js_path(), &[]);
3649 assert!(result.is_none());
3650 }
3651
3652 #[test]
3653 fn object_keys_empty_path() {
3654 let source = r"export default { plugins: {} };";
3655 let result = extract_config_object_keys(source, &js_path(), &[]);
3656 assert!(result.is_empty());
3657 }
3658
3659 #[test]
3660 fn no_config_object_returns_empty() {
3661 let source = r"const x = 42;";
3662 let result = extract_config_string(source, &js_path(), &["key"]);
3663 assert!(result.is_none());
3664
3665 let arr = extract_config_string_array(source, &js_path(), &["items"]);
3666 assert!(arr.is_empty());
3667
3668 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3669 assert!(keys.is_empty());
3670 }
3671
3672 #[test]
3673 fn property_with_string_key() {
3674 let source = r#"export default { "string-key": "value" };"#;
3675 let val = extract_config_string(source, &js_path(), &["string-key"]);
3676 assert_eq!(val, Some("value".to_string()));
3677 }
3678
3679 #[test]
3680 fn nested_navigation_through_non_object() {
3681 let source = r#"export default { level1: "not-an-object" };"#;
3682 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
3683 assert!(val.is_none());
3684 }
3685
3686 #[test]
3687 fn variable_reference_untyped() {
3688 let source = r#"
3689 const config = {
3690 testDir: "./tests"
3691 };
3692 export default config;
3693 "#;
3694 let val = extract_config_string(source, &js_path(), &["testDir"]);
3695 assert_eq!(val, Some("./tests".to_string()));
3696 }
3697
3698 #[test]
3699 fn variable_reference_with_type_annotation() {
3700 let source = r#"
3701 import type { StorybookConfig } from '@storybook/react-vite';
3702 const config: StorybookConfig = {
3703 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
3704 framework: "@storybook/react-vite"
3705 };
3706 export default config;
3707 "#;
3708 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
3709 assert_eq!(
3710 addons,
3711 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
3712 );
3713
3714 let framework = extract_config_string(source, &ts_path(), &["framework"]);
3715 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
3716 }
3717
3718 #[test]
3719 fn variable_reference_with_define_config() {
3720 let source = r#"
3721 import { defineConfig } from 'vitest/config';
3722 const config = defineConfig({
3723 test: {
3724 include: ["**/*.test.ts"]
3725 }
3726 });
3727 export default config;
3728 "#;
3729 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
3730 assert_eq!(include, vec!["**/*.test.ts"]);
3731 }
3732
3733 #[test]
3734 fn ts_satisfies_direct_export() {
3735 let source = r#"
3736 export default {
3737 testDir: "./tests"
3738 } satisfies PlaywrightTestConfig;
3739 "#;
3740 let val = extract_config_string(source, &ts_path(), &["testDir"]);
3741 assert_eq!(val, Some("./tests".to_string()));
3742 }
3743
3744 #[test]
3745 fn ts_as_direct_export() {
3746 let source = r#"
3747 export default {
3748 testDir: "./tests"
3749 } as const;
3750 "#;
3751 let val = extract_config_string(source, &ts_path(), &["testDir"]);
3752 assert_eq!(val, Some("./tests".to_string()));
3753 }
3754
3755 fn aliases(source: &str) -> Vec<(String, String)> {
3758 extract_config_aliases(source, &js_path(), &["resolve", "alias"])
3759 }
3760
3761 #[test]
3762 fn aliases_inline_object_still_extracted() {
3763 let source = r#"
3765 export default defineConfig({
3766 resolve: { alias: { "@": "./src", utils: "../../utils" } }
3767 });
3768 "#;
3769 let mut got = aliases(source);
3770 got.sort();
3771 assert_eq!(
3772 got,
3773 vec![
3774 ("@".to_string(), "./src".to_string()),
3775 ("utils".to_string(), "../../utils".to_string()),
3776 ]
3777 );
3778 }
3779
3780 #[test]
3781 fn aliases_inline_array_still_extracted() {
3782 let source = r#"
3783 export default defineConfig({
3784 resolve: { alias: [{ find: "@", replacement: "./src" }] }
3785 });
3786 "#;
3787 assert_eq!(
3788 aliases(source),
3789 vec![("@".to_string(), "./src".to_string())]
3790 );
3791 }
3792
3793 #[test]
3794 fn aliases_local_const_array_identifier() {
3795 let source = r#"
3796 const sharedAliases = [{ find: "@", replacement: "./src" }];
3797 export default defineConfig({ resolve: { alias: sharedAliases } });
3798 "#;
3799 assert_eq!(
3800 aliases(source),
3801 vec![("@".to_string(), "./src".to_string())]
3802 );
3803 }
3804
3805 #[test]
3806 fn aliases_local_const_object_identifier() {
3807 let source = r#"
3808 const sharedAliases = { "@": "./src" };
3809 export default defineConfig({ resolve: { alias: sharedAliases } });
3810 "#;
3811 assert_eq!(
3812 aliases(source),
3813 vec![("@".to_string(), "./src".to_string())]
3814 );
3815 }
3816
3817 #[test]
3818 fn aliases_array_spread_of_identifiers_and_inline() {
3819 let source = r##"
3820 const a = [{ find: "@", replacement: "./src" }];
3821 const b = [{ find: "~", replacement: "./lib" }];
3822 export default defineConfig({
3823 resolve: { alias: [...a, ...b, { find: "#", replacement: "./test" }] }
3824 });
3825 "##;
3826 let mut got = aliases(source);
3827 got.sort();
3828 assert_eq!(
3829 got,
3830 vec![
3831 ("#".to_string(), "./test".to_string()),
3832 ("@".to_string(), "./src".to_string()),
3833 ("~".to_string(), "./lib".to_string()),
3834 ]
3835 );
3836 }
3837
3838 #[test]
3839 fn aliases_object_spread_of_identifier_and_inline() {
3840 let source = r#"
3841 const base = { "@": "./src" };
3842 export default defineConfig({
3843 resolve: { alias: { ...base, "~": "./lib" } }
3844 });
3845 "#;
3846 let mut got = aliases(source);
3847 got.sort();
3848 assert_eq!(
3849 got,
3850 vec![
3851 ("@".to_string(), "./src".to_string()),
3852 ("~".to_string(), "./lib".to_string()),
3853 ]
3854 );
3855 }
3856
3857 #[test]
3858 fn aliases_local_const_chained_identifier() {
3859 let source = r#"
3861 const real = [{ find: "@", replacement: "./src" }];
3862 const alias2 = real;
3863 export default defineConfig({ resolve: { alias: alias2 } });
3864 "#;
3865 assert_eq!(
3866 aliases(source),
3867 vec![("@".to_string(), "./src".to_string())]
3868 );
3869 }
3870
3871 #[test]
3872 fn aliases_imported_named_identifier_from_sibling() {
3873 let dir = tempfile::tempdir().unwrap();
3874 std::fs::write(
3875 dir.path().join("vite.shared.js"),
3876 r#"export const sharedAliases = [
3877 { find: "@", replacement: new URL("./src", import.meta.url).pathname },
3878 ];"#,
3879 )
3880 .unwrap();
3881 let config = dir.path().join("vite.config.js");
3882 let source = r#"
3883 import { defineConfig } from "vite";
3884 import { sharedAliases } from "./vite.shared.js";
3885 export default defineConfig({ resolve: { alias: sharedAliases } });
3886 "#;
3887 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
3888 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
3889 }
3890
3891 #[test]
3892 fn aliases_imported_extensionless_specifier_probed() {
3893 let dir = tempfile::tempdir().unwrap();
3894 std::fs::write(
3895 dir.path().join("aliases.mjs"),
3896 r#"export const sharedAliases = { "@": "./src" };"#,
3897 )
3898 .unwrap();
3899 let config = dir.path().join("vite.config.ts");
3900 let source = r#"
3901 import { sharedAliases } from "./aliases";
3902 export default defineConfig({ resolve: { alias: sharedAliases } });
3903 "#;
3904 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
3905 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
3906 }
3907
3908 #[test]
3909 fn aliases_imported_default_export_from_sibling() {
3910 let dir = tempfile::tempdir().unwrap();
3911 std::fs::write(
3912 dir.path().join("aliases.js"),
3913 r#"export default [{ find: "@", replacement: "./src" }];"#,
3914 )
3915 .unwrap();
3916 let config = dir.path().join("vite.config.js");
3917 let source = r#"
3918 import sharedAliases from "./aliases.js";
3919 export default defineConfig({ resolve: { alias: sharedAliases } });
3920 "#;
3921 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
3922 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
3923 }
3924
3925 #[test]
3926 fn aliases_imported_spread_from_two_siblings() {
3927 let dir = tempfile::tempdir().unwrap();
3928 std::fs::write(
3929 dir.path().join("a.js"),
3930 r#"export const a = [{ find: "@", replacement: "./src" }];"#,
3931 )
3932 .unwrap();
3933 std::fs::write(
3934 dir.path().join("b.js"),
3935 r#"export const b = [{ find: "~", replacement: "./lib" }];"#,
3936 )
3937 .unwrap();
3938 let config = dir.path().join("vite.config.js");
3939 let source = r#"
3940 import { a } from "./a.js";
3941 import { b } from "./b.js";
3942 export default defineConfig({ resolve: { alias: [...a, ...b] } });
3943 "#;
3944 let mut got = extract_config_aliases(source, &config, &["resolve", "alias"]);
3945 got.sort();
3946 assert_eq!(
3947 got,
3948 vec![
3949 ("@".to_string(), "./src".to_string()),
3950 ("~".to_string(), "./lib".to_string()),
3951 ]
3952 );
3953 }
3954
3955 #[test]
3956 fn aliases_import_cycle_terminates() {
3957 let dir = tempfile::tempdir().unwrap();
3960 std::fs::write(
3961 dir.path().join("a.js"),
3962 r#"import { b } from "./b.js";
3963 export const a = [{ find: "@", replacement: "./src" }, ...b];"#,
3964 )
3965 .unwrap();
3966 std::fs::write(
3967 dir.path().join("b.js"),
3968 r#"import { a } from "./a.js";
3969 export const b = [...a];"#,
3970 )
3971 .unwrap();
3972 let config = dir.path().join("vite.config.js");
3973 let source = r#"
3974 import { a } from "./a.js";
3975 export default defineConfig({ resolve: { alias: a } });
3976 "#;
3977 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
3978 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
3979 }
3980
3981 #[test]
3982 fn aliases_non_relative_import_not_followed() {
3983 let source = r#"
3986 import { sharedAliases } from "some-pkg";
3987 export default defineConfig({ resolve: { alias: sharedAliases } });
3988 "#;
3989 let dir = tempfile::tempdir().unwrap();
3990 let config = dir.path().join("vite.config.js");
3991 assert!(extract_config_aliases(source, &config, &["resolve", "alias"]).is_empty());
3992 }
3993
3994 #[test]
3995 fn aliases_object_array_value_takes_first_entry() {
3996 let source = r#"
4001 export default {
4002 compilerOptions: { paths: { "@/*": ["./src/*"], "~/*": ["./lib/*", "./vendor/*"] } }
4003 };
4004 "#;
4005 let mut got = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
4006 got.sort();
4007 assert_eq!(
4008 got,
4009 vec![
4010 ("@/*".to_string(), "./src/*".to_string()),
4011 ("~/*".to_string(), "./lib/*".to_string()),
4012 ]
4013 );
4014 }
4015
4016 #[test]
4017 fn aliases_kinded_preserves_is_bare_through_resolution() {
4018 let source = r#"
4021 const a = [{ find: "lodash-es", replacement: "lodash" }];
4022 export default defineConfig({
4023 resolve: { alias: [...a, { find: "@", replacement: "./src" }] }
4024 });
4025 "#;
4026 let mut got = extract_config_aliases_kinded(source, &js_path(), &["resolve", "alias"]);
4027 got.sort();
4028 assert_eq!(
4029 got,
4030 vec![
4031 ("@".to_string(), "./src".to_string(), false),
4032 ("lodash-es".to_string(), "lodash".to_string(), true),
4033 ]
4034 );
4035 }
4036
4037 #[test]
4038 fn aliases_kinded_preserves_is_bare_through_imported_spread() {
4039 let dir = tempfile::tempdir().unwrap();
4040 std::fs::write(
4041 dir.path().join("aliases.js"),
4042 r#"export const packageAliases = [{ find: "lodash-es", replacement: "lodash" }];"#,
4043 )
4044 .unwrap();
4045 let config = dir.path().join("vite.config.js");
4046 let source = r#"
4047 import { packageAliases } from "./aliases.js";
4048 export default defineConfig({
4049 resolve: { alias: [...packageAliases, { find: "@", replacement: "./src" }] }
4050 });
4051 "#;
4052 let mut got = extract_config_aliases_kinded(source, &config, &["resolve", "alias"]);
4053 got.sort();
4054 assert_eq!(
4055 got,
4056 vec![
4057 ("@".to_string(), "./src".to_string(), false),
4058 ("lodash-es".to_string(), "lodash".to_string(), true),
4059 ]
4060 );
4061 }
4062
4063 #[test]
4066 fn extract_command_string_literal() {
4067 let source = r#"export default { start: "node server.js" };"#;
4068 let val = extract_config_command(source, &js_path(), &["start"]);
4069 assert_eq!(val, Some("node server.js".to_string()));
4070 }
4071
4072 #[test]
4073 fn extract_command_nested_path() {
4074 let source = r#"
4075 export default {
4076 scripts: {
4077 dev: "vite dev"
4078 }
4079 };
4080 "#;
4081 let val = extract_config_command(source, &js_path(), &["scripts", "dev"]);
4082 assert_eq!(val, Some("vite dev".to_string()));
4083 }
4084
4085 #[test]
4086 fn extract_command_missing_key_returns_none() {
4087 let source = r#"export default { other: "val" };"#;
4088 let val = extract_config_command(source, &js_path(), &["start"]);
4089 assert!(val.is_none());
4090 }
4091
4092 #[test]
4093 fn extract_command_ts_as_expression() {
4094 let source = r#"export default { start: "node server.js" as string };"#;
4095 let val = extract_config_command(source, &ts_path(), &["start"]);
4096 assert_eq!(val, Some("node server.js".to_string()));
4097 }
4098
4099 #[test]
4100 fn extract_command_ts_satisfies_expression() {
4101 let source = r#"export default { start: "node server.js" satisfies string };"#;
4102 let val = extract_config_command(source, &ts_path(), &["start"]);
4103 assert_eq!(val, Some("node server.js".to_string()));
4104 }
4105
4106 #[test]
4107 fn extract_command_parenthesized_expression() {
4108 let source = r#"export default { start: ("node server.js") };"#;
4109 let val = extract_config_command(source, &js_path(), &["start"]);
4110 assert_eq!(val, Some("node server.js".to_string()));
4111 }
4112
4113 #[test]
4114 fn extract_command_empty_path_returns_none() {
4115 let source = r#"export default { start: "node server.js" };"#;
4116 let val = extract_config_command(source, &js_path(), &[]);
4117 assert!(val.is_none());
4118 }
4119
4120 #[test]
4123 fn truthy_bool_or_object_with_true_value() {
4124 let source = r"export default { typescript: true };";
4125 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4126 assert!(result);
4127 }
4128
4129 #[test]
4130 fn truthy_bool_or_object_with_false_value() {
4131 let source = r"export default { typescript: false };";
4132 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4133 assert!(!result);
4134 }
4135
4136 #[test]
4137 fn truthy_bool_or_object_with_object_value() {
4138 let source = r#"export default { typescript: { reactDocgen: "react-docgen" } };"#;
4139 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4140 assert!(result);
4141 }
4142
4143 #[test]
4144 fn truthy_bool_or_object_missing_key_returns_false() {
4145 let source = r"export default { other: true };";
4146 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4147 assert!(!result);
4148 }
4149
4150 #[test]
4151 fn truthy_bool_or_object_with_string_value_returns_false() {
4152 let source = r#"export default { typescript: "yes" };"#;
4154 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4155 assert!(!result);
4156 }
4157
4158 #[test]
4159 fn truthy_bool_or_object_ts_satisfies_wrapper() {
4160 let source = r"export default { typescript: (true satisfies boolean) };";
4161 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4162 assert!(result);
4163 }
4164
4165 #[test]
4166 fn truthy_bool_or_object_ts_as_wrapper() {
4167 let source = r"export default { typescript: (true as boolean) };";
4168 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4169 assert!(result);
4170 }
4171
4172 #[test]
4173 fn truthy_bool_or_object_parenthesized_wrapper() {
4174 let source = r"export default { typescript: (true) };";
4175 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4176 assert!(result);
4177 }
4178
4179 #[test]
4185 fn static_dir_entries_object_form_exercises_property_string() {
4186 let source = r#"
4189 export default {
4190 staticDirs: [
4191 { from: "./media", to: "/assets" }
4192 ]
4193 };
4194 "#;
4195 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
4196 assert_eq!(
4197 entries,
4198 vec![("./media".to_string(), Some("/assets".to_string()))]
4199 );
4200 }
4201
4202 #[test]
4205 fn expression_to_path_values_array_form_via_config_path() {
4206 let source = r#"export default { entries: ["./src/a.ts", "./src/b.ts"] };"#;
4209 let result = extract_config_string_or_array(source, &js_path(), &["entries"]);
4210 assert_eq!(result, vec!["./src/a.ts", "./src/b.ts"]);
4211 }
4212
4213 #[test]
4216 fn array_nested_aliases_object_form() {
4217 let source = r#"
4218 export default {
4219 test: {
4220 projects: [
4221 {
4222 resolve: {
4223 alias: { "@": "./src" }
4224 }
4225 }
4226 ]
4227 }
4228 };
4229 "#;
4230 let aliases = extract_config_array_nested_aliases(
4231 source,
4232 &ts_path(),
4233 &["test", "projects"],
4234 &["resolve", "alias"],
4235 );
4236 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
4237 }
4238
4239 #[test]
4240 fn array_nested_aliases_array_form_find_replacement() {
4241 let source = r#"
4242 export default {
4243 projects: [
4244 {
4245 resolve: {
4246 alias: [
4247 { find: "@", replacement: "./src" },
4248 { find: "~", replacement: "./lib" }
4249 ]
4250 }
4251 }
4252 ]
4253 };
4254 "#;
4255 let aliases = extract_config_array_nested_aliases(
4256 source,
4257 &ts_path(),
4258 &["projects"],
4259 &["resolve", "alias"],
4260 );
4261 assert_eq!(
4262 aliases,
4263 vec![
4264 ("@".to_string(), "./src".to_string()),
4265 ("~".to_string(), "./lib".to_string()),
4266 ]
4267 );
4268 }
4269
4270 #[test]
4271 fn array_nested_aliases_empty_when_path_is_not_array() {
4272 let source = r#"export default { test: { projects: "not-an-array" } };"#;
4273 let aliases = extract_config_array_nested_aliases(
4274 source,
4275 &ts_path(),
4276 &["test", "projects"],
4277 &["resolve", "alias"],
4278 );
4279 assert!(aliases.is_empty());
4280 }
4281
4282 #[test]
4283 fn array_nested_aliases_kinded_tracks_is_bare() {
4284 let source = r#"
4285 export default {
4286 projects: [
4287 {
4288 resolve: {
4289 alias: [
4290 { find: "lodash-es", replacement: "lodash" },
4291 { find: "@", replacement: "./src" }
4292 ]
4293 }
4294 }
4295 ]
4296 };
4297 "#;
4298 let mut aliases = extract_config_array_nested_aliases_kinded(
4299 source,
4300 &ts_path(),
4301 &["projects"],
4302 &["resolve", "alias"],
4303 );
4304 aliases.sort();
4305 assert_eq!(
4306 aliases,
4307 vec![
4308 ("@".to_string(), "./src".to_string(), false),
4309 ("lodash-es".to_string(), "lodash".to_string(), true),
4310 ]
4311 );
4312 }
4313
4314 #[test]
4317 fn default_export_array_aliases_kinded_extracts_from_workspace_config() {
4318 let source = r#"
4319 export default [
4320 {
4321 resolve: {
4322 alias: { "@": "./src" }
4323 }
4324 },
4325 {
4326 resolve: {
4327 alias: [{ find: "~", replacement: "./lib" }]
4328 }
4329 }
4330 ];
4331 "#;
4332 let mut aliases =
4333 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4334 aliases.sort();
4335 assert_eq!(
4336 aliases,
4337 vec![
4338 ("@".to_string(), "./src".to_string(), false),
4339 ("~".to_string(), "./lib".to_string(), false),
4340 ]
4341 );
4342 }
4343
4344 #[test]
4345 fn default_export_array_aliases_kinded_define_workspace_wrapper() {
4346 let source = r#"
4347 export default defineWorkspace([
4348 {
4349 resolve: { alias: { "@": "./src" } }
4350 }
4351 ]);
4352 "#;
4353 let aliases =
4354 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4355 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
4356 }
4357
4358 #[test]
4359 fn default_export_array_aliases_kinded_empty_when_no_alias_path() {
4360 let source = r#"
4361 export default [
4362 { test: { include: ["**/*.test.ts"] } }
4363 ];
4364 "#;
4365 let aliases =
4366 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4367 assert!(aliases.is_empty());
4368 }
4369
4370 #[test]
4373 fn config_default_export_unreachable_when_no_export() {
4374 let source = r"const x = 42;";
4375 assert!(config_default_export_unreachable(source, &js_path()));
4376 }
4377
4378 #[test]
4379 fn config_default_export_unreachable_false_for_object_export() {
4380 let source = r#"export default { key: "value" };"#;
4381 assert!(!config_default_export_unreachable(source, &js_path()));
4382 }
4383
4384 #[test]
4385 fn config_default_export_unreachable_false_for_array_export() {
4386 let source = r#"export default ["a", "b"];"#;
4387 assert!(!config_default_export_unreachable(source, &js_path()));
4388 }
4389
4390 #[test]
4391 fn config_default_export_unreachable_true_for_function_without_return_object() {
4392 let source = r"export default function config() { return 42; }";
4394 assert!(config_default_export_unreachable(source, &js_path()));
4395 }
4396
4397 #[test]
4400 fn static_dir_entries_string_and_object_form() {
4401 let source = r#"
4402 export default {
4403 staticDirs: [
4404 "./public",
4405 { from: "../assets", to: "/static" }
4406 ]
4407 };
4408 "#;
4409 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
4410 assert_eq!(
4411 entries,
4412 vec![
4413 ("./public".to_string(), None),
4414 ("../assets".to_string(), Some("/static".to_string())),
4415 ]
4416 );
4417 }
4418
4419 #[test]
4420 fn static_dir_entries_object_without_to() {
4421 let source = r#"
4422 export default {
4423 staticDirs: [
4424 { from: "./media" }
4425 ]
4426 };
4427 "#;
4428 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
4429 assert_eq!(entries, vec![("./media".to_string(), None)]);
4430 }
4431
4432 #[test]
4433 fn static_dir_entries_object_missing_from_skipped() {
4434 let source = r#"
4436 export default {
4437 staticDirs: [
4438 { to: "/target" },
4439 "./public"
4440 ]
4441 };
4442 "#;
4443 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
4444 assert_eq!(entries, vec![("./public".to_string(), None)]);
4445 }
4446
4447 #[test]
4448 fn static_dir_entries_empty_when_not_array() {
4449 let source = r#"export default { staticDirs: "./public" };"#;
4450 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
4451 assert!(entries.is_empty());
4452 }
4453
4454 #[test]
4457 fn aliases_array_form_missing_find_or_replacement_skipped() {
4458 let source = r#"
4460 export default {
4461 resolve: {
4462 alias: [
4463 { replacement: "./src" },
4464 { find: "@" },
4465 { find: "~", replacement: "./lib" }
4466 ]
4467 }
4468 };
4469 "#;
4470 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
4471 assert_eq!(aliases, vec![("~".to_string(), "./lib".to_string())]);
4472 }
4473
4474 #[test]
4475 fn aliases_object_form_computed_key_skipped() {
4476 let source = r#"
4478 const k = "@";
4479 export default {
4480 resolve: {
4481 alias: {
4482 [k]: "./src",
4483 "~": "./lib"
4484 }
4485 }
4486 };
4487 "#;
4488 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
4489 assert_eq!(aliases, vec![("~".to_string(), "./lib".to_string())]);
4491 }
4492
4493 #[test]
4494 fn aliases_kinded_array_form_path_replacement_is_not_bare() {
4495 let source = r#"
4496 export default {
4497 resolve: {
4498 alias: [{ find: "@", replacement: "./src" }]
4499 }
4500 };
4501 "#;
4502 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4503 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
4504 }
4505
4506 #[test]
4507 fn aliases_kinded_object_form_bare_and_path_discrimination() {
4508 let source = r#"
4509 export default {
4510 resolve: {
4511 alias: {
4512 "lodash-es": "lodash",
4513 "@": "./src"
4514 }
4515 }
4516 };
4517 "#;
4518 let mut aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4519 aliases.sort();
4520 assert_eq!(
4521 aliases,
4522 vec![
4523 ("@".to_string(), "./src".to_string(), false),
4524 ("lodash-es".to_string(), "lodash".to_string(), true),
4525 ]
4526 );
4527 }
4528
4529 #[test]
4530 fn aliases_kinded_parent_relative_replacement_is_not_bare() {
4531 let source = r#"
4532 export default {
4533 resolve: { alias: { "@": "../shared/src" } }
4534 };
4535 "#;
4536 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4537 assert_eq!(
4538 aliases,
4539 vec![("@".to_string(), "../shared/src".to_string(), false)]
4540 );
4541 }
4542
4543 #[test]
4544 fn aliases_kinded_absolute_replacement_is_not_bare() {
4545 let source = r#"
4546 export default {
4547 resolve: { alias: { "@": "/absolute/path" } }
4548 };
4549 "#;
4550 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4551 assert_eq!(
4552 aliases,
4553 vec![("@".to_string(), "/absolute/path".to_string(), false)]
4554 );
4555 }
4556
4557 #[test]
4560 fn default_export_array_ts_as_wrapper() {
4561 let source = r"export default [] as string[];";
4563 assert!(!config_default_export_unreachable(source, &js_path()));
4564 }
4565
4566 #[test]
4567 fn default_export_array_ts_satisfies_wrapper() {
4568 let source = r"export default [] satisfies string[];";
4569 assert!(!config_default_export_unreachable(source, &ts_path()));
4570 }
4571
4572 #[test]
4573 fn default_export_array_define_config_call_wrapper() {
4574 let source = r#"export default defineConfig(["**/*.test.ts"]);"#;
4575 assert!(!config_default_export_unreachable(source, &ts_path()));
4576 }
4577
4578 #[test]
4581 fn shallow_strings_object_with_string_values() {
4582 let source = r#"
4584 export default {
4585 plugins: {
4586 autoprefixer: "autoprefixer",
4587 tailwindcss: "tailwindcss"
4588 }
4589 };
4590 "#;
4591 let vals = extract_config_shallow_strings(source, &js_path(), "plugins");
4592 assert!(vals.contains(&"autoprefixer".to_string()));
4593 assert!(vals.contains(&"tailwindcss".to_string()));
4594 }
4595
4596 #[test]
4597 fn shallow_strings_object_with_sub_array_first_element() {
4598 let source = r#"
4600 export default {
4601 reporters: {
4602 main: ["jest-junit", { outputFile: "report.xml" }],
4603 alt: ["html-reporter"]
4604 }
4605 };
4606 "#;
4607 let vals = extract_config_shallow_strings(source, &js_path(), "reporters");
4608 assert!(vals.contains(&"jest-junit".to_string()));
4609 assert!(vals.contains(&"html-reporter".to_string()));
4610 }
4611
4612 #[test]
4615 fn shallow_strings_or_object_property_non_array_single_string() {
4616 let source = r#"export default { jsPlugins: "eslint-plugin-foo" };"#;
4618 let vals = extract_config_shallow_strings_or_object_property(
4619 source,
4620 &ts_path(),
4621 "jsPlugins",
4622 "specifier",
4623 );
4624 assert_eq!(vals, vec!["eslint-plugin-foo"]);
4625 }
4626
4627 #[test]
4628 fn shallow_strings_or_object_property_ts_satisfies_array_element() {
4629 let source = r#"
4631 export default {
4632 jsPlugins: [
4633 ("eslint-plugin-a" satisfies string)
4634 ]
4635 };
4636 "#;
4637 let vals = extract_config_shallow_strings_or_object_property(
4638 source,
4639 &ts_path(),
4640 "jsPlugins",
4641 "specifier",
4642 );
4643 assert_eq!(vals, vec!["eslint-plugin-a"]);
4644 }
4645
4646 #[test]
4647 fn shallow_strings_or_object_property_ts_as_array_element() {
4648 let source = r#"
4649 export default {
4650 jsPlugins: [
4651 ("eslint-plugin-b" as string)
4652 ]
4653 };
4654 "#;
4655 let vals = extract_config_shallow_strings_or_object_property(
4656 source,
4657 &ts_path(),
4658 "jsPlugins",
4659 "specifier",
4660 );
4661 assert_eq!(vals, vec!["eslint-plugin-b"]);
4662 }
4663
4664 #[test]
4665 fn shallow_strings_or_object_property_sub_array_first_element_string() {
4666 let source = r#"
4668 export default {
4669 jsPlugins: [
4670 ["eslint-plugin-tuple-pkg", { options: true }]
4671 ]
4672 };
4673 "#;
4674 let vals = extract_config_shallow_strings_or_object_property(
4675 source,
4676 &ts_path(),
4677 "jsPlugins",
4678 "specifier",
4679 );
4680 assert_eq!(vals, vec!["eslint-plugin-tuple-pkg"]);
4681 }
4682
4683 #[test]
4686 fn array_object_command_pairs_basic() {
4687 let source = r#"
4688 export default {
4689 webServer: [
4690 { command: "node server.js", cwd: "packages/api" },
4691 { command: "vite dev" }
4692 ]
4693 };
4694 "#;
4695 let pairs = extract_config_array_object_command_pairs(
4696 source,
4697 &ts_path(),
4698 &["webServer"],
4699 "command",
4700 "cwd",
4701 );
4702 assert_eq!(
4703 pairs,
4704 vec![
4705 (
4706 "node server.js".to_string(),
4707 Some("packages/api".to_string())
4708 ),
4709 ("vite dev".to_string(), None),
4710 ]
4711 );
4712 }
4713
4714 #[test]
4715 fn array_object_command_pairs_skips_missing_command() {
4716 let source = r#"
4717 export default {
4718 webServer: [
4719 { cwd: "packages/api" },
4720 { command: "vite dev", cwd: "apps/web" }
4721 ]
4722 };
4723 "#;
4724 let pairs = extract_config_array_object_command_pairs(
4725 source,
4726 &ts_path(),
4727 &["webServer"],
4728 "command",
4729 "cwd",
4730 );
4731 assert_eq!(
4732 pairs,
4733 vec![("vite dev".to_string(), Some("apps/web".to_string()))]
4734 );
4735 }
4736
4737 #[test]
4738 fn array_object_command_pairs_empty_when_not_array() {
4739 let source = r#"export default { webServer: { command: "vite dev" } };"#;
4740 let pairs = extract_config_array_object_command_pairs(
4741 source,
4742 &ts_path(),
4743 &["webServer"],
4744 "command",
4745 "cwd",
4746 );
4747 assert!(pairs.is_empty());
4748 }
4749
4750 #[test]
4753 fn normalize_config_path_empty_string_returns_none() {
4754 let config_path = PathBuf::from("/project/vite.config.ts");
4755 let root = PathBuf::from("/project");
4756 assert_eq!(normalize_config_path("", &config_path, &root), None);
4757 }
4758
4759 #[test]
4760 fn normalize_config_path_escapes_to_above_root_returns_none() {
4761 let config_path = PathBuf::from("/project/vite.config.ts");
4762 let root = PathBuf::from("/project");
4763 assert_eq!(
4765 normalize_config_path("../../etc", &config_path, &root),
4766 None
4767 );
4768 }
4769
4770 #[test]
4771 fn normalize_config_path_dot_slash_resolves_relative_to_config_dir() {
4772 let config_path = PathBuf::from("/project/packages/app/vite.config.ts");
4773 let root = PathBuf::from("/project");
4774 assert_eq!(
4775 normalize_config_path("./src", &config_path, &root),
4776 Some("packages/app/src".to_string())
4777 );
4778 }
4779
4780 #[test]
4783 fn json_config_array_of_arrays_via_shallow_strings() {
4784 let source = r#"{"reporters": ["default", ["jest-junit", {}]]}"#;
4786 let vals = extract_config_shallow_strings(source, &json_path(), "reporters");
4787 assert_eq!(vals, vec!["default", "jest-junit"]);
4788 }
4789
4790 #[test]
4793 fn extract_config_path_string_literal() {
4794 let source = r#"export default { outDir: "./dist" };"#;
4795 let path = extract_config_path(source, &js_path(), &["outDir"]);
4796 assert_eq!(
4797 path.map(|p| p.to_string_lossy().replace('\\', "/")),
4798 Some("./dist".to_string())
4799 );
4800 }
4801
4802 #[test]
4803 fn extract_config_path_with_resolve_call() {
4804 let source = r#"
4805 import { resolve } from "node:path";
4806 export default { outDir: resolve(__dirname, "dist") };
4807 "#;
4808 let path = extract_config_path(source, &js_path(), &["outDir"]);
4809 assert_eq!(
4810 path.map(|p| p.to_string_lossy().replace('\\', "/")),
4811 Some("dist".to_string())
4812 );
4813 }
4814
4815 #[test]
4816 fn extract_config_path_missing_key_returns_none() {
4817 let source = r#"export default { other: "val" };"#;
4818 let path = extract_config_path(source, &js_path(), &["outDir"]);
4819 assert!(path.is_none());
4820 }
4821
4822 #[test]
4825 fn extract_imports_and_requires_both_forms() {
4826 let source = r"
4827 import foo from 'foo-pkg';
4828 require('bar-pkg');
4829 export default {};
4830 ";
4831 let sources = extract_imports_and_requires(source, &js_path());
4832 assert!(sources.contains(&"foo-pkg".to_string()));
4833 assert!(sources.contains(&"bar-pkg".to_string()));
4834 }
4835
4836 #[test]
4837 fn extract_imports_and_requires_skips_non_require_calls() {
4838 let source = r"
4839 import foo from 'foo-pkg';
4840 someOtherCall('bar-pkg');
4841 export default {};
4842 ";
4843 let sources = extract_imports_and_requires(source, &js_path());
4844 assert_eq!(sources, vec!["foo-pkg"]);
4845 }
4846
4847 #[test]
4850 fn nested_shallow_strings_non_object_nested_returns_empty() {
4851 let source = r#"export default { test: "not-an-object" };"#;
4853 let vals =
4854 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4855 assert!(vals.is_empty());
4856 }
4857
4858 #[test]
4861 fn vite_react_babel_dependencies_namespace_import() {
4862 let source = r#"
4863 import * as react from "@vitejs/plugin-react";
4864
4865 export default defineConfig({
4866 plugins: [
4867 react.default({
4868 babel: {
4869 plugins: ["babel-plugin-ns"],
4870 },
4871 }),
4872 ],
4873 });
4874 "#;
4875 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
4876 assert_eq!(deps, vec!["babel-plugin-ns".to_string()]);
4877 }
4878
4879 #[test]
4882 fn property_strings_deeply_nested_object_values() {
4883 let source = r#"
4885 export default {
4886 settings: {
4887 a: "val-a",
4888 b: {
4889 c: "val-c",
4890 d: ["val-d1", "val-d2"]
4891 }
4892 }
4893 };
4894 "#;
4895 let values = extract_config_property_strings(source, &js_path(), "settings");
4896 assert!(values.contains(&"val-a".to_string()));
4897 assert!(values.contains(&"val-c".to_string()));
4898 assert!(values.contains(&"val-d1".to_string()));
4899 assert!(values.contains(&"val-d2".to_string()));
4900 }
4901
4902 #[test]
4905 fn aliases_exported_const_form_resolves() {
4906 let source = r#"
4908 export const sharedAliases = { "@": "./src" };
4909 export default defineConfig({ resolve: { alias: sharedAliases } });
4910 "#;
4911 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
4912 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
4913 }
4914
4915 #[test]
4918 fn aliases_imported_from_sibling_directory_index_file() {
4919 let dir = tempfile::tempdir().unwrap();
4922 let aliases_dir = dir.path().join("aliases");
4923 std::fs::create_dir_all(&aliases_dir).unwrap();
4924 std::fs::write(
4925 aliases_dir.join("index.js"),
4926 r#"export const aliases = [{ find: "@", replacement: "./src" }];"#,
4927 )
4928 .unwrap();
4929 let config = dir.path().join("vite.config.js");
4930 let source = r#"
4931 import { aliases } from "./aliases";
4932 export default defineConfig({ resolve: { alias: aliases } });
4933 "#;
4934 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4935 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4936 }
4937
4938 #[test]
4941 fn aliases_depth_limit_terminates_deep_chain() {
4942 let source = r#"
4945 const a9 = [{ find: "@", replacement: "./src" }];
4946 const a8 = a9;
4947 const a7 = a8;
4948 const a6 = a7;
4949 const a5 = a6;
4950 const a4 = a5;
4951 const a3 = a4;
4952 const a2 = a3;
4953 const a1 = a2;
4954 export default defineConfig({ resolve: { alias: a1 } });
4955 "#;
4956 let got = extract_config_aliases(source, &js_path(), &["resolve", "alias"]);
4958 let _ = got; }
4960
4961 #[test]
4964 fn extract_aliases_file_url_to_path_new_url() {
4965 let source = r#"
4967 import { fileURLToPath, URL } from 'node:url';
4968 export default {
4969 resolve: {
4970 alias: {
4971 "@": fileURLToPath(new URL("./src", import.meta.url))
4972 }
4973 }
4974 };
4975 "#;
4976 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
4977 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
4978 }
4979
4980 #[test]
4981 fn extract_path_via_new_url_pathname_member() {
4982 let source = r#"
4984 export default {
4985 resolve: {
4986 alias: {
4987 "@": new URL("./src", import.meta.url).pathname
4988 }
4989 }
4990 };
4991 "#;
4992 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
4993 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
4994 }
4995
4996 #[test]
4999 fn truthy_bool_or_object_null_literal_returns_false() {
5000 let source = r"export default { typescript: null };";
5002 let result = extract_config_truthy_bool_or_object(source, &js_path(), &["typescript"]);
5003 assert!(!result);
5004 }
5005
5006 #[test]
5009 fn string_array_non_array_value_returns_empty() {
5010 let source = r#"export default { items: "not-an-array" };"#;
5011 let result = extract_config_string_array(source, &js_path(), &["items"]);
5012 assert!(result.is_empty());
5013 }
5014
5015 #[test]
5018 fn object_nested_empty_when_inner_value_is_not_object() {
5019 let source = r#"export default { targets: { build: "not-an-object" } };"#;
5021 let results =
5022 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
5023 assert!(results.is_empty());
5024 }
5025
5026 #[test]
5029 fn array_nested_string_or_array_missing_inner_path_returns_empty() {
5030 let source = r#"
5031 export default {
5032 test: {
5033 projects: [
5034 { test: { include: ["**/*.test.ts"] } }
5035 ]
5036 }
5037 };
5038 "#;
5039 let results = extract_config_array_nested_string_or_array(
5040 source,
5041 &ts_path(),
5042 &["test", "projects"],
5043 &["test", "setupFiles"],
5044 );
5045 assert!(results.is_empty());
5046 }
5047
5048 #[test]
5049 fn wrapped_named_const_default_export_resolves() {
5050 let source = r#"
5053 import createMDX from "@next/mdx";
5054 const nextConfig = { pageExtensions: ["ts", "tsx", "md", "mdx"] };
5055 const withMDX = createMDX({});
5056 export default withMDX(nextConfig);
5057 "#;
5058 let exts = extract_config_string_array(source, &ts_path(), &["pageExtensions"]);
5059 assert_eq!(exts, vec!["ts", "tsx", "md", "mdx"]);
5060 }
5061
5062 #[test]
5063 fn wrapped_named_const_module_exports_resolves() {
5064 let source = r#"
5066 const nextJest = require("next/jest");
5067 const createJestConfig = nextJest();
5068 const customConfig = { testMatch: ["**/*.test.ts"] };
5069 module.exports = createJestConfig(customConfig);
5070 "#;
5071 let matches = extract_config_string_array(source, &js_path(), &["testMatch"]);
5072 assert_eq!(matches, vec!["**/*.test.ts"]);
5073 }
5074
5075 #[test]
5076 fn wrapped_named_const_nested_and_curried_resolve() {
5077 let nested = r#"
5078 const nextConfig = { pageExtensions: ["mdx"] };
5079 const withMDX = (c) => c;
5080 const withFoo = (c) => c;
5081 export default withMDX(withFoo(nextConfig));
5082 "#;
5083 assert_eq!(
5084 extract_config_string_array(nested, &js_path(), &["pageExtensions"]),
5085 vec!["mdx"]
5086 );
5087
5088 let curried = r#"
5089 const nextConfig = { pageExtensions: ["md"] };
5090 const compose = (..._p) => (c) => c;
5091 export default compose(a, b)(nextConfig);
5092 "#;
5093 assert_eq!(
5094 extract_config_string_array(curried, &js_path(), &["pageExtensions"]),
5095 vec!["md"]
5096 );
5097 }
5098
5099 #[test]
5100 fn wrapped_inline_object_still_resolves() {
5101 let source = r#"
5103 const withMDX = createMDX({});
5104 export default withMDX({ pageExtensions: ["mdx"] });
5105 "#;
5106 assert_eq!(
5107 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
5108 vec!["mdx"]
5109 );
5110 }
5111}