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