1use std::path::{Path, PathBuf};
6
7use fallow_extract::{JsText, visitor::extract_import_from_callable};
8#[allow(clippy::wildcard_imports, reason = "many AST types used")]
9use oxc_ast::ast::*;
10use oxc_span::SourceType;
11use rustc_hash::FxHashSet;
12
13#[path = "config_parser_const_inline.rs"]
14mod const_inline;
15use const_inline::holds_one_stable_object;
16#[path = "config_parser_vite_plugin_call.rs"]
17mod vite_plugin_call;
18pub(crate) use vite_plugin_call::extract_vite_plugin_call_option_path;
19use vite_plugin_call::is_plugin_binding_call;
20
21#[must_use]
23pub(crate) fn extract_imports(source: &str, path: &Path) -> Vec<String> {
24 extract_from_source(source, path, |program| {
25 let mut sources = Vec::new();
26 for stmt in &program.body {
27 if let Statement::ImportDeclaration(decl) = stmt {
28 sources.push(decl.source.value.text().into_owned());
29 }
30 }
31 Some(sources)
32 })
33 .unwrap_or_default()
34}
35
36#[must_use]
38pub(crate) fn extract_imports_and_requires(source: &str, path: &Path) -> Vec<String> {
39 extract_from_source(source, path, |program| {
40 let mut sources = Vec::new();
41 for stmt in &program.body {
42 match stmt {
43 Statement::ImportDeclaration(decl) => {
44 sources.push(decl.source.value.text().into_owned());
45 }
46 Statement::ExpressionStatement(expr) => {
47 if let Expression::CallExpression(call) = &expr.expression
48 && is_require_call(call)
49 && let Some(s) = get_require_source(call)
50 {
51 sources.push(s);
52 }
53 }
54 _ => {}
55 }
56 }
57 Some(sources)
58 })
59 .unwrap_or_default()
60}
61
62#[must_use]
64pub(crate) fn extract_config_string_array(
65 source: &str,
66 path: &Path,
67 prop_path: &[&str],
68) -> Vec<String> {
69 extract_from_source(source, path, |program| {
70 let obj = find_config_object(program)?;
71 get_nested_string_array_from_object(obj, prop_path)
72 })
73 .unwrap_or_default()
74}
75
76#[must_use]
78pub(crate) fn extract_config_string(
79 source: &str,
80 path: &Path,
81 prop_path: &[&str],
82) -> Option<String> {
83 extract_from_source(source, path, |program| {
84 let obj = find_config_object(program)?;
85 get_nested_string_from_object(obj, prop_path)
86 })
87}
88
89#[must_use]
91pub(crate) fn extract_config_command(
92 source: &str,
93 path: &Path,
94 prop_path: &[&str],
95) -> Option<String> {
96 extract_from_source(source, path, |program| {
97 let obj = find_config_object(program)?;
98 get_nested_command_from_object(obj, prop_path)
99 })
100}
101
102#[must_use]
105pub(crate) fn extract_config_property_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
106 extract_from_source(source, path, |program| {
107 let obj = find_config_object(program)?;
108 let mut values = Vec::new();
109 if let Some(prop) = find_property(obj, key) {
110 collect_all_string_values(&prop.value, &mut values);
111 }
112 Some(values)
113 })
114 .unwrap_or_default()
115}
116
117#[must_use]
119pub(crate) fn extract_config_shallow_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
120 extract_from_source(source, path, |program| {
121 let obj = find_config_object(program)?;
122 let prop = find_property(obj, key)?;
123 Some(collect_shallow_string_values(&prop.value))
124 })
125 .unwrap_or_default()
126}
127
128#[must_use]
130pub(crate) fn extract_config_shallow_strings_or_object_property(
131 source: &str,
132 path: &Path,
133 key: &str,
134 object_property: &str,
135) -> Vec<String> {
136 extract_from_source(source, path, |program| {
137 let obj = find_config_object(program)?;
138 let prop = find_property(obj, key)?;
139 Some(collect_shallow_string_or_object_property_values(
140 &prop.value,
141 object_property,
142 ))
143 })
144 .unwrap_or_default()
145}
146
147#[must_use]
149pub(crate) fn extract_config_nested_shallow_strings(
150 source: &str,
151 path: &Path,
152 outer_path: &[&str],
153 key: &str,
154) -> Vec<String> {
155 extract_from_source(source, path, |program| {
156 let obj = find_config_object(program)?;
157 let nested = get_nested_expression(obj, outer_path)?;
158 if let Expression::ObjectExpression(nested_obj) = nested {
159 let prop = find_property(nested_obj, key)?;
160 Some(collect_shallow_string_values(&prop.value))
161 } else {
162 None
163 }
164 })
165 .unwrap_or_default()
166}
167
168pub(crate) fn property_expr<'a>(
170 obj: &'a ObjectExpression<'a>,
171 key: &str,
172) -> Option<&'a Expression<'a>> {
173 find_property(obj, key).map(|prop| &prop.value)
174}
175
176pub(crate) fn property_object<'a>(
178 obj: &'a ObjectExpression<'a>,
179 key: &str,
180) -> Option<&'a ObjectExpression<'a>> {
181 property_expr(obj, key).and_then(object_expression)
182}
183
184pub(crate) fn property_string(obj: &ObjectExpression<'_>, key: &str) -> Option<String> {
186 property_expr(obj, key).and_then(expression_to_string)
187}
188
189pub(crate) fn object_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ObjectExpression<'a>> {
191 match expr {
192 Expression::ObjectExpression(obj) => Some(obj),
193 Expression::ParenthesizedExpression(paren) => object_expression(&paren.expression),
194 Expression::TSSatisfiesExpression(ts_sat) => object_expression(&ts_sat.expression),
195 Expression::TSAsExpression(ts_as) => object_expression(&ts_as.expression),
196 _ => None,
197 }
198}
199
200pub(crate) fn array_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
202 match expr {
203 Expression::ArrayExpression(arr) => Some(arr),
204 Expression::ParenthesizedExpression(paren) => array_expression(&paren.expression),
205 Expression::TSSatisfiesExpression(ts_sat) => array_expression(&ts_sat.expression),
206 Expression::TSAsExpression(ts_as) => array_expression(&ts_as.expression),
207 _ => None,
208 }
209}
210
211pub(crate) fn path_from_config_string(raw: &str) -> PathBuf {
214 PathBuf::from(raw.replace('\\', "/"))
215}
216
217pub(crate) fn path_to_config_string(path: &Path) -> String {
219 path.to_string_lossy().replace('\\', "/")
220}
221
222pub(crate) fn expression_to_path(expr: &Expression<'_>) -> Option<PathBuf> {
224 expression_to_path_string(expr).map(|path| path_from_config_string(&path))
225}
226
227pub(crate) fn expression_to_path_values(expr: &Expression<'_>) -> Vec<PathBuf> {
229 match expr {
230 Expression::ArrayExpression(arr) => arr
231 .elements
232 .iter()
233 .filter_map(|element| element.as_expression().and_then(expression_to_path))
234 .collect(),
235 _ => expression_to_path(expr).into_iter().collect(),
236 }
237}
238
239pub(crate) fn is_disabled_expression(expr: &Expression<'_>) -> bool {
241 matches!(expr, Expression::BooleanLiteral(boolean) if !boolean.value)
242 || matches!(expr, Expression::NullLiteral(_))
243}
244
245#[must_use]
247pub(crate) fn extract_config_truthy_bool_or_object(
248 source: &str,
249 path: &Path,
250 prop_path: &[&str],
251) -> bool {
252 extract_from_source(source, path, |program| {
253 let obj = find_config_object(program)?;
254 let expr = get_nested_expression(obj, prop_path)?;
255 Some(is_truthy_bool_or_object(expr))
256 })
257 .unwrap_or(false)
258}
259
260fn is_truthy_bool_or_object(expr: &Expression<'_>) -> bool {
261 match expr {
262 Expression::BooleanLiteral(boolean) => boolean.value,
263 Expression::ObjectExpression(_) => true,
264 Expression::ParenthesizedExpression(paren) => is_truthy_bool_or_object(&paren.expression),
265 Expression::TSSatisfiesExpression(ts_sat) => is_truthy_bool_or_object(&ts_sat.expression),
266 Expression::TSAsExpression(ts_as) => is_truthy_bool_or_object(&ts_as.expression),
267 _ => false,
268 }
269}
270
271#[must_use]
273pub(crate) fn extract_config_object_keys(
274 source: &str,
275 path: &Path,
276 prop_path: &[&str],
277) -> Vec<String> {
278 extract_from_source(source, path, |program| {
279 let obj = find_config_object(program)?;
280 get_nested_object_keys(obj, prop_path)
281 })
282 .unwrap_or_default()
283}
284
285#[must_use]
288pub(crate) fn extract_config_string_or_array(
289 source: &str,
290 path: &Path,
291 prop_path: &[&str],
292) -> Vec<String> {
293 extract_from_source(source, path, |program| {
294 let obj = find_config_object(program)?;
295 get_nested_string_or_array(obj, prop_path, PathScope::Program(program))
296 })
297 .unwrap_or_default()
298}
299
300#[must_use]
307pub(crate) fn extract_config_array_or_object_string_or_array(
308 source: &str,
309 path: &Path,
310 prop_path: &[&str],
311) -> Vec<String> {
312 extract_from_source(source, path, |program| {
313 let Some(arr) = find_default_export_array(program) else {
314 let obj = find_config_object(program)?;
315 return get_nested_string_or_array(obj, prop_path, PathScope::Program(program));
316 };
317 let values = arr
318 .elements
319 .iter()
320 .filter_map(|element| element.as_expression().and_then(object_expression))
321 .filter_map(|element_obj| {
322 get_nested_string_or_array(element_obj, prop_path, PathScope::Program(program))
323 })
324 .flatten()
325 .collect();
326 Some(values)
327 })
328 .unwrap_or_default()
329}
330
331#[must_use]
333pub(crate) fn extract_config_path(
334 source: &str,
335 path: &Path,
336 prop_path: &[&str],
337) -> Option<PathBuf> {
338 extract_from_source(source, path, |program| {
339 let obj = find_config_object(program)?;
340 let expr = get_nested_expression(obj, prop_path)?;
341 expression_to_path(expr)
342 })
343}
344
345#[must_use]
347pub(crate) fn extract_config_array_nested_string_or_array(
348 source: &str,
349 path: &Path,
350 array_path: &[&str],
351 inner_path: &[&str],
352) -> Vec<String> {
353 extract_from_source(source, path, |program| {
354 let obj = find_config_object(program)?;
355 let array_expr = get_nested_expression(obj, array_path)?;
356 let Expression::ArrayExpression(arr) = array_expr else {
357 return None;
358 };
359 let mut results = Vec::new();
360 for element in &arr.elements {
361 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
362 && let Some(values) =
363 get_nested_string_or_array(element_obj, inner_path, PathScope::Program(program))
364 {
365 results.extend(values);
366 }
367 }
368 if results.is_empty() {
369 None
370 } else {
371 Some(results)
372 }
373 })
374 .unwrap_or_default()
375}
376
377#[must_use]
384pub(crate) fn extract_config_array_object_fields(
385 source: &str,
386 path: &Path,
387 array_path: &[&str],
388 keys: &[&str],
389) -> Vec<Vec<Vec<String>>> {
390 extract_from_source(source, path, |program| {
391 let obj = find_config_object(program)?;
392 let Expression::ArrayExpression(arr) = get_nested_expression(obj, array_path)? else {
393 return None;
394 };
395 let elements = arr
396 .elements
397 .iter()
398 .filter_map(|element| match element.as_expression() {
399 Some(Expression::ObjectExpression(element_obj)) => Some(
400 keys.iter()
401 .map(|key| {
402 get_nested_string_or_array(
403 element_obj,
404 &[key],
405 PathScope::Program(program),
406 )
407 .unwrap_or_default()
408 })
409 .collect(),
410 ),
411 _ => None,
412 })
413 .collect();
414 Some(elements)
415 })
416 .unwrap_or_default()
417}
418
419#[must_use]
421pub(crate) fn extract_config_object_nested_string_or_array(
422 source: &str,
423 path: &Path,
424 object_path: &[&str],
425 inner_path: &[&str],
426) -> Vec<String> {
427 extract_config_object_nested(source, path, object_path, |value_obj| {
428 get_nested_string_or_array(value_obj, inner_path, PathScope::Bare)
429 })
430}
431
432#[must_use]
434pub(crate) fn extract_config_object_nested_strings(
435 source: &str,
436 path: &Path,
437 object_path: &[&str],
438 inner_path: &[&str],
439) -> Vec<String> {
440 extract_config_object_nested(source, path, object_path, |value_obj| {
441 get_nested_string_from_object(value_obj, inner_path).map(|s| vec![s])
442 })
443}
444
445fn extract_config_object_nested(
447 source: &str,
448 path: &Path,
449 object_path: &[&str],
450 extract_fn: impl Fn(&ObjectExpression<'_>) -> Option<Vec<String>> + Sync,
451) -> Vec<String> {
452 extract_from_source(source, path, |program| {
453 let obj = find_config_object(program)?;
454 let obj_expr = get_nested_expression(obj, object_path)?;
455 let Expression::ObjectExpression(target_obj) = obj_expr else {
456 return None;
457 };
458 let mut results = Vec::new();
459 for prop in &target_obj.properties {
460 if let ObjectPropertyKind::ObjectProperty(p) = prop
461 && let Expression::ObjectExpression(value_obj) = &p.value
462 && let Some(values) = extract_fn(value_obj)
463 {
464 results.extend(values);
465 }
466 }
467 if results.is_empty() {
468 None
469 } else {
470 Some(results)
471 }
472 })
473 .unwrap_or_default()
474}
475
476#[must_use]
478pub(crate) fn extract_config_require_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
479 extract_from_source(source, path, |program| {
480 let obj = find_config_object(program)?;
481 let prop = find_property(obj, key)?;
482 Some(collect_require_sources(&prop.value))
483 })
484 .unwrap_or_default()
485}
486
487#[must_use]
489pub(crate) fn extract_config_aliases(
490 source: &str,
491 path: &Path,
492 prop_path: &[&str],
493) -> Vec<(String, String)> {
494 extract_config_aliases_kinded(source, path, prop_path)
495 .into_iter()
496 .map(|(find, replacement, _is_bare)| (find, replacement))
497 .collect()
498}
499
500#[must_use]
502pub(crate) fn extract_config_path_aliases(
503 source: &str,
504 path: &Path,
505 prop_path: &[&str],
506) -> Vec<(String, PathBuf)> {
507 extract_config_aliases_kinded(source, path, prop_path)
508 .into_iter()
509 .map(|(find, replacement, _is_bare)| (find, path_from_config_string(&replacement)))
510 .collect()
511}
512
513#[must_use]
515pub(crate) fn extract_config_aliases_kinded(
516 source: &str,
517 path: &Path,
518 prop_path: &[&str],
519) -> Vec<(String, String, bool)> {
520 extract_from_source(source, path, |program| {
521 let obj = find_config_object(program)?;
522 let expr = get_nested_expression(obj, prop_path)?;
523 let mut visited = FxHashSet::default();
524 let aliases = resolve_alias_pairs_kinded(program, path, expr, &mut visited, 0);
525 (!aliases.is_empty()).then_some(aliases)
526 })
527 .unwrap_or_default()
528}
529
530#[must_use]
534pub(crate) fn extract_config_array_nested_aliases_kinded(
535 source: &str,
536 path: &Path,
537 array_path: &[&str],
538 alias_path: &[&str],
539) -> Vec<(String, String, bool)> {
540 extract_from_source(source, path, |program| {
541 let obj = find_config_object(program)?;
542 let array_expr = get_nested_expression(obj, array_path)?;
543 let Expression::ArrayExpression(arr) = array_expr else {
544 return None;
545 };
546 let mut results = Vec::new();
547 for element in &arr.elements {
548 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
549 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
550 {
551 results.extend(expression_to_alias_pairs_kinded(alias_expr));
552 }
553 }
554 (!results.is_empty()).then_some(results)
555 })
556 .unwrap_or_default()
557}
558
559#[must_use]
561pub(crate) fn extract_default_export_array_aliases_kinded(
562 source: &str,
563 path: &Path,
564 alias_path: &[&str],
565) -> Vec<(String, String, bool)> {
566 extract_from_source(source, path, |program| {
567 let arr = find_default_export_array(program)?;
568 let mut results = Vec::new();
569 for element in &arr.elements {
570 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
571 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
572 {
573 results.extend(expression_to_alias_pairs_kinded(alias_expr));
574 }
575 }
576 (!results.is_empty()).then_some(results)
577 })
578 .unwrap_or_default()
579}
580
581#[must_use]
583pub(crate) fn config_default_export_unreachable(source: &str, path: &Path) -> bool {
584 extract_from_source(source, path, |program| {
585 let reachable =
586 find_config_object(program).is_some() || find_default_export_array(program).is_some();
587 Some(reachable)
588 })
589 .is_some_and(|reachable| !reachable)
590}
591
592#[must_use]
598pub(crate) fn extract_config_array_object_strings(
599 source: &str,
600 path: &Path,
601 array_path: &[&str],
602 key: &str,
603) -> Vec<String> {
604 extract_from_source(source, path, |program| {
605 let obj = find_config_object(program)?;
606 let array_expr = get_nested_expression(obj, array_path)?;
607 let Expression::ArrayExpression(arr) = array_expr else {
608 return None;
609 };
610
611 let mut results = Vec::new();
612 for element in &arr.elements {
613 let Some(expr) = element.as_expression() else {
614 continue;
615 };
616 match expr {
617 Expression::ObjectExpression(item) => {
618 if let Some(prop) = find_property(item, key)
619 && let Some(value) = expression_to_path_string(&prop.value)
620 {
621 results.push(value);
622 }
623 }
624 _ => {
625 if let Some(value) = expression_to_path_string(expr) {
626 results.push(value);
627 }
628 }
629 }
630 }
631
632 (!results.is_empty()).then_some(results)
633 })
634 .unwrap_or_default()
635}
636
637#[must_use]
642pub(crate) fn extract_config_static_dir_entries(
643 source: &str,
644 path: &Path,
645 array_path: &[&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(expr) = element.as_expression() else {
657 continue;
658 };
659 match expr {
660 Expression::ObjectExpression(item) => {
661 if let Some(from) = property_string(item, "from") {
662 let to = property_string(item, "to");
663 results.push((from, to));
664 }
665 }
666 _ => {
667 if let Some(from) = expression_to_path_string(expr) {
668 results.push((from, None));
669 }
670 }
671 }
672 }
673
674 (!results.is_empty()).then_some(results)
675 })
676 .unwrap_or_default()
677}
678
679#[must_use]
681pub(crate) fn extract_config_array_object_command_pairs(
682 source: &str,
683 path: &Path,
684 array_path: &[&str],
685 primary_key: &str,
686 secondary_key: &str,
687) -> Vec<(String, Option<String>)> {
688 extract_from_source(source, path, |program| {
689 let obj = find_config_object(program)?;
690 let array_expr = get_nested_expression(obj, array_path)?;
691 let Expression::ArrayExpression(arr) = array_expr else {
692 return None;
693 };
694
695 let mut results = Vec::new();
696 for element in &arr.elements {
697 let Some(Expression::ObjectExpression(item)) = element.as_expression() else {
698 continue;
699 };
700 let Some(primary) = find_property(item, primary_key)
701 .and_then(|prop| expression_to_command(&prop.value))
702 else {
703 continue;
704 };
705 let secondary = find_property(item, secondary_key)
706 .and_then(|prop| expression_to_path_string(&prop.value));
707 results.push((primary, secondary));
708 }
709
710 (!results.is_empty()).then_some(results)
711 })
712 .unwrap_or_default()
713}
714
715#[must_use]
761pub(crate) fn extract_lazy_imports_in_array(
762 source: &str,
763 path: &Path,
764 prop_path: &[&str],
765) -> Vec<String> {
766 extract_from_source(source, path, |program| {
767 let obj = find_config_object(program)?;
768 let array_expr = get_nested_expression(obj, prop_path)?;
769 let Expression::ArrayExpression(arr) = array_expr else {
770 return None;
771 };
772 let mut specs = Vec::new();
773 for element in &arr.elements {
774 let Some(expr) = element.as_expression() else {
775 continue;
776 };
777 if let Some(spec) = lazy_import_specifier(expr) {
778 specs.push(spec);
779 }
780 }
781 (!specs.is_empty()).then_some(specs)
782 })
783 .unwrap_or_default()
784}
785
786fn lazy_import_specifier(expr: &Expression<'_>) -> Option<String> {
799 let callable = match expr {
800 Expression::ObjectExpression(obj) => &find_property(obj, "file")?.value,
801 _ => expr,
802 };
803 let import_expr = extract_import_from_callable(callable)?;
804 expression_to_string(&import_expr.source)
805}
806
807#[must_use]
812pub(crate) fn extract_config_array_tuple_heads(
813 source: &str,
814 path: &Path,
815 array_path: &[&str],
816) -> Vec<String> {
817 extract_from_source(source, path, |program| {
818 let obj = find_config_object(program)?;
819 let Expression::ArrayExpression(entries) = get_nested_expression(obj, array_path)? else {
820 return None;
821 };
822 let heads = entries
823 .elements
824 .iter()
825 .filter_map(|entry| match entry.as_expression() {
826 Some(Expression::ArrayExpression(tuple)) => tuple
827 .elements
828 .first()
829 .and_then(ArrayExpressionElement::as_expression)
830 .and_then(expression_to_string),
831 _ => None,
832 })
833 .collect();
834 Some(heads)
835 })
836 .unwrap_or_default()
837}
838
839#[must_use]
846fn extract_config_plugin_option_string(
847 source: &str,
848 path: &Path,
849 plugins_path: &[&str],
850 plugin_name: &str,
851 option_key: &str,
852) -> Option<String> {
853 extract_from_source(source, path, |program| {
854 let obj = find_config_object(program)?;
855 let plugins_expr = get_nested_expression(obj, plugins_path)?;
856 let Expression::ArrayExpression(plugins) = plugins_expr else {
857 return None;
858 };
859
860 for entry in &plugins.elements {
861 let Some(Expression::ArrayExpression(tuple)) = entry.as_expression() else {
862 continue;
863 };
864 let Some(plugin_expr) = tuple
865 .elements
866 .first()
867 .and_then(ArrayExpressionElement::as_expression)
868 else {
869 continue;
870 };
871 if expression_to_string(plugin_expr).as_deref() != Some(plugin_name) {
872 continue;
873 }
874
875 let Some(options_expr) = tuple
876 .elements
877 .get(1)
878 .and_then(ArrayExpressionElement::as_expression)
879 else {
880 continue;
881 };
882 let Expression::ObjectExpression(options_obj) = options_expr else {
883 continue;
884 };
885 let option = find_property(options_obj, option_key)?;
886 return expression_to_path_string(&option.value);
887 }
888
889 None
890 })
891}
892
893#[must_use]
895pub(crate) fn extract_config_plugin_option_string_from_paths(
896 source: &str,
897 path: &Path,
898 plugin_paths: &[&[&str]],
899 plugin_name: &str,
900 option_key: &str,
901) -> Option<String> {
902 plugin_paths.iter().find_map(|plugins_path| {
903 extract_config_plugin_option_string(source, path, plugins_path, plugin_name, option_key)
904 })
905}
906
907struct VitePluginOptionReader {
909 package: &'static str,
910 read: fn(&ObjectExpression<'_>, &mut Vec<String>),
911}
912
913const VITE_PLUGIN_OPTION_READERS: &[VitePluginOptionReader] = &[
917 VitePluginOptionReader {
918 package: "@vitejs/plugin-react",
919 read: collect_vite_react_babel_dependencies,
920 },
921 VitePluginOptionReader {
922 package: "@vitejs/plugin-react-swc",
923 read: collect_vite_react_swc_dependencies,
924 },
925 VitePluginOptionReader {
926 package: "@wyw-in-js/vite",
927 read: collect_wyw_babel_options_dependencies,
928 },
929];
930
931const MAX_VITE_PLUGIN_WRAPPER_DEPTH: usize = 3;
934
935#[must_use]
940pub(crate) fn extract_vite_plugin_option_dependencies(source: &str, path: &Path) -> Vec<String> {
941 extract_from_source(source, path, |program| {
942 let bindings: Vec<(PluginImportBindings, &VitePluginOptionReader)> =
943 VITE_PLUGIN_OPTION_READERS
944 .iter()
945 .map(|reader| {
946 (
947 collect_plugin_import_bindings(program, reader.package),
948 reader,
949 )
950 })
951 .filter(|(imports, _)| !imports.is_empty())
952 .collect();
953 if bindings.is_empty() {
954 return None;
955 }
956
957 let obj = find_config_object(program)?;
958 let plugins = get_nested_expression(obj, &["plugins"])?;
959 let Expression::ArrayExpression(plugin_array) = plugins else {
960 return None;
961 };
962
963 let mut deps = Vec::new();
964 for element in &plugin_array.elements {
965 if let Some(Expression::CallExpression(call)) = element.as_expression() {
966 read_vite_plugin_call(call, &bindings, 0, &mut deps);
967 }
968 }
969
970 (!deps.is_empty()).then_some(deps)
971 })
972 .unwrap_or_default()
973}
974
975fn read_vite_plugin_call(
976 call: &CallExpression<'_>,
977 bindings: &[(PluginImportBindings, &VitePluginOptionReader)],
978 depth: usize,
979 deps: &mut Vec<String>,
980) {
981 if let Some((_, reader)) = bindings
982 .iter()
983 .find(|(imports, _)| is_plugin_binding_call(call, imports))
984 {
985 if let Some(Expression::ObjectExpression(options)) =
986 call.arguments.first().and_then(Argument::as_expression)
987 {
988 (reader.read)(options, deps);
989 }
990 return;
991 }
992 if depth >= MAX_VITE_PLUGIN_WRAPPER_DEPTH {
993 return;
994 }
995 for argument in &call.arguments {
996 if let Some(Expression::CallExpression(inner)) = argument.as_expression() {
997 read_vite_plugin_call(inner, bindings, depth + 1, deps);
998 }
999 }
1000}
1001
1002#[must_use]
1006pub(crate) fn config_string_package_name(raw: &str) -> Option<String> {
1007 let specifier = raw.trim();
1008 is_package_specifier(specifier).then(|| crate::resolve::extract_package_name(specifier))
1009}
1010
1011#[must_use]
1014pub(crate) fn extract_config_swc_plugin_dependencies(
1015 source: &str,
1016 path: &Path,
1017 array_path: &[&str],
1018) -> Vec<String> {
1019 extract_config_array_tuple_heads(source, path, array_path)
1020 .iter()
1021 .filter_map(|raw| config_string_package_name(raw))
1022 .fold(Vec::new(), |mut deps, dep| {
1023 push_unique_string(&mut deps, dep);
1024 deps
1025 })
1026}
1027
1028#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1030enum LeadingSlash {
1031 RootRelative,
1033 Filesystem,
1035}
1036
1037#[must_use]
1046pub(crate) fn normalize_config_path_buf(
1047 raw: impl AsRef<Path>,
1048 config_path: &Path,
1049 root: &Path,
1050) -> Option<PathBuf> {
1051 normalize_path_value(raw.as_ref(), config_path, root, LeadingSlash::RootRelative)
1052}
1053
1054#[must_use]
1058pub(crate) fn normalize_filesystem_config_path_buf(
1059 raw: impl AsRef<Path>,
1060 config_path: &Path,
1061 root: &Path,
1062) -> Option<PathBuf> {
1063 normalize_path_value(raw.as_ref(), config_path, root, LeadingSlash::Filesystem)
1064}
1065
1066fn normalize_path_value(
1067 raw: &Path,
1068 config_path: &Path,
1069 root: &Path,
1070 leading_slash: LeadingSlash,
1071) -> Option<PathBuf> {
1072 if raw.as_os_str().is_empty() {
1073 return None;
1074 }
1075
1076 let raw_string = path_to_config_string(raw);
1077 let raw_path = Path::new(&raw_string);
1078 let root_relative_slash =
1079 leading_slash == LeadingSlash::RootRelative && raw_string.starts_with('/');
1080 let absolute = raw_path
1081 .is_absolute()
1082 .then(|| lexical_normalize(raw_path))
1083 .filter(|absolute| absolute.starts_with(root))
1084 .filter(|absolute| !(root_relative_slash && (absolute == root || root_in_root(root))));
1085 let candidate = if let Some(absolute) = absolute {
1086 absolute
1087 } else if let Some(stripped) = raw_string.strip_prefix('/') {
1088 match leading_slash {
1089 LeadingSlash::RootRelative => lexical_normalize(&root.join(stripped)),
1090 LeadingSlash::Filesystem => return None,
1091 }
1092 } else if raw_path.is_absolute() {
1093 return None;
1094 } else {
1095 let base = config_path.parent().unwrap_or(root);
1096 lexical_normalize(&base.join(raw_path))
1097 };
1098
1099 let relative = candidate.strip_prefix(root).ok()?;
1100 (!relative.as_os_str().is_empty()).then(|| relative.to_path_buf())
1101}
1102
1103fn root_in_root(root: &Path) -> bool {
1107 let root_string = path_to_config_string(root);
1108 let Some(stripped) = root_string.strip_prefix('/') else {
1109 return false;
1110 };
1111 !stripped.is_empty() && root.join(stripped).is_dir()
1112}
1113
1114#[must_use]
1117pub(crate) fn normalize_filesystem_config_path(
1118 raw: impl AsRef<Path>,
1119 config_path: &Path,
1120 root: &Path,
1121) -> Option<String> {
1122 normalize_filesystem_config_path_buf(raw, config_path, root)
1123 .map(|path| path_to_config_string(&path))
1124}
1125
1126#[must_use]
1128pub(crate) fn normalize_config_path(
1129 raw: impl AsRef<Path>,
1130 config_path: &Path,
1131 root: &Path,
1132) -> Option<String> {
1133 normalize_config_path_buf(raw, config_path, root).map(|path| path_to_config_string(&path))
1134}
1135
1136pub(crate) fn parent_relative_config_path(
1146 target: &str,
1147 config_path: &Path,
1148 root: &Path,
1149) -> Option<String> {
1150 if !target.starts_with("../") {
1151 return None;
1152 }
1153 let directory = config_path.parent().unwrap_or(root);
1154 let candidate = lexical_normalize(&directory.join(target));
1155 let root = lexical_normalize(root);
1156 let mut ancestor = root.as_path();
1157 let mut climbs = 0;
1158 while !candidate.starts_with(ancestor) {
1159 ancestor = ancestor.parent()?;
1160 climbs += 1;
1161 }
1162 let rest = candidate.strip_prefix(ancestor).ok()?;
1163 (climbs > 0 && !rest.as_os_str().is_empty())
1164 .then(|| format!("{}{}", "../".repeat(climbs), path_to_config_string(rest)))
1165}
1166
1167pub(crate) fn extract_from_source<T: Send>(
1178 source: &str,
1179 path: &Path,
1180 extractor: impl FnOnce(&Program) -> Option<T> + Send,
1181) -> Option<T> {
1182 let is_json = path
1183 .extension()
1184 .is_some_and(|ext| ext == "json" || ext == "jsonc");
1185 if is_json {
1186 let wrapped = wrap_json_as_expression(source);
1187 return fallow_extract::parse_bounded_or_default(&wrapped, SourceType::mjs(), |parsed| {
1188 extractor(&parsed.program)
1189 });
1190 }
1191 let source_type = SourceType::from_path(path).unwrap_or_default();
1192 fallow_extract::parse_bounded(source, || {
1193 let allocator = oxc_allocator::Allocator::default();
1194 let mut parsed = oxc_parser::Parser::new(&allocator, source, source_type).parse();
1195 const_inline::inline_value_constants(&allocator, &mut parsed.program, source.len());
1196 extractor(&parsed.program)
1197 })
1198 .flatten()
1199}
1200
1201pub(crate) fn wrap_json_as_expression(source: &str) -> String {
1205 format!("({source}\n)")
1206}
1207
1208#[derive(Default)]
1211struct PluginImportBindings {
1212 callables: Vec<String>,
1213 namespaces: Vec<String>,
1214}
1215
1216impl PluginImportBindings {
1217 fn is_empty(&self) -> bool {
1218 self.callables.is_empty() && self.namespaces.is_empty()
1219 }
1220}
1221
1222fn collect_plugin_import_bindings(program: &Program<'_>, package: &str) -> PluginImportBindings {
1223 let mut imports = PluginImportBindings::default();
1224
1225 for stmt in &program.body {
1226 let Statement::ImportDeclaration(decl) = stmt else {
1227 continue;
1228 };
1229 if decl.source.value != package {
1230 continue;
1231 }
1232 let Some(specifiers) = &decl.specifiers else {
1233 continue;
1234 };
1235 for specifier in specifiers {
1236 match specifier {
1237 ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
1238 push_unique_string(&mut imports.callables, specifier.local.name.to_string());
1239 }
1240 ImportDeclarationSpecifier::ImportSpecifier(specifier)
1241 if specifier.imported.name().as_ref() == "default" =>
1242 {
1243 push_unique_string(&mut imports.callables, specifier.local.name.to_string());
1244 }
1245 ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
1246 push_unique_string(&mut imports.namespaces, specifier.local.name.to_string());
1247 }
1248 ImportDeclarationSpecifier::ImportSpecifier(_) => {}
1249 }
1250 }
1251 }
1252
1253 imports
1254}
1255
1256fn collect_vite_react_babel_dependencies(options: &ObjectExpression<'_>, deps: &mut Vec<String>) {
1257 if let Some(babel) = property_object(options, "babel") {
1258 collect_babel_plugin_and_preset_dependencies(babel, deps);
1259 }
1260}
1261
1262fn collect_wyw_babel_options_dependencies(options: &ObjectExpression<'_>, deps: &mut Vec<String>) {
1265 if let Some(babel) = property_object(options, "babelOptions") {
1266 collect_babel_plugin_and_preset_dependencies(babel, deps);
1267 }
1268}
1269
1270fn collect_vite_react_swc_dependencies(options: &ObjectExpression<'_>, deps: &mut Vec<String>) {
1273 let Some(prop) = find_property(options, "plugins") else {
1274 return;
1275 };
1276 let Expression::ArrayExpression(plugins) = &prop.value else {
1277 return;
1278 };
1279 for entry in &plugins.elements {
1280 let Some(Expression::ArrayExpression(tuple)) = entry.as_expression() else {
1281 continue;
1282 };
1283 if let Some(dep) = tuple
1284 .elements
1285 .first()
1286 .and_then(ArrayExpressionElement::as_expression)
1287 .and_then(expression_to_string)
1288 .and_then(|raw| config_string_package_name(&raw))
1289 {
1290 push_unique_string(deps, dep);
1291 }
1292 }
1293}
1294
1295fn collect_babel_plugin_and_preset_dependencies(
1296 babel: &ObjectExpression<'_>,
1297 deps: &mut Vec<String>,
1298) {
1299 for key in ["plugins", "presets"] {
1300 let Some(prop) = find_property(babel, key) else {
1301 continue;
1302 };
1303 for raw in collect_shallow_string_values(&prop.value) {
1304 if let Some(dep) = vite_react_babel_dependency_name(&raw) {
1305 push_unique_string(deps, dep);
1306 }
1307 }
1308 }
1309}
1310
1311fn vite_react_babel_dependency_name(raw: &str) -> Option<String> {
1312 let raw = raw.trim();
1313 let specifier = raw.strip_prefix("module:").unwrap_or(raw).trim();
1314 if specifier.is_empty()
1315 || specifier.starts_with('.')
1316 || specifier.starts_with('/')
1317 || specifier.contains(':')
1318 || specifier.contains('\\')
1319 {
1320 return None;
1321 }
1322 Some(crate::resolve::extract_package_name(specifier))
1323}
1324
1325fn push_unique_string(items: &mut Vec<String>, value: String) {
1326 if !items.contains(&value) {
1327 items.push(value);
1328 }
1329}
1330
1331pub(crate) fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
1343 for stmt in &program.body {
1344 match stmt {
1345 Statement::ExportDefaultDeclaration(decl) => {
1346 let expr: Option<&Expression> = match &decl.declaration {
1347 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
1348 return Some(obj);
1349 }
1350 ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
1351 return extract_object_from_function(func);
1352 }
1353 _ => decl.declaration.as_expression(),
1354 };
1355 if let Some(expr) = expr {
1356 if let Some(obj) =
1357 resolve_call_config_object(program, expr, MAX_CONFIG_WRAPPER_DEPTH)
1358 {
1359 return Some(obj);
1360 }
1361 if let Some(obj) = extract_object_from_expression(expr) {
1362 return Some(obj);
1363 }
1364 if let Some(name) = unwrap_to_identifier_name(expr) {
1365 return find_variable_init_object(program, name);
1366 }
1367 if let Some(obj) = resolve_wrapped_config_object(program, expr) {
1368 return Some(obj);
1369 }
1370 }
1371 }
1372 Statement::ExpressionStatement(expr_stmt) => {
1373 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
1374 && is_module_exports_target(&assign.left)
1375 {
1376 if let Some(obj) =
1377 resolve_call_config_object(program, &assign.right, MAX_CONFIG_WRAPPER_DEPTH)
1378 {
1379 return Some(obj);
1380 }
1381 if let Some(obj) = extract_object_from_expression(&assign.right) {
1382 return Some(obj);
1383 }
1384 if let Some(name) = unwrap_to_identifier_name(&assign.right) {
1385 return find_variable_init_object(program, name);
1386 }
1387 return resolve_wrapped_config_object(program, &assign.right);
1388 }
1389 }
1390 _ => {}
1391 }
1392 }
1393
1394 if program.body.len() == 1
1395 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
1396 {
1397 match &expr_stmt.expression {
1398 Expression::ObjectExpression(obj) => return Some(obj),
1399 Expression::ParenthesizedExpression(paren) => {
1400 if let Expression::ObjectExpression(obj) = &paren.expression {
1401 return Some(obj);
1402 }
1403 }
1404 _ => {}
1405 }
1406 }
1407
1408 None
1409}
1410
1411pub(crate) fn extract_object_from_expression<'a>(
1413 expr: &'a Expression<'a>,
1414) -> Option<&'a ObjectExpression<'a>> {
1415 match expr {
1416 Expression::ObjectExpression(obj) => Some(obj),
1417 Expression::CallExpression(call) => {
1418 for arg in &call.arguments {
1419 match arg {
1420 Argument::ObjectExpression(obj) => return Some(obj),
1421 Argument::ArrowFunctionExpression(arrow) => {
1428 if let Some(obj) = extract_object_from_arrow_function(arrow) {
1429 return Some(obj);
1430 }
1431 }
1432 Argument::FunctionExpression(func) => {
1433 if let Some(obj) = extract_object_from_function(func) {
1434 return Some(obj);
1435 }
1436 }
1437 _ => {}
1438 }
1439 }
1440 None
1441 }
1442 Expression::ParenthesizedExpression(paren) => {
1443 extract_object_from_expression(&paren.expression)
1444 }
1445 Expression::TSSatisfiesExpression(ts_sat) => {
1446 extract_object_from_expression(&ts_sat.expression)
1447 }
1448 Expression::TSAsExpression(ts_as) => extract_object_from_expression(&ts_as.expression),
1449 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
1450 Expression::FunctionExpression(func) => extract_object_from_function(func),
1451 _ => None,
1452 }
1453}
1454
1455fn extract_object_from_arrow_function<'a>(
1456 arrow: &'a ArrowFunctionExpression<'a>,
1457) -> Option<&'a ObjectExpression<'a>> {
1458 match &arrow.body {
1459 ArrowFunctionBody::FunctionBody(body) => extract_object_from_function_body(body),
1460 body => body
1461 .as_expression()
1462 .and_then(extract_object_from_expression),
1463 }
1464}
1465
1466fn extract_object_from_function<'a>(func: &'a Function<'a>) -> Option<&'a ObjectExpression<'a>> {
1467 func.body
1468 .as_ref()
1469 .and_then(|body| extract_object_from_function_body(body))
1470}
1471
1472fn extract_object_from_function_body<'a>(
1481 body: &'a FunctionBody<'a>,
1482) -> Option<&'a ObjectExpression<'a>> {
1483 find_top_level_returned_object(&body.statements)
1484 .or_else(|| find_returned_object(&body.statements, MAX_CONFIG_BRANCH_DEPTH))
1485}
1486
1487const MAX_CONFIG_BRANCH_DEPTH: u8 = 4;
1489
1490fn find_top_level_returned_object<'a>(
1492 statements: &'a [Statement<'a>],
1493) -> Option<&'a ObjectExpression<'a>> {
1494 statements.iter().find_map(|stmt| match stmt {
1495 Statement::ReturnStatement(ret) => ret
1496 .argument
1497 .as_ref()
1498 .and_then(|argument| extract_object_from_expression(argument)),
1499 _ => None,
1500 })
1501}
1502
1503fn find_returned_object<'a>(
1512 statements: &'a [Statement<'a>],
1513 depth: u8,
1514) -> Option<&'a ObjectExpression<'a>> {
1515 if depth == 0 {
1516 return None;
1517 }
1518 for stmt in statements {
1519 let found = match stmt {
1520 Statement::ReturnStatement(ret) => ret
1521 .argument
1522 .as_ref()
1523 .and_then(|argument| extract_object_from_expression(argument)),
1524 Statement::BlockStatement(block) => find_returned_object(&block.body, depth - 1),
1525 Statement::IfStatement(if_stmt) => {
1526 find_returned_object_in_statement(&if_stmt.consequent, depth - 1).or_else(|| {
1527 if_stmt
1528 .alternate
1529 .as_ref()
1530 .and_then(|alt| find_returned_object_in_statement(alt, depth - 1))
1531 })
1532 }
1533 Statement::TryStatement(try_stmt) => {
1534 find_returned_object(&try_stmt.block.body, depth - 1)
1535 .or_else(|| {
1536 try_stmt
1537 .handler
1538 .as_ref()
1539 .and_then(|handler| find_returned_object(&handler.body.body, depth - 1))
1540 })
1541 .or_else(|| {
1542 try_stmt
1543 .finalizer
1544 .as_ref()
1545 .and_then(|finalizer| find_returned_object(&finalizer.body, depth - 1))
1546 })
1547 }
1548 Statement::SwitchStatement(switch) => switch
1549 .cases
1550 .iter()
1551 .find_map(|case| find_returned_object(&case.consequent, depth - 1)),
1552 _ => None,
1553 };
1554 if found.is_some() {
1555 return found;
1556 }
1557 }
1558 None
1559}
1560
1561fn find_returned_object_in_statement<'a>(
1562 stmt: &'a Statement<'a>,
1563 depth: u8,
1564) -> Option<&'a ObjectExpression<'a>> {
1565 match stmt {
1566 Statement::BlockStatement(block) => find_returned_object(&block.body, depth),
1567 other => find_returned_object(std::slice::from_ref(other), depth),
1568 }
1569}
1570
1571fn is_module_exports_target(target: &AssignmentTarget) -> bool {
1573 if let AssignmentTarget::StaticMemberExpression(member) = target
1574 && let Expression::Identifier(obj) = &member.object
1575 {
1576 return obj.name == "module" && member.property.name == "exports";
1577 }
1578 false
1579}
1580
1581fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
1585 match expr {
1586 Expression::Identifier(id) => Some(&id.name),
1587 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
1588 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
1589 _ => None,
1590 }
1591}
1592
1593pub(crate) fn find_variable_init_object<'a>(
1598 program: &'a Program,
1599 name: &str,
1600) -> Option<&'a ObjectExpression<'a>> {
1601 for decl in top_level_variable_declarations(program) {
1602 for declarator in &decl.declarations {
1603 if let BindingPattern::BindingIdentifier(id) = &declarator.id
1604 && id.name == name
1605 && let Some(init) = &declarator.init
1606 {
1607 return extract_object_from_expression(init);
1608 }
1609 }
1610 }
1611 None
1612}
1613
1614fn find_variable_init<'a>(program: &'a Program<'a>, name: &str) -> Option<&'a Expression<'a>> {
1616 top_level_variable_declarations(program)
1617 .flat_map(|decl| &decl.declarations)
1618 .find(|declarator| {
1619 matches!(&declarator.id, BindingPattern::BindingIdentifier(id) if id.name == name)
1620 })
1621 .and_then(|declarator| declarator.init.as_ref())
1622}
1623
1624fn exported_config_expression<'a>(program: &'a Program<'a>) -> Option<&'a Expression<'a>> {
1627 program.body.iter().find_map(|stmt| match stmt {
1628 Statement::ExportDefaultDeclaration(decl) => decl.declaration.as_expression(),
1629 Statement::ExpressionStatement(expr_stmt) => match &expr_stmt.expression {
1630 Expression::AssignmentExpression(assign) if is_module_exports_target(&assign.left) => {
1631 Some(&assign.right)
1632 }
1633 _ => None,
1634 },
1635 _ => None,
1636 })
1637}
1638
1639fn config_call<'a>(
1642 program: &'a Program<'a>,
1643 expr: &'a Expression<'a>,
1644 follow_binding: bool,
1645) -> Option<&'a CallExpression<'a>> {
1646 match expr {
1647 Expression::CallExpression(call) => Some(call),
1648 Expression::ParenthesizedExpression(paren) => {
1649 config_call(program, &paren.expression, follow_binding)
1650 }
1651 Expression::TSSatisfiesExpression(ts_sat) => {
1652 config_call(program, &ts_sat.expression, follow_binding)
1653 }
1654 Expression::TSAsExpression(ts_as) => {
1655 config_call(program, &ts_as.expression, follow_binding)
1656 }
1657 Expression::Identifier(id) if follow_binding => {
1658 config_call(program, find_variable_init(program, &id.name)?, false)
1659 }
1660 _ => None,
1661 }
1662}
1663
1664pub(crate) fn find_config_merge_arguments<'a>(
1673 program: &'a Program<'a>,
1674) -> Option<Vec<Option<&'a ObjectExpression<'a>>>> {
1675 let call = config_call(program, exported_config_expression(program)?, true)?;
1676 if call.arguments.len() < 2 {
1677 return None;
1678 }
1679 let resolve = |arg: &'a Argument<'a>| {
1680 let expr = arg.as_expression()?;
1681 if config_call(program, expr, true).is_some_and(|nested| nested.arguments.len() > 1) {
1682 return None;
1683 }
1684 resolve_config_argument(program, expr, MAX_CONFIG_WRAPPER_DEPTH)
1685 };
1686 Some(call.arguments.iter().map(resolve).collect())
1687}
1688
1689fn top_level_variable_declarations<'a>(
1692 program: &'a Program<'a>,
1693) -> impl Iterator<Item = &'a VariableDeclaration<'a>> {
1694 program.body.iter().filter_map(|stmt| match stmt {
1695 Statement::VariableDeclaration(decl) => Some(&**decl),
1696 Statement::ExportDeclaration(export) => match &export.declaration {
1697 Declaration::VariableDeclaration(decl) => Some(&**decl),
1698 _ => None,
1699 },
1700 _ => None,
1701 })
1702}
1703
1704pub(crate) fn find_stable_binding_init<'a>(
1715 program: &'a Program<'a>,
1716 name: &str,
1717 allow_mutating_calls: bool,
1718) -> Option<&'a Expression<'a>> {
1719 if !holds_one_stable_object(program, name, allow_mutating_calls) {
1720 return None;
1721 }
1722 find_variable_init_expression(program, name)
1723}
1724
1725fn resolve_wrapped_config_object<'a>(
1738 program: &'a Program,
1739 expr: &'a Expression<'a>,
1740) -> Option<&'a ObjectExpression<'a>> {
1741 let call = match expr {
1742 Expression::CallExpression(call) => call,
1743 Expression::ParenthesizedExpression(paren) => {
1744 return resolve_wrapped_config_object(program, &paren.expression);
1745 }
1746 Expression::TSSatisfiesExpression(ts_sat) => {
1747 return resolve_wrapped_config_object(program, &ts_sat.expression);
1748 }
1749 Expression::TSAsExpression(ts_as) => {
1750 return resolve_wrapped_config_object(program, &ts_as.expression);
1751 }
1752 _ => return None,
1753 };
1754 for arg in &call.arguments {
1755 let Some(arg_expr) = arg.as_expression() else {
1756 continue;
1757 };
1758 if let Some(name) = unwrap_to_identifier_name(arg_expr)
1759 && let Some(obj) = find_variable_init_object(program, name)
1760 {
1761 return Some(obj);
1762 }
1763 if let Some(obj) = resolve_wrapped_config_object(program, arg_expr) {
1764 return Some(obj);
1765 }
1766 }
1767 None
1768}
1769
1770const MAX_CONFIG_WRAPPER_DEPTH: u8 = 3;
1775
1776fn resolve_call_config_object<'a>(
1787 program: &'a Program,
1788 expr: &'a Expression<'a>,
1789 depth: u8,
1790) -> Option<&'a ObjectExpression<'a>> {
1791 if depth == 0 {
1792 return None;
1793 }
1794 let call = match expr {
1795 Expression::CallExpression(call) => call,
1796 Expression::ParenthesizedExpression(paren) => {
1797 return resolve_call_config_object(program, &paren.expression, depth);
1798 }
1799 Expression::TSSatisfiesExpression(ts_sat) => {
1800 return resolve_call_config_object(program, &ts_sat.expression, depth);
1801 }
1802 Expression::TSAsExpression(ts_as) => {
1803 return resolve_call_config_object(program, &ts_as.expression, depth);
1804 }
1805 _ => return None,
1806 };
1807
1808 call.arguments
1809 .iter()
1810 .filter_map(oxc_ast::ast::Argument::as_expression)
1811 .find_map(|arg| resolve_config_argument(program, arg, depth))
1812}
1813
1814fn resolve_config_argument<'a>(
1816 program: &'a Program,
1817 expr: &'a Expression<'a>,
1818 depth: u8,
1819) -> Option<&'a ObjectExpression<'a>> {
1820 match expr {
1821 Expression::ObjectExpression(obj) => Some(obj),
1822 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
1823 Expression::FunctionExpression(func) => extract_object_from_function(func),
1824 Expression::ParenthesizedExpression(paren) => {
1825 resolve_config_argument(program, &paren.expression, depth)
1826 }
1827 Expression::TSSatisfiesExpression(ts_sat) => {
1828 resolve_config_argument(program, &ts_sat.expression, depth)
1829 }
1830 Expression::TSAsExpression(ts_as) => {
1831 resolve_config_argument(program, &ts_as.expression, depth)
1832 }
1833 Expression::CallExpression(_) => resolve_call_config_object(program, expr, depth - 1),
1834 _ => unwrap_to_identifier_name(expr)
1835 .and_then(|name| find_variable_init_object(program, name)),
1836 }
1837}
1838
1839pub(crate) fn find_property<'a>(
1841 obj: &'a ObjectExpression<'a>,
1842 key: &str,
1843) -> Option<&'a ObjectProperty<'a>> {
1844 for prop in &obj.properties {
1845 if let ObjectPropertyKind::ObjectProperty(p) = prop
1846 && property_key_matches(&p.key, key)
1847 {
1848 return Some(p);
1849 }
1850 }
1851 None
1852}
1853
1854fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
1856 match key {
1857 PropertyKey::StaticIdentifier(id) => id.name == name,
1858 PropertyKey::StringLiteral(s) => s.value == name,
1859 _ => false,
1860 }
1861}
1862
1863fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
1865 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
1866}
1867
1868fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
1870 find_property(obj, key)
1871 .map(|p| expression_to_string_array(&p.value))
1872 .unwrap_or_default()
1873}
1874
1875fn get_nested_string_array_from_object(
1877 obj: &ObjectExpression,
1878 path: &[&str],
1879) -> Option<Vec<String>> {
1880 if path.is_empty() {
1881 return None;
1882 }
1883 if path.len() == 1 {
1884 return Some(get_object_string_array_property(obj, path[0]));
1885 }
1886 let prop = find_property(obj, path[0])?;
1887 if let Expression::ObjectExpression(nested) = &prop.value {
1888 get_nested_string_array_from_object(nested, &path[1..])
1889 } else {
1890 None
1891 }
1892}
1893
1894fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
1896 if path.is_empty() {
1897 return None;
1898 }
1899 if path.len() == 1 {
1900 return get_object_string_property(obj, path[0]);
1901 }
1902 let prop = find_property(obj, path[0])?;
1903 if let Expression::ObjectExpression(nested) = &prop.value {
1904 get_nested_string_from_object(nested, &path[1..])
1905 } else {
1906 None
1907 }
1908}
1909
1910fn get_nested_command_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
1912 if path.is_empty() {
1913 return None;
1914 }
1915 if path.len() == 1 {
1916 return find_property(obj, path[0]).and_then(|prop| expression_to_command(&prop.value));
1917 }
1918 let prop = find_property(obj, path[0])?;
1919 if let Expression::ObjectExpression(nested) = &prop.value {
1920 get_nested_command_from_object(nested, &path[1..])
1921 } else {
1922 None
1923 }
1924}
1925
1926pub(crate) fn expression_to_string(expr: &Expression) -> Option<String> {
1928 match expr {
1929 Expression::StringLiteral(s) => Some(s.value.text().into_owned()),
1930 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
1931 t.quasis.first().map(|q| q.value.raw.to_string())
1932 }
1933 _ => None,
1934 }
1935}
1936
1937fn expression_to_command(expr: &Expression) -> Option<String> {
1939 match expr {
1940 Expression::StringLiteral(s) => Some(s.value.text().into_owned()),
1941 Expression::TemplateLiteral(template) => template_literal_to_command(template),
1942 Expression::ParenthesizedExpression(paren) => expression_to_command(&paren.expression),
1943 Expression::TSAsExpression(ts_as) => expression_to_command(&ts_as.expression),
1944 Expression::TSSatisfiesExpression(ts_sat) => expression_to_command(&ts_sat.expression),
1945 _ => None,
1946 }
1947}
1948
1949fn template_literal_to_command(template: &TemplateLiteral<'_>) -> Option<String> {
1950 let first = template.quasis.first()?.value.raw.as_str();
1951 if first.trim_start().is_empty() {
1952 return None;
1953 }
1954
1955 let mut command = String::new();
1956 for (idx, quasi) in template.quasis.iter().enumerate() {
1957 command.push_str(quasi.value.raw.as_str());
1958 if idx < template.expressions.len() {
1959 let next = template
1960 .quasis
1961 .get(idx + 1)
1962 .map_or("", |next| next.value.raw.as_str());
1963 if dynamic_template_boundary_splits_static_token(quasi.value.raw.as_str(), next) {
1964 return None;
1965 }
1966 command.push(' ');
1967 }
1968 }
1969
1970 Some(command)
1971}
1972
1973fn dynamic_template_boundary_splits_static_token(before: &str, after: &str) -> bool {
1974 before
1975 .chars()
1976 .next_back()
1977 .is_some_and(is_command_token_char)
1978 && after.chars().next().is_some_and(is_command_token_char)
1979}
1980
1981fn is_command_token_char(ch: char) -> bool {
1982 !ch.is_whitespace() && !matches!(ch, '&' | '|' | ';' | '"' | '\'')
1983}
1984
1985#[derive(Clone, Copy)]
1987enum PathScope<'p> {
1988 Bare,
1990 Program(&'p Program<'p>),
1992 Param { name: &'p str, value: &'p str },
1994}
1995
1996pub(crate) fn expression_to_path_string(expr: &Expression) -> Option<String> {
1998 path_string_in(expr, PathScope::Bare)
1999}
2000
2001fn path_string_in(expr: &Expression, scope: PathScope<'_>) -> Option<String> {
2002 match expr {
2003 Expression::ParenthesizedExpression(paren) => path_string_in(&paren.expression, scope),
2004 Expression::TSAsExpression(ts_as) => path_string_in(&ts_as.expression, scope),
2005 Expression::TSSatisfiesExpression(ts_sat) => path_string_in(&ts_sat.expression, scope),
2006 Expression::StaticMemberExpression(member) if member.property.name == "pathname" => {
2007 path_string_in(&member.object, scope)
2008 }
2009 Expression::CallExpression(call) => call_expression_to_path_string(call, scope),
2010 Expression::NewExpression(new_expr) => new_expression_to_path_string(new_expr, scope),
2011 _ => scoped_string(expr, scope),
2012 }
2013}
2014
2015fn scoped_string(expr: &Expression, scope: PathScope<'_>) -> Option<String> {
2017 match (expr, scope) {
2018 (Expression::Identifier(id), PathScope::Param { name, value }) if id.name == name => {
2019 Some(value.to_string())
2020 }
2021 _ => expression_to_string(expr),
2022 }
2023}
2024
2025fn call_expression_to_path_string(call: &CallExpression, scope: PathScope<'_>) -> Option<String> {
2026 if let PathScope::Program(program) = scope
2027 && let Expression::Identifier(id) = &call.callee
2028 && let Some(path) = local_helper_call_path(program, &id.name, call)
2029 {
2030 return Some(path);
2031 }
2032
2033 if matches!(&call.callee, Expression::Identifier(id) if id.name == "fileURLToPath") {
2034 return call
2035 .arguments
2036 .first()
2037 .and_then(Argument::as_expression)
2038 .and_then(|arg| path_string_in(arg, scope));
2039 }
2040
2041 let callee_name = match &call.callee {
2042 Expression::Identifier(id) => Some(id.name.as_str()),
2043 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
2044 _ => None,
2045 }?;
2046
2047 if !matches!(callee_name, "resolve" | "join") {
2048 return None;
2049 }
2050
2051 let mut segments = Vec::new();
2052 for (index, arg) in call.arguments.iter().enumerate() {
2053 let expr = arg.as_expression()?;
2054
2055 if is_dirname_anchor(expr) {
2056 if index == 0 {
2057 continue;
2058 }
2059 return None;
2060 }
2061
2062 segments.push(scoped_string(expr, scope)?);
2063 }
2064
2065 (!segments.is_empty()).then(|| join_path_segments(&segments))
2066}
2067
2068fn local_helper_call_path(program: &Program, name: &str, call: &CallExpression) -> Option<String> {
2075 let [argument] = call.arguments.as_slice() else {
2076 return None;
2077 };
2078 let value = expression_to_string(argument.as_expression()?)?;
2079 let (param, body) = find_local_path_helper(program, name)?;
2080 path_string_in(
2081 body,
2082 PathScope::Param {
2083 name: param,
2084 value: &value,
2085 },
2086 )
2087}
2088
2089fn find_local_path_helper<'a>(
2093 program: &'a Program<'a>,
2094 name: &str,
2095) -> Option<(&'a str, &'a Expression<'a>)> {
2096 let declared = program.body.iter().find_map(|stmt| {
2097 let func = match stmt {
2098 Statement::FunctionDeclaration(func) => func,
2099 Statement::ExportDeclaration(export) => match &export.declaration {
2100 Declaration::FunctionDeclaration(func) => func,
2101 _ => return None,
2102 },
2103 _ => return None,
2104 };
2105 func.id
2106 .as_ref()
2107 .is_some_and(|id| id.name == name)
2108 .then_some(&**func)
2109 });
2110 if let Some(func) = declared {
2111 return function_helper_body(func);
2112 }
2113 match find_variable_init_expression(program, name)?.without_parentheses() {
2114 Expression::ArrowFunctionExpression(arrow) => {
2115 let param = single_simple_param(&arrow.params)?;
2116 let body = match &arrow.body {
2117 ArrowFunctionBody::FunctionBody(body) => single_returned_expression(body)?,
2118 body => body.as_expression()?,
2119 };
2120 Some((param, body))
2121 }
2122 Expression::FunctionExpression(func) => function_helper_body(func),
2123 _ => None,
2124 }
2125}
2126
2127fn function_helper_body<'a>(func: &'a Function<'a>) -> Option<(&'a str, &'a Expression<'a>)> {
2128 let param = single_simple_param(&func.params)?;
2129 let body = single_returned_expression(func.body.as_ref()?)?;
2130 Some((param, body))
2131}
2132
2133fn single_simple_param<'a>(params: &'a FormalParameters<'a>) -> Option<&'a str> {
2134 let [param] = params.items.as_slice() else {
2135 return None;
2136 };
2137 if params.rest.is_some() || param.initializer.is_some() {
2138 return None;
2139 }
2140 match ¶m.pattern {
2141 BindingPattern::BindingIdentifier(id) => Some(id.name.as_str()),
2142 _ => None,
2143 }
2144}
2145
2146fn single_returned_expression<'a>(body: &'a FunctionBody<'a>) -> Option<&'a Expression<'a>> {
2147 match body.statements.as_slice() {
2148 [Statement::ReturnStatement(ret)] => ret.argument.as_ref(),
2149 _ => None,
2150 }
2151}
2152
2153fn is_dirname_anchor(expr: &Expression) -> bool {
2158 match expr {
2159 Expression::Identifier(id) => id.name == "__dirname",
2160 Expression::StaticMemberExpression(member) => {
2161 member.property.name == "dirname" && is_import_meta_expression(&member.object)
2162 }
2163 _ => false,
2164 }
2165}
2166
2167fn is_import_meta_expression(expr: &Expression) -> bool {
2169 matches!(expr, Expression::ImportMeta(_))
2170}
2171
2172fn new_expression_to_path_string(new_expr: &NewExpression, scope: PathScope<'_>) -> Option<String> {
2173 if !matches!(&new_expr.callee, Expression::Identifier(id) if id.name == "URL") {
2174 return None;
2175 }
2176
2177 let source = new_expr
2178 .arguments
2179 .first()
2180 .and_then(Argument::as_expression)
2181 .and_then(|arg| scoped_string(arg, scope))?;
2182
2183 let base = new_expr
2184 .arguments
2185 .get(1)
2186 .and_then(Argument::as_expression)?;
2187 is_import_meta_url_expression(base).then_some(source)
2188}
2189
2190fn is_import_meta_url_expression(expr: &Expression) -> bool {
2191 if let Expression::StaticMemberExpression(member) = expr {
2192 member.property.name == "url"
2193 && matches!(
2194 member.object,
2195 Expression::ImportMeta(_) | Expression::NewTarget(_)
2196 )
2197 } else {
2198 false
2199 }
2200}
2201
2202fn join_path_segments(segments: &[String]) -> String {
2203 let mut joined = PathBuf::new();
2204 for segment in segments {
2205 joined.push(segment);
2206 }
2207 joined.to_string_lossy().replace('\\', "/")
2208}
2209
2210fn expression_to_alias_pairs_kinded(expr: &Expression) -> Vec<(String, String, bool)> {
2215 match expr {
2216 Expression::ObjectExpression(obj) => obj
2217 .properties
2218 .iter()
2219 .filter_map(|prop| {
2220 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
2221 return None;
2222 };
2223 let find = property_key_to_string(&prop.key)?;
2224 let (replacement, is_bare) =
2225 alias_replacement_kinded(&prop.value, PathScope::Bare)?;
2226 Some((find, replacement, is_bare))
2227 })
2228 .collect(),
2229 Expression::ArrayExpression(arr) => arr
2230 .elements
2231 .iter()
2232 .filter_map(|element| {
2233 let Expression::ObjectExpression(obj) = element.as_expression()? else {
2234 return None;
2235 };
2236 let find = find_property(obj, "find")
2237 .and_then(|prop| expression_to_string(&prop.value))?;
2238 let (replacement, is_bare) = find_property(obj, "replacement")
2239 .and_then(|prop| alias_replacement_kinded(&prop.value, PathScope::Bare))?;
2240 Some((find, replacement, is_bare))
2241 })
2242 .collect(),
2243 _ => Vec::new(),
2244 }
2245}
2246
2247fn alias_replacement_kinded(expr: &Expression, scope: PathScope<'_>) -> Option<(String, bool)> {
2254 match expr {
2255 Expression::ParenthesizedExpression(paren) => {
2256 alias_replacement_kinded(&paren.expression, scope)
2257 }
2258 Expression::TSAsExpression(ts_as) => alias_replacement_kinded(&ts_as.expression, scope),
2259 Expression::TSSatisfiesExpression(ts_sat) => {
2260 alias_replacement_kinded(&ts_sat.expression, scope)
2261 }
2262 Expression::StringLiteral(s) => {
2263 let value = s.value.text().into_owned();
2264 let is_bare =
2265 !value.starts_with("./") && !value.starts_with("../") && !value.starts_with('/');
2266 Some((value, is_bare))
2267 }
2268 Expression::ArrayExpression(arr) => arr
2272 .elements
2273 .iter()
2274 .find_map(ArrayExpressionElement::as_expression)
2275 .and_then(|element| alias_replacement_kinded(element, scope)),
2276 _ => path_string_in(expr, scope).map(|value| (value, false)),
2277 }
2278}
2279
2280const MAX_ALIAS_RESOLVE_DEPTH: usize = 8;
2286
2287const ALIAS_SIBLING_EXTS: [&str; 6] = ["js", "mjs", "cjs", "ts", "mts", "cts"];
2294
2295fn resolve_alias_pairs_kinded(
2312 program: &Program,
2313 config_path: &Path,
2314 expr: &Expression,
2315 visited: &mut FxHashSet<PathBuf>,
2316 depth: usize,
2317) -> Vec<(String, String, bool)> {
2318 match expr {
2319 Expression::ParenthesizedExpression(paren) => {
2320 resolve_alias_pairs_kinded(program, config_path, &paren.expression, visited, depth)
2321 }
2322 Expression::TSAsExpression(ts_as) => {
2323 resolve_alias_pairs_kinded(program, config_path, &ts_as.expression, visited, depth)
2324 }
2325 Expression::TSSatisfiesExpression(ts_sat) => {
2326 resolve_alias_pairs_kinded(program, config_path, &ts_sat.expression, visited, depth)
2327 }
2328 Expression::ObjectExpression(obj) => {
2329 resolve_object_alias_pairs_kinded(program, config_path, obj, visited, depth)
2330 }
2331 Expression::ArrayExpression(arr) => {
2332 resolve_array_alias_pairs_kinded(program, config_path, arr, visited, depth)
2333 }
2334 Expression::Identifier(id) => {
2335 resolve_identifier_alias_pairs(program, config_path, id.name.as_str(), visited, depth)
2336 }
2337 _ => Vec::new(),
2338 }
2339}
2340
2341fn resolve_object_alias_pairs_kinded(
2344 program: &Program,
2345 config_path: &Path,
2346 obj: &ObjectExpression,
2347 visited: &mut FxHashSet<PathBuf>,
2348 depth: usize,
2349) -> Vec<(String, String, bool)> {
2350 let mut pairs = Vec::new();
2351 for prop in &obj.properties {
2352 match prop {
2353 ObjectPropertyKind::ObjectProperty(prop) => {
2354 if let Some(find) = property_key_to_string(&prop.key)
2355 && let Some((replacement, is_bare)) =
2356 alias_replacement_kinded(&prop.value, PathScope::Program(program))
2357 {
2358 pairs.push((find, replacement, is_bare));
2359 }
2360 }
2361 ObjectPropertyKind::SpreadProperty(spread) => {
2363 pairs.extend(resolve_alias_pairs_kinded(
2364 program,
2365 config_path,
2366 &spread.argument,
2367 visited,
2368 depth,
2369 ));
2370 }
2371 }
2372 }
2373 pairs
2374}
2375
2376fn resolve_array_alias_pairs_kinded(
2379 program: &Program,
2380 config_path: &Path,
2381 arr: &ArrayExpression,
2382 visited: &mut FxHashSet<PathBuf>,
2383 depth: usize,
2384) -> Vec<(String, String, bool)> {
2385 let mut pairs = Vec::new();
2386 for element in &arr.elements {
2387 match element {
2388 ArrayExpressionElement::SpreadElement(spread) => {
2390 pairs.extend(resolve_alias_pairs_kinded(
2391 program,
2392 config_path,
2393 &spread.argument,
2394 visited,
2395 depth,
2396 ));
2397 }
2398 _ => {
2399 if let Some(Expression::ObjectExpression(obj)) = element.as_expression()
2400 && let Some(find) = find_property(obj, "find")
2401 .and_then(|prop| expression_to_string(&prop.value))
2402 && let Some((replacement, is_bare)) = find_property(obj, "replacement")
2403 .and_then(|prop| {
2404 alias_replacement_kinded(&prop.value, PathScope::Program(program))
2405 })
2406 {
2407 pairs.push((find, replacement, is_bare));
2408 }
2409 }
2410 }
2411 }
2412 pairs
2413}
2414
2415fn resolve_identifier_alias_pairs(
2418 program: &Program,
2419 config_path: &Path,
2420 name: &str,
2421 visited: &mut FxHashSet<PathBuf>,
2422 depth: usize,
2423) -> Vec<(String, String, bool)> {
2424 if depth >= MAX_ALIAS_RESOLVE_DEPTH {
2425 return Vec::new();
2426 }
2427 if let Some(init) = find_variable_init_expression(program, name) {
2429 return resolve_alias_pairs_kinded(program, config_path, init, visited, depth + 1);
2430 }
2431 let Some((specifier, imported_name)) = find_relative_import_binding(program, name) else {
2433 return Vec::new();
2434 };
2435 resolve_imported_alias_pairs(
2436 config_path,
2437 &specifier,
2438 imported_name.as_deref(),
2439 visited,
2440 depth + 1,
2441 )
2442}
2443
2444fn resolve_imported_alias_pairs(
2447 config_path: &Path,
2448 specifier: &str,
2449 imported_name: Option<&str>,
2450 visited: &mut FxHashSet<PathBuf>,
2451 depth: usize,
2452) -> Vec<(String, String, bool)> {
2453 let Some((sibling_path, sibling_source)) = resolve_sibling_module(config_path, specifier)
2454 else {
2455 return Vec::new();
2456 };
2457 if !visited.insert(sibling_path.clone()) {
2458 return Vec::new();
2459 }
2460 extract_from_source(&sibling_source, &sibling_path, |program| {
2461 let init = find_exported_init(program, imported_name)?;
2462 let pairs = resolve_alias_pairs_kinded(program, &sibling_path, init, visited, depth);
2463 (!pairs.is_empty()).then_some(pairs)
2464 })
2465 .unwrap_or_default()
2466}
2467
2468fn find_variable_init_expression<'a>(
2473 program: &'a Program<'a>,
2474 name: &str,
2475) -> Option<&'a Expression<'a>> {
2476 for decl in top_level_variable_declarations(program) {
2477 for declarator in &decl.declarations {
2478 if let BindingPattern::BindingIdentifier(id) = &declarator.id
2479 && id.name == name
2480 && let Some(init) = &declarator.init
2481 {
2482 return Some(init);
2483 }
2484 }
2485 }
2486 None
2487}
2488
2489pub(crate) fn find_module_export_expression<'a>(
2492 program: &'a Program<'a>,
2493) -> Option<&'a Expression<'a>> {
2494 program.body.iter().find_map(|stmt| match stmt {
2495 Statement::ExportDefaultDeclaration(decl) => decl.declaration.as_expression(),
2496 Statement::ExpressionStatement(expr_stmt) => match &expr_stmt.expression {
2497 Expression::AssignmentExpression(assign) if is_module_exports_target(&assign.left) => {
2498 Some(&assign.right)
2499 }
2500 _ => None,
2501 },
2502 _ => None,
2503 })
2504}
2505
2506pub(crate) fn find_exported_init<'a>(
2511 program: &'a Program<'a>,
2512 name: Option<&str>,
2513) -> Option<&'a Expression<'a>> {
2514 match name {
2515 Some(name) => find_variable_init_expression(program, name),
2516 None => program.body.iter().find_map(|stmt| {
2517 if let Statement::ExportDefaultDeclaration(decl) = stmt {
2518 decl.declaration.as_expression()
2519 } else {
2520 None
2521 }
2522 }),
2523 }
2524}
2525
2526pub(crate) fn find_relative_import_binding(
2531 program: &Program,
2532 name: &str,
2533) -> Option<(String, Option<String>)> {
2534 for stmt in &program.body {
2535 let Statement::ImportDeclaration(decl) = stmt else {
2536 continue;
2537 };
2538 let specifier = &*decl.source.value.text();
2539 if !is_relative_specifier(specifier) {
2540 continue;
2541 }
2542 let Some(specifiers) = &decl.specifiers else {
2543 continue;
2544 };
2545 for spec in specifiers {
2546 match spec {
2547 ImportDeclarationSpecifier::ImportSpecifier(spec) if spec.local.name == name => {
2548 return Some((
2549 specifier.to_string(),
2550 Some(spec.imported.name().to_string()),
2551 ));
2552 }
2553 ImportDeclarationSpecifier::ImportDefaultSpecifier(spec)
2554 if spec.local.name == name =>
2555 {
2556 return Some((specifier.to_string(), None));
2557 }
2558 _ => {}
2559 }
2560 }
2561 }
2562 None
2563}
2564
2565pub(crate) fn is_relative_specifier(specifier: &str) -> bool {
2568 specifier.starts_with("./") || specifier.starts_with("../") || specifier.starts_with('/')
2569}
2570
2571pub(crate) fn resolve_sibling_module(
2577 config_path: &Path,
2578 specifier: &str,
2579) -> Option<(PathBuf, String)> {
2580 let parent = config_path.parent().unwrap_or(config_path);
2581 let direct = parent.join(specifier);
2582 if let Ok(source) = std::fs::read_to_string(&direct) {
2583 return Some((direct, source));
2584 }
2585 for ext in ALIAS_SIBLING_EXTS {
2586 let candidate = parent.join(format!("{specifier}.{ext}"));
2587 if let Ok(source) = std::fs::read_to_string(&candidate) {
2588 return Some((candidate, source));
2589 }
2590 }
2591 for ext in ALIAS_SIBLING_EXTS {
2592 let candidate = direct.join(format!("index.{ext}"));
2593 if let Ok(source) = std::fs::read_to_string(&candidate) {
2594 return Some((candidate, source));
2595 }
2596 }
2597 None
2598}
2599
2600fn find_default_export_array<'a>(program: &'a Program<'a>) -> Option<&'a ArrayExpression<'a>> {
2606 for stmt in &program.body {
2607 if let Statement::ExportDefaultDeclaration(decl) = stmt {
2608 if let ExportDefaultDeclarationKind::FunctionDeclaration(function) = &decl.declaration {
2609 if function.generator {
2610 return None;
2611 }
2612 return function.body.as_ref().and_then(|body| returned_array(body));
2613 }
2614 return decl
2615 .declaration
2616 .as_expression()
2617 .and_then(array_from_config_expression);
2618 }
2619 }
2620 None
2621}
2622
2623fn returned_array<'a>(body: &'a FunctionBody<'a>) -> Option<&'a ArrayExpression<'a>> {
2625 body.statements
2626 .iter()
2627 .find_map(|statement| match statement {
2628 Statement::ReturnStatement(returned) => Some(returned),
2629 _ => None,
2630 })
2631 .and_then(|returned| returned.argument.as_ref())
2632 .and_then(array_from_expression)
2633}
2634
2635fn array_from_config_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
2637 match expr {
2638 Expression::ArrowFunctionExpression(arrow) => match &arrow.body {
2639 ArrowFunctionBody::FunctionBody(body) => returned_array(body),
2640 body => body.as_expression().and_then(array_from_expression),
2641 },
2642 Expression::FunctionExpression(function) => {
2643 if function.generator {
2644 return None;
2645 }
2646 function.body.as_ref().and_then(|body| returned_array(body))
2647 }
2648 Expression::ParenthesizedExpression(paren) => {
2649 array_from_config_expression(&paren.expression)
2650 }
2651 Expression::TSAsExpression(ts_as) => array_from_config_expression(&ts_as.expression),
2652 Expression::TSSatisfiesExpression(ts_sat) => {
2653 array_from_config_expression(&ts_sat.expression)
2654 }
2655 Expression::CallExpression(call) => call
2656 .arguments
2657 .first()
2658 .and_then(Argument::as_expression)
2659 .and_then(array_from_config_expression),
2660 _ => array_from_expression(expr),
2661 }
2662}
2663
2664fn array_from_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
2665 match expr {
2666 Expression::ArrayExpression(arr) => Some(arr),
2667 Expression::ParenthesizedExpression(paren) => array_from_expression(&paren.expression),
2668 Expression::TSAsExpression(ts_as) => array_from_expression(&ts_as.expression),
2669 Expression::TSSatisfiesExpression(ts_sat) => array_from_expression(&ts_sat.expression),
2670 Expression::CallExpression(call) => call
2671 .arguments
2672 .first()
2673 .and_then(Argument::as_expression)
2674 .and_then(array_from_expression),
2675 _ => None,
2676 }
2677}
2678
2679pub(crate) fn lexical_normalize(path: &Path) -> PathBuf {
2680 let mut normalized = PathBuf::new();
2681
2682 for component in path.components() {
2683 match component {
2684 std::path::Component::CurDir => {}
2685 std::path::Component::ParentDir => {
2686 normalized.pop();
2687 }
2688 _ => normalized.push(component.as_os_str()),
2689 }
2690 }
2691
2692 normalized
2693}
2694
2695pub(crate) fn is_inside_root(path: &Path, root: &Path) -> bool {
2701 let Ok(resolved) = std::fs::canonicalize(path) else {
2702 return false;
2703 };
2704 let root = std::fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
2705 resolved.starts_with(root)
2706}
2707
2708pub(crate) fn glob_base_is_inside_root(dir: &Path, pattern: &str, root: &Path) -> bool {
2717 let literal = &pattern[..pattern.find(['*', '?', '[', '{']).unwrap_or(pattern.len())];
2718 lexical_normalize(&dir.join(path_from_config_string(literal))).starts_with(root)
2719}
2720
2721pub(crate) fn is_package_specifier(specifier: &str) -> bool {
2725 !specifier.is_empty()
2726 && specifier != "."
2727 && specifier != ".."
2728 && !specifier.starts_with("./")
2729 && !specifier.starts_with("../")
2730 && !specifier.starts_with('/')
2731 && !specifier.contains(':')
2732 && !specifier.contains('\\')
2733 && !specifier.chars().any(char::is_whitespace)
2734}
2735
2736fn expression_to_string_array(expr: &Expression) -> Vec<String> {
2738 match expr {
2739 Expression::ArrayExpression(arr) => arr
2740 .elements
2741 .iter()
2742 .filter_map(|el| match el {
2743 ArrayExpressionElement::SpreadElement(_) => None,
2744 _ => el.as_expression().and_then(expression_to_string),
2745 })
2746 .collect(),
2747 _ => vec![],
2748 }
2749}
2750
2751pub(crate) fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
2756 let mut values = Vec::new();
2757 match expr {
2758 Expression::StringLiteral(s) => {
2759 values.push(s.value.text().into_owned());
2760 }
2761 Expression::ArrayExpression(arr) => collect_shallow_array_values(arr, &mut values),
2762 Expression::ObjectExpression(obj) => {
2763 for prop in &obj.properties {
2764 if let ObjectPropertyKind::ObjectProperty(p) = prop {
2765 match &p.value {
2766 Expression::StringLiteral(s) => {
2767 values.push(s.value.text().into_owned());
2768 }
2769 Expression::ArrayExpression(sub_arr) => {
2770 if let Some(first) = sub_arr.elements.first()
2771 && let Some(first_expr) = first.as_expression()
2772 && let Some(s) = expression_to_string(first_expr)
2773 {
2774 values.push(s);
2775 }
2776 }
2777 _ => {}
2778 }
2779 }
2780 }
2781 }
2782 _ => {}
2783 }
2784 values
2785}
2786
2787fn collect_shallow_array_values(arr: &ArrayExpression<'_>, values: &mut Vec<String>) {
2791 for element in &arr.elements {
2792 let inner = match element {
2793 ArrayExpressionElement::SpreadElement(spread) => {
2794 collect_spread_array_values(&spread.argument, values);
2795 continue;
2796 }
2797 element => match element.as_expression() {
2798 Some(expr) => expr.get_inner_expression(),
2799 None => continue,
2800 },
2801 };
2802 match inner {
2803 Expression::StringLiteral(s) => values.push(s.value.text().into_owned()),
2804 Expression::ArrayExpression(sub_arr) => {
2805 if let Some(first) = sub_arr.elements.first()
2806 && let Some(first_expr) = first.as_expression()
2807 && let Some(s) = expression_to_string(first_expr)
2808 {
2809 values.push(s);
2810 }
2811 }
2812 _ => {}
2813 }
2814 }
2815}
2816
2817fn collect_spread_array_values(expr: &Expression<'_>, values: &mut Vec<String>) {
2820 match expr.get_inner_expression() {
2821 Expression::ArrayExpression(arr) => collect_shallow_array_values(arr, values),
2822 Expression::ConditionalExpression(conditional) => {
2823 collect_spread_array_values(&conditional.consequent, values);
2824 collect_spread_array_values(&conditional.alternate, values);
2825 }
2826 Expression::LogicalExpression(logical) => {
2827 collect_spread_array_values(&logical.right, values);
2828 }
2829 _ => {}
2830 }
2831}
2832
2833pub(crate) fn collect_shallow_string_or_object_property_values(
2835 expr: &Expression,
2836 object_property: &str,
2837) -> Vec<String> {
2838 match expr {
2839 Expression::ArrayExpression(arr) => arr
2840 .elements
2841 .iter()
2842 .filter_map(|element| {
2843 element
2844 .as_expression()
2845 .and_then(|expr| shallow_string_or_object_property(expr, object_property))
2846 })
2847 .collect(),
2848 _ => shallow_string_or_object_property(expr, object_property)
2849 .into_iter()
2850 .collect(),
2851 }
2852}
2853
2854fn shallow_string_or_object_property(expr: &Expression, object_property: &str) -> Option<String> {
2855 match expr {
2856 Expression::ParenthesizedExpression(paren) => {
2857 shallow_string_or_object_property(&paren.expression, object_property)
2858 }
2859 Expression::TSSatisfiesExpression(ts_sat) => {
2860 shallow_string_or_object_property(&ts_sat.expression, object_property)
2861 }
2862 Expression::TSAsExpression(ts_as) => {
2863 shallow_string_or_object_property(&ts_as.expression, object_property)
2864 }
2865 Expression::ArrayExpression(sub_arr) => sub_arr
2866 .elements
2867 .first()
2868 .and_then(ArrayExpressionElement::as_expression)
2869 .and_then(expression_to_string),
2870 Expression::ObjectExpression(obj) => {
2871 find_property(obj, object_property).and_then(|prop| expression_to_string(&prop.value))
2872 }
2873 _ => expression_to_string(expr),
2874 }
2875}
2876
2877fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
2879 match expr {
2880 Expression::StringLiteral(s) => {
2881 values.push(s.value.text().into_owned());
2882 }
2883 Expression::ArrayExpression(arr) => {
2884 for el in &arr.elements {
2885 if let Some(expr) = el.as_expression() {
2886 collect_all_string_values(expr, values);
2887 }
2888 }
2889 }
2890 Expression::ObjectExpression(obj) => {
2891 for prop in &obj.properties {
2892 if let ObjectPropertyKind::ObjectProperty(p) = prop {
2893 collect_all_string_values(&p.value, values);
2894 }
2895 }
2896 }
2897 _ => {}
2898 }
2899}
2900
2901fn property_key_to_string(key: &PropertyKey) -> Option<String> {
2903 match key {
2904 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
2905 PropertyKey::StringLiteral(s) => Some(s.value.text().into_owned()),
2906 _ => None,
2907 }
2908}
2909
2910fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
2912 if path.is_empty() {
2913 return None;
2914 }
2915 let prop = find_property(obj, path[0])?;
2916 if path.len() == 1 {
2917 if let Expression::ObjectExpression(nested) = &prop.value {
2918 let keys = nested
2919 .properties
2920 .iter()
2921 .filter_map(|p| {
2922 if let ObjectPropertyKind::ObjectProperty(p) = p {
2923 property_key_to_string(&p.key)
2924 } else {
2925 None
2926 }
2927 })
2928 .collect();
2929 return Some(keys);
2930 }
2931 return None;
2932 }
2933 if let Expression::ObjectExpression(nested) = &prop.value {
2934 get_nested_object_keys(nested, &path[1..])
2935 } else {
2936 None
2937 }
2938}
2939
2940fn get_nested_expression<'a>(
2942 obj: &'a ObjectExpression<'a>,
2943 path: &[&str],
2944) -> Option<&'a Expression<'a>> {
2945 if path.is_empty() {
2946 return None;
2947 }
2948 let prop = find_property(obj, path[0])?;
2949 if path.len() == 1 {
2950 return Some(&prop.value);
2951 }
2952 if let Expression::ObjectExpression(nested) = &prop.value {
2953 get_nested_expression(nested, &path[1..])
2954 } else {
2955 None
2956 }
2957}
2958
2959fn get_nested_string_or_array(
2961 obj: &ObjectExpression,
2962 path: &[&str],
2963 scope: PathScope<'_>,
2964) -> Option<Vec<String>> {
2965 if path.is_empty() {
2966 return None;
2967 }
2968 if path.len() == 1 {
2969 let prop = find_property(obj, path[0])?;
2970 return Some(string_or_array_in(&prop.value, scope));
2971 }
2972 let prop = find_property(obj, path[0])?;
2973 if let Expression::ObjectExpression(nested) = &prop.value {
2974 get_nested_string_or_array(nested, &path[1..], scope)
2975 } else {
2976 None
2977 }
2978}
2979
2980pub(crate) fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
2988 string_or_array_in(expr, PathScope::Bare)
2989}
2990
2991fn string_or_array_in(expr: &Expression, scope: PathScope<'_>) -> Vec<String> {
2992 match expr {
2993 Expression::StringLiteral(s) => vec![s.value.text().into_owned()],
2994 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
2995 .quasis
2996 .first()
2997 .map(|q| vec![q.value.raw.to_string()])
2998 .unwrap_or_default(),
2999 Expression::ArrayExpression(arr) => arr
3000 .elements
3001 .iter()
3002 .filter_map(|el| el.as_expression())
3003 .flat_map(|e| match e {
3004 Expression::ObjectExpression(obj) => find_property(obj, "input")
3005 .map(|p| string_or_array_in(&p.value, scope))
3006 .unwrap_or_default(),
3007 _ => path_string_in(e, scope).into_iter().collect(),
3008 })
3009 .collect(),
3010 Expression::ObjectExpression(obj) => obj
3011 .properties
3012 .iter()
3013 .flat_map(|p| {
3014 if let ObjectPropertyKind::ObjectProperty(p) = p {
3015 match &p.value {
3016 Expression::ArrayExpression(_) => string_or_array_in(&p.value, scope),
3017 Expression::ObjectExpression(value_obj) => {
3018 find_property(value_obj, "import")
3019 .map(|import_prop| string_or_array_in(&import_prop.value, scope))
3020 .unwrap_or_default()
3021 }
3022 _ => path_string_in(&p.value, scope).into_iter().collect(),
3023 }
3024 } else {
3025 Vec::new()
3026 }
3027 })
3028 .collect(),
3029 _ => path_string_in(expr, scope).into_iter().collect(),
3030 }
3031}
3032
3033fn collect_require_sources(expr: &Expression) -> Vec<String> {
3035 let mut sources = Vec::new();
3036 match expr {
3037 Expression::CallExpression(call) if is_require_call(call) => {
3038 if let Some(s) = get_require_source(call) {
3039 sources.push(s);
3040 }
3041 }
3042 Expression::ArrayExpression(arr) => {
3043 for el in &arr.elements {
3044 if let Some(inner) = el.as_expression() {
3045 match inner {
3046 Expression::CallExpression(call) if is_require_call(call) => {
3047 if let Some(s) = get_require_source(call) {
3048 sources.push(s);
3049 }
3050 }
3051 Expression::ArrayExpression(sub_arr) => {
3052 if let Some(first) = sub_arr.elements.first()
3053 && let Some(Expression::CallExpression(call)) =
3054 first.as_expression()
3055 && is_require_call(call)
3056 && let Some(s) = get_require_source(call)
3057 {
3058 sources.push(s);
3059 }
3060 }
3061 _ => {}
3062 }
3063 }
3064 }
3065 }
3066 _ => {}
3067 }
3068 sources
3069}
3070
3071pub(crate) fn is_require_call(call: &CallExpression) -> bool {
3073 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
3074}
3075
3076pub(crate) fn get_require_source(call: &CallExpression) -> Option<String> {
3078 call.arguments.first().and_then(|arg| {
3079 if let Argument::StringLiteral(s) = arg {
3080 Some(s.value.text().into_owned())
3081 } else {
3082 None
3083 }
3084 })
3085}
3086
3087#[cfg(test)]
3088mod tests {
3089 use super::*;
3090 use std::path::PathBuf;
3091
3092 fn js_path() -> PathBuf {
3093 PathBuf::from("config.js")
3094 }
3095
3096 fn ts_path() -> PathBuf {
3097 PathBuf::from("config.ts")
3098 }
3099
3100 #[test]
3101 fn extract_config_array_tuple_heads_reads_first_string_of_each_tuple() {
3102 let source = r#"
3103 export default defineNuxtConfig({
3104 modules: ["@nuxt/image", ["@nuxt/content", { watch: false }], [someModule, {}], []]
3105 });
3106 "#;
3107 assert_eq!(
3108 extract_config_array_tuple_heads(source, &ts_path(), &["modules"]),
3109 vec!["@nuxt/content".to_string()]
3110 );
3111 }
3112
3113 #[test]
3114 fn extract_lazy_imports_bare_arrows() {
3115 let source = r"
3116 import { defineConfig } from '@adonisjs/core/app'
3117 export default defineConfig({
3118 preloads: [
3119 () => import('#start/routes'),
3120 () => import('#start/kernel'),
3121 ],
3122 })
3123 ";
3124 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["preloads"]);
3125 assert_eq!(specs, vec!["#start/routes", "#start/kernel"]);
3126 }
3127
3128 #[test]
3129 fn extract_lazy_imports_object_form_with_file_key() {
3130 let source = r"
3131 export default defineConfig({
3132 providers: [
3133 () => import('@adonisjs/core/providers/app_provider'),
3134 {
3135 file: () => import('@adonisjs/core/providers/repl_provider'),
3136 environment: ['repl', 'test'],
3137 },
3138 ],
3139 })
3140 ";
3141 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
3142 assert_eq!(
3143 specs,
3144 vec![
3145 "@adonisjs/core/providers/app_provider",
3146 "@adonisjs/core/providers/repl_provider",
3147 ]
3148 );
3149 }
3150
3151 #[test]
3152 fn extract_lazy_imports_block_body_with_return() {
3153 let source = r"
3154 export default defineConfig({
3155 commands: [
3156 () => { return import('@adonisjs/core/commands') },
3157 ],
3158 })
3159 ";
3160 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
3161 assert_eq!(specs, vec!["@adonisjs/core/commands"]);
3162 }
3163
3164 #[test]
3165 fn extract_lazy_imports_skips_unknown_element_shapes() {
3166 let source = r"
3167 export default defineConfig({
3168 commands: [
3169 'string-entry',
3170 42,
3171 { other: 'value' },
3172 () => import('@adonisjs/lucid/commands'),
3173 ],
3174 })
3175 ";
3176 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
3177 assert_eq!(specs, vec!["@adonisjs/lucid/commands"]);
3178 }
3179
3180 #[test]
3181 fn extract_lazy_imports_missing_property_returns_empty() {
3182 let source = r"
3183 export default defineConfig({
3184 preloads: [() => import('#start/routes')],
3185 })
3186 ";
3187 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
3188 assert!(specs.is_empty());
3189 }
3190
3191 #[test]
3192 fn extract_imports_basic() {
3193 let source = r"
3194 import foo from 'foo-pkg';
3195 import { bar } from '@scope/bar';
3196 export default {};
3197 ";
3198 let imports = extract_imports(source, &js_path());
3199 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
3200 }
3201
3202 #[test]
3203 fn extract_default_export_object_property() {
3204 let source = r#"export default { testDir: "./tests" };"#;
3205 let val = extract_config_string(source, &js_path(), &["testDir"]);
3206 assert_eq!(val, Some("./tests".to_string()));
3207 }
3208
3209 #[test]
3210 fn extract_define_config_property() {
3211 let source = r#"
3212 import { defineConfig } from 'vitest/config';
3213 export default defineConfig({
3214 test: {
3215 include: ["**/*.test.ts", "**/*.spec.ts"],
3216 setupFiles: ["./test/setup.ts"]
3217 }
3218 });
3219 "#;
3220 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
3221 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
3222
3223 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
3224 assert_eq!(setup, vec!["./test/setup.ts"]);
3225 }
3226
3227 #[test]
3228 fn extract_module_exports_property() {
3229 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
3230 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
3231 assert_eq!(val, Some("jsdom".to_string()));
3232 }
3233
3234 #[test]
3235 fn extract_nested_string_array() {
3236 let source = r#"
3237 export default {
3238 resolve: {
3239 alias: {
3240 "@": "./src"
3241 }
3242 },
3243 test: {
3244 include: ["src/**/*.test.ts"]
3245 }
3246 };
3247 "#;
3248 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
3249 assert_eq!(include, vec!["src/**/*.test.ts"]);
3250 }
3251
3252 #[test]
3253 fn extract_addons_array() {
3254 let source = r#"
3255 export default {
3256 addons: [
3257 "@storybook/addon-a11y",
3258 "@storybook/addon-docs",
3259 "@storybook/addon-links"
3260 ]
3261 };
3262 "#;
3263 let addons = extract_config_property_strings(source, &ts_path(), "addons");
3264 assert_eq!(
3265 addons,
3266 vec![
3267 "@storybook/addon-a11y",
3268 "@storybook/addon-docs",
3269 "@storybook/addon-links"
3270 ]
3271 );
3272 }
3273
3274 #[test]
3275 fn handle_empty_config() {
3276 let source = "";
3277 let result = extract_config_string(source, &js_path(), &["key"]);
3278 assert_eq!(result, None);
3279 }
3280
3281 #[test]
3282 fn object_keys_postcss_plugins() {
3283 let source = r"
3284 module.exports = {
3285 plugins: {
3286 autoprefixer: {},
3287 tailwindcss: {},
3288 'postcss-import': {}
3289 }
3290 };
3291 ";
3292 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3293 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
3294 }
3295
3296 #[test]
3297 fn object_keys_nested_path() {
3298 let source = r"
3299 export default {
3300 build: {
3301 plugins: {
3302 minify: {},
3303 compress: {}
3304 }
3305 }
3306 };
3307 ";
3308 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
3309 assert_eq!(keys, vec!["minify", "compress"]);
3310 }
3311
3312 #[test]
3313 fn object_keys_empty_object() {
3314 let source = r"export default { plugins: {} };";
3315 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3316 assert!(keys.is_empty());
3317 }
3318
3319 #[test]
3320 fn object_keys_non_object_returns_empty() {
3321 let source = r#"export default { plugins: ["a", "b"] };"#;
3322 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3323 assert!(keys.is_empty());
3324 }
3325
3326 #[test]
3327 fn string_or_array_single_string() {
3328 let source = r#"export default { entry: "./src/index.js" };"#;
3329 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3330 assert_eq!(result, vec!["./src/index.js"]);
3331 }
3332
3333 #[test]
3334 fn string_or_array_array() {
3335 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
3336 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3337 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
3338 }
3339
3340 #[test]
3341 fn string_or_array_object_values() {
3342 let source =
3343 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
3344 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3345 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
3346 }
3347
3348 #[test]
3349 fn array_or_object_string_or_array_reads_each_array_element() {
3350 let source = r#"
3351 const shared = { format: ["esm"] };
3352 export default defineConfig([
3353 { entry: "./src/a.ts" },
3354 ({ ...shared, entry: { b: "./src/b.ts" } }),
3355 { entry: ["./src/c.ts"] } satisfies Options,
3356 { format: ["cjs"] },
3357 shared,
3358 ] as Options[]);
3359 "#;
3360 let result = extract_config_array_or_object_string_or_array(source, &ts_path(), &["entry"]);
3361 assert_eq!(result, vec!["./src/a.ts", "./src/b.ts", "./src/c.ts"]);
3362 }
3363
3364 #[test]
3365 fn array_or_object_string_or_array_falls_back_to_config_object() {
3366 let source = r#"export default defineConfig({ entry: { main: "./src/main.ts" } });"#;
3367 let result = extract_config_array_or_object_string_or_array(source, &ts_path(), &["entry"]);
3368 assert_eq!(result, vec!["./src/main.ts"]);
3369 }
3370
3371 #[test]
3372 fn string_or_array_object_array_values() {
3373 let source = r#"export default { entry: { app: ["./src/polyfill.js", "./src/app.js"] } };"#;
3374 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3375 assert_eq!(result, vec!["./src/polyfill.js", "./src/app.js"]);
3376 }
3377
3378 #[test]
3379 fn string_or_array_webpack_entry_descriptors() {
3380 let source = r#"
3381 export default {
3382 entry: {
3383 app: {
3384 import: "./src/app.js",
3385 filename: "pages/app.js",
3386 dependOn: "shared",
3387 },
3388 admin: {
3389 import: ["./src/admin-polyfill.js", "./src/admin.js"],
3390 runtime: "runtime",
3391 },
3392 shared: ["react", "react-dom"],
3393 },
3394 };
3395 "#;
3396 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3397 assert_eq!(
3398 result,
3399 vec![
3400 "./src/app.js",
3401 "./src/admin-polyfill.js",
3402 "./src/admin.js",
3403 "react",
3404 "react-dom"
3405 ]
3406 );
3407 }
3408
3409 #[test]
3410 fn string_or_array_nested_path() {
3411 let source = r#"
3412 export default {
3413 build: {
3414 rollupOptions: {
3415 input: ["./index.html", "./about.html"]
3416 }
3417 }
3418 };
3419 "#;
3420 let result = extract_config_string_or_array(
3421 source,
3422 &js_path(),
3423 &["build", "rollupOptions", "input"],
3424 );
3425 assert_eq!(result, vec!["./index.html", "./about.html"]);
3426 }
3427
3428 #[test]
3429 fn string_or_array_template_literal() {
3430 let source = r"export default { entry: `./src/index.js` };";
3431 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3432 assert_eq!(result, vec!["./src/index.js"]);
3433 }
3434
3435 #[test]
3436 fn string_or_array_object_path_helper_values() {
3437 let source = r#"
3438 import { resolve, join } from "node:path";
3439 import path from "node:path";
3440 export default {
3441 build: {
3442 rollupOptions: {
3443 input: {
3444 app: resolve(__dirname, "src/app.ts"),
3445 modal: path.resolve(__dirname, "src/modal.ts"),
3446 tabs: join(__dirname, "src/tabs.ts"),
3447 styles: resolve(__dirname, "src/index.css"),
3448 },
3449 },
3450 },
3451 };
3452 "#;
3453 let result = extract_config_string_or_array(
3454 source,
3455 &js_path(),
3456 &["build", "rollupOptions", "input"],
3457 );
3458 assert_eq!(
3459 result,
3460 vec!["src/app.ts", "src/modal.ts", "src/tabs.ts", "src/index.css"]
3461 );
3462 }
3463
3464 #[test]
3465 fn string_or_array_array_path_helper_values() {
3466 let source = r#"
3467 import { resolve } from "node:path";
3468 export default {
3469 build: {
3470 rollupOptions: {
3471 input: [resolve(__dirname, "src/a.ts"), "./src/b.ts"],
3472 },
3473 },
3474 };
3475 "#;
3476 let result = extract_config_string_or_array(
3477 source,
3478 &js_path(),
3479 &["build", "rollupOptions", "input"],
3480 );
3481 assert_eq!(result, vec!["src/a.ts", "./src/b.ts"]);
3482 }
3483
3484 #[test]
3485 fn string_or_array_top_level_path_helper_call() {
3486 let source = r#"
3487 import { resolve } from "node:path";
3488 export default { build: { lib: { entry: resolve(__dirname, "src/index.ts") } } };
3489 "#;
3490 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
3491 assert_eq!(result, vec!["src/index.ts"]);
3492 }
3493
3494 #[test]
3495 fn string_or_array_import_meta_dirname_anchor() {
3496 let source = r#"
3497 import { resolve } from "node:path";
3498 export default {
3499 build: { lib: { entry: resolve(import.meta.dirname, "src/index.ts") } },
3500 };
3501 "#;
3502 let result = extract_config_string_or_array(source, &ts_path(), &["build", "lib", "entry"]);
3503 assert_eq!(result, vec!["src/index.ts"]);
3504 }
3505
3506 #[test]
3507 fn string_or_array_non_literal_path_helper_args_dropped() {
3508 let source = r#"
3509 import { resolve } from "node:path";
3510 export default { build: { lib: { entry: resolve(baseDir, "src/index.ts") } } };
3511 "#;
3512 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
3513 assert!(
3514 result.is_empty(),
3515 "non-literal path-helper args must be dropped: {result:?}"
3516 );
3517 }
3518
3519 #[test]
3520 fn require_strings_array() {
3521 let source = r"
3522 module.exports = {
3523 plugins: [
3524 require('autoprefixer'),
3525 require('postcss-import')
3526 ]
3527 };
3528 ";
3529 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3530 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
3531 }
3532
3533 #[test]
3534 fn require_strings_with_tuples() {
3535 let source = r"
3536 module.exports = {
3537 plugins: [
3538 require('autoprefixer'),
3539 [require('postcss-preset-env'), { stage: 3 }]
3540 ]
3541 };
3542 ";
3543 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3544 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
3545 }
3546
3547 #[test]
3548 fn require_strings_empty_array() {
3549 let source = r"module.exports = { plugins: [] };";
3550 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3551 assert!(deps.is_empty());
3552 }
3553
3554 #[test]
3555 fn require_strings_no_require_calls() {
3556 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
3557 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3558 assert!(deps.is_empty());
3559 }
3560
3561 #[test]
3562 fn extract_aliases_from_object_with_file_url_to_path() {
3563 let source = r#"
3564 import { defineConfig } from 'vite';
3565 import { fileURLToPath, URL } from 'node:url';
3566
3567 export default defineConfig({
3568 resolve: {
3569 alias: {
3570 "@": fileURLToPath(new URL("./src", import.meta.url))
3571 }
3572 }
3573 });
3574 "#;
3575
3576 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
3577 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
3578 }
3579
3580 #[test]
3581 fn extract_aliases_from_array_form() {
3582 let source = r#"
3583 export default {
3584 resolve: {
3585 alias: [
3586 { find: "@", replacement: "./src" },
3587 { find: "$utils", replacement: "src/lib/utils" }
3588 ]
3589 }
3590 };
3591 "#;
3592
3593 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
3594 assert_eq!(
3595 aliases,
3596 vec![
3597 ("@".to_string(), "./src".to_string()),
3598 ("$utils".to_string(), "src/lib/utils".to_string())
3599 ]
3600 );
3601 }
3602
3603 #[test]
3604 fn extract_aliases_from_object_with_array_values() {
3605 let source = r#"
3606 ({
3607 compilerOptions: {
3608 paths: {
3609 "@/*": ["./src/*"],
3610 "@shared/*": ["./shared/*", "./fallback/*"]
3611 }
3612 }
3613 })
3614 "#;
3615
3616 let aliases = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
3617 assert_eq!(
3618 aliases,
3619 vec![
3620 ("@/*".to_string(), "./src/*".to_string()),
3621 ("@shared/*".to_string(), "./shared/*".to_string())
3622 ]
3623 );
3624 }
3625
3626 #[test]
3627 fn extract_array_object_strings_mixed_forms() {
3628 let source = r#"
3629 export default {
3630 components: [
3631 "~/components",
3632 { path: "@/feature-components" }
3633 ]
3634 };
3635 "#;
3636
3637 let values =
3638 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
3639 assert_eq!(
3640 values,
3641 vec![
3642 "~/components".to_string(),
3643 "@/feature-components".to_string()
3644 ]
3645 );
3646 }
3647
3648 #[test]
3649 fn extract_config_plugin_option_string_from_json() {
3650 let source = r#"{
3651 "expo": {
3652 "plugins": [
3653 ["expo-router", { "root": "src/app" }]
3654 ]
3655 }
3656 }"#;
3657
3658 let value = extract_config_plugin_option_string(
3659 source,
3660 &json_path(),
3661 &["expo", "plugins"],
3662 "expo-router",
3663 "root",
3664 );
3665
3666 assert_eq!(value, Some("src/app".to_string()));
3667 }
3668
3669 #[test]
3670 fn extract_config_plugin_option_string_from_top_level_plugins() {
3671 let source = r#"{
3672 "plugins": [
3673 ["expo-router", { "root": "./src/routes" }]
3674 ]
3675 }"#;
3676
3677 let value = extract_config_plugin_option_string_from_paths(
3678 source,
3679 &json_path(),
3680 &[&["plugins"], &["expo", "plugins"]],
3681 "expo-router",
3682 "root",
3683 );
3684
3685 assert_eq!(value, Some("./src/routes".to_string()));
3686 }
3687
3688 #[test]
3689 fn extract_config_plugin_option_string_from_ts_config() {
3690 let source = r"
3691 export default {
3692 expo: {
3693 plugins: [
3694 ['expo-router', { root: './src/app' }]
3695 ]
3696 }
3697 };
3698 ";
3699
3700 let value = extract_config_plugin_option_string(
3701 source,
3702 &ts_path(),
3703 &["expo", "plugins"],
3704 "expo-router",
3705 "root",
3706 );
3707
3708 assert_eq!(value, Some("./src/app".to_string()));
3709 }
3710
3711 #[test]
3712 fn extract_config_plugin_option_string_returns_none_when_plugin_missing() {
3713 let source = r#"{
3714 "expo": {
3715 "plugins": [
3716 ["expo-font", {}]
3717 ]
3718 }
3719 }"#;
3720
3721 let value = extract_config_plugin_option_string(
3722 source,
3723 &json_path(),
3724 &["expo", "plugins"],
3725 "expo-router",
3726 "root",
3727 );
3728
3729 assert_eq!(value, None);
3730 }
3731
3732 #[test]
3733 fn vite_react_babel_dependencies_extract_plain_tuple_and_prefixed_entries() {
3734 let source = r#"
3735 import react from "@vitejs/plugin-react";
3736
3737 export default defineConfig({
3738 plugins: [
3739 react({
3740 babel: {
3741 plugins: [
3742 "babel-plugin-plain",
3743 ["module:@preact/signals-react-transform", { mode: "auto" }],
3744 ],
3745 presets: [["@babel/preset-react", { runtime: "automatic" }]],
3746 },
3747 }),
3748 ],
3749 });
3750 "#;
3751
3752 let deps = extract_vite_plugin_option_dependencies(source, &ts_path());
3753
3754 assert_eq!(
3755 deps,
3756 vec![
3757 "babel-plugin-plain".to_string(),
3758 "@preact/signals-react-transform".to_string(),
3759 "@babel/preset-react".to_string(),
3760 ]
3761 );
3762 }
3763
3764 #[test]
3765 fn vite_react_babel_dependencies_support_default_alias_import() {
3766 let source = r#"
3767 import { default as viteReact } from "@vitejs/plugin-react";
3768
3769 export default {
3770 plugins: [
3771 viteReact({
3772 babel: {
3773 plugins: [["module:@scope/pkg/plugin", {}]],
3774 },
3775 }),
3776 ],
3777 };
3778 "#;
3779
3780 let deps = extract_vite_plugin_option_dependencies(source, &ts_path());
3781
3782 assert_eq!(deps, vec!["@scope/pkg".to_string()]);
3783 }
3784
3785 #[test]
3786 fn vite_react_babel_dependencies_ignore_unrelated_plugin_calls() {
3787 let source = r#"
3788 import vue from "@vitejs/plugin-vue";
3789
3790 export default {
3791 plugins: [
3792 vue({
3793 babel: {
3794 plugins: ["@preact/signals-react-transform"],
3795 },
3796 }),
3797 ],
3798 };
3799 "#;
3800
3801 let deps = extract_vite_plugin_option_dependencies(source, &ts_path());
3802
3803 assert!(deps.is_empty());
3804 }
3805
3806 #[test]
3807 fn vite_react_babel_dependencies_skip_relative_and_protocol_entries() {
3808 let source = r#"
3809 import react from "@vitejs/plugin-react";
3810
3811 export default {
3812 plugins: [
3813 react({
3814 babel: {
3815 plugins: ["./local-plugin", "module:./local-prefixed", "http://example.com/plugin"],
3816 },
3817 }),
3818 ],
3819 };
3820 "#;
3821
3822 let deps = extract_vite_plugin_option_dependencies(source, &ts_path());
3823
3824 assert!(deps.is_empty());
3825 }
3826
3827 #[test]
3828 fn normalize_config_path_relative_to_root() {
3829 let config_path = PathBuf::from("/project/vite.config.ts");
3830 let root = PathBuf::from("/project");
3831
3832 assert_eq!(
3833 normalize_config_path("./src/lib", &config_path, &root),
3834 Some("src/lib".to_string())
3835 );
3836 assert_eq!(
3837 normalize_config_path("/src/lib", &config_path, &root),
3838 Some("src/lib".to_string())
3839 );
3840 }
3841
3842 #[test]
3843 fn normalize_config_path_mixed_separators_and_parent_dirs() {
3844 let config_path = PathBuf::from("/project/config/vite.config.ts");
3845 let root = PathBuf::from("/project");
3846
3847 assert_eq!(
3848 normalize_config_path(".\\src\\..\\app\\lib", &config_path, &root),
3849 Some("config/app/lib".to_string())
3850 );
3851 }
3852
3853 #[test]
3856 fn normalize_config_path_reads_an_absolute_path_under_root_as_absolute() {
3857 let root = std::env::temp_dir().join("fallow-2806-project");
3858 let config_path = root.join("vite.config.ts");
3859 let absolute = path_to_config_string(&root.join("src").join("lib"));
3860
3861 assert_eq!(
3862 normalize_config_path(&absolute, &config_path, &root),
3863 Some("src/lib".to_string())
3864 );
3865 assert_eq!(
3866 normalize_filesystem_config_path(&absolute, &config_path, &root),
3867 Some("src/lib".to_string())
3868 );
3869 }
3870
3871 #[cfg(unix)]
3876 #[test]
3877 fn normalize_config_path_keeps_the_vite_root_in_root_reading() {
3878 let temp = tempfile::tempdir().expect("temp dir");
3879 let root = temp.path().join("src");
3880 std::fs::create_dir_all(&root).expect("root dir");
3881 let config_path = root.join("vite.config.ts");
3882 let root_string = path_to_config_string(&root);
3883 let nested = root_string.trim_start_matches('/').to_string();
3884 let under = format!("{root_string}/lib");
3885
3886 assert_eq!(
3887 normalize_config_path(&root_string, &config_path, &root),
3888 Some(nested.clone())
3889 );
3890 assert_eq!(
3891 normalize_config_path(&under, &config_path, &root),
3892 Some("lib".to_string())
3893 );
3894
3895 std::fs::create_dir_all(root.join(&nested)).expect("root-in-root dir");
3896 assert_eq!(
3897 normalize_config_path(&under, &config_path, &root),
3898 Some(format!("{nested}/lib"))
3899 );
3900 assert_eq!(
3901 normalize_filesystem_config_path(&under, &config_path, &root),
3902 Some("lib".to_string())
3903 );
3904 }
3905
3906 #[test]
3909 fn normalize_filesystem_config_path_does_not_read_a_leading_slash_as_root_relative() {
3910 let config_path = PathBuf::from("/project/config/webpack.config.js");
3911 let root = PathBuf::from("/project");
3912
3913 assert_eq!(
3914 normalize_filesystem_config_path("/src/lib", &config_path, &root),
3915 None
3916 );
3917 assert_eq!(
3918 normalize_filesystem_config_path("./src/lib", &config_path, &root),
3919 Some("config/src/lib".to_string())
3920 );
3921 assert_eq!(
3922 normalize_config_path("/src/lib", &config_path, &root),
3923 Some("src/lib".to_string())
3924 );
3925 }
3926
3927 #[test]
3928 fn normalize_config_path_leading_slash_stays_project_relative() {
3929 let config_path = PathBuf::from("/project/vite.config.ts");
3930 let root = PathBuf::from("/project");
3931
3932 assert_eq!(
3933 normalize_config_path("/src\\lib", &config_path, &root),
3934 Some("src/lib".to_string())
3935 );
3936 }
3937
3938 #[test]
3939 fn json_wrapped_in_parens_string() {
3940 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
3941 let val = extract_config_string(source, &js_path(), &["extends"]);
3942 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
3943 }
3944
3945 #[test]
3946 fn json_wrapped_in_parens_nested_array() {
3947 let source =
3948 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
3949 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
3950 assert_eq!(types, vec!["node", "jest"]);
3951
3952 let include = extract_config_string_array(source, &js_path(), &["include"]);
3953 assert_eq!(include, vec!["src/**/*"]);
3954 }
3955
3956 #[test]
3957 fn json_wrapped_in_parens_object_keys() {
3958 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
3959 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3960 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
3961 }
3962
3963 fn json_path() -> PathBuf {
3964 PathBuf::from("config.json")
3965 }
3966
3967 #[test]
3968 fn json_file_parsed_correctly() {
3969 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
3970 let val = extract_config_string(source, &json_path(), &["key"]);
3971 assert_eq!(val, Some("value".to_string()));
3972
3973 let list = extract_config_string_array(source, &json_path(), &["list"]);
3974 assert_eq!(list, vec!["a", "b"]);
3975 }
3976
3977 #[test]
3978 fn jsonc_file_parsed_correctly() {
3979 let source = r#"{"key": "value"}"#;
3980 let path = PathBuf::from("tsconfig.jsonc");
3981 let val = extract_config_string(source, &path, &["key"]);
3982 assert_eq!(val, Some("value".to_string()));
3983 }
3984
3985 #[test]
3986 fn jsonc_file_ending_in_line_comment_parsed_correctly() {
3987 let source = "{\"key\": \"value\"}\n// trailing comment, no final newline";
3988 let path = PathBuf::from(".oxlintrc.jsonc");
3989 let val = extract_config_string(source, &path, &["key"]);
3990 assert_eq!(val, Some("value".to_string()));
3991 }
3992
3993 #[test]
3994 fn extract_define_config_arrow_function() {
3995 let source = r#"
3996 import { defineConfig } from 'vite';
3997 export default defineConfig(() => ({
3998 test: {
3999 include: ["**/*.test.ts"]
4000 }
4001 }));
4002 "#;
4003 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
4004 assert_eq!(include, vec!["**/*.test.ts"]);
4005 }
4006
4007 #[test]
4011 fn extract_define_config_block_body_arrow_function() {
4012 let source = r#"
4013 import { defineConfig } from 'vite';
4014 export default defineConfig(({ mode }) => {
4015 const isProduction = mode === 'production';
4016 return {
4017 test: {
4018 environment: "jsdom",
4019 setupFiles: "./tests/setup.ts"
4020 },
4021 base: isProduction ? '/app/' : '/'
4022 };
4023 });
4024 "#;
4025 assert_eq!(
4026 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4027 Some("jsdom")
4028 );
4029 assert_eq!(
4030 extract_config_string(source, &ts_path(), &["test", "setupFiles"]).as_deref(),
4031 Some("./tests/setup.ts")
4032 );
4033 }
4034
4035 #[test]
4036 fn extract_define_config_function_expression() {
4037 let source = r#"
4038 import { defineConfig } from 'vite';
4039 export default defineConfig(function () {
4040 return { test: { environment: "happy-dom" } };
4041 });
4042 "#;
4043 assert_eq!(
4044 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4045 Some("happy-dom")
4046 );
4047 }
4048
4049 #[test]
4053 fn wrapper_options_object_does_not_shadow_named_config_arg() {
4054 let source = r#"
4055 const nextConfig = { pageExtensions: ["page.tsx"] };
4056 module.exports = withSentryConfig(nextConfig, { org: "o", project: "p", silent: true });
4057 "#;
4058 assert_eq!(
4059 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
4060 vec!["page.tsx"]
4061 );
4062 }
4063
4064 #[test]
4065 fn wrapper_options_object_does_not_shadow_named_config_arg_esm() {
4066 let source = r#"
4067 const nextConfig = { pageExtensions: ["page.tsx"] };
4068 export default withSentryConfig(nextConfig, { org: "o", project: "p" });
4069 "#;
4070 assert_eq!(
4071 extract_config_string_array(source, &ts_path(), &["pageExtensions"]),
4072 vec!["page.tsx"]
4073 );
4074 }
4075
4076 #[test]
4078 fn merge_config_extracts_nested_define_config_object() {
4079 let source = r#"
4080 import { defineConfig, mergeConfig } from 'vitest/config';
4081 import viteConfig from './vite.config';
4082 export default mergeConfig(viteConfig, defineConfig({
4083 test: { environment: "jsdom" }
4084 }));
4085 "#;
4086 assert_eq!(
4087 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4088 Some("jsdom")
4089 );
4090 }
4091
4092 #[test]
4093 fn define_config_wrapping_merge_config_extracts_object() {
4094 let source = r#"
4095 import { defineConfig, mergeConfig } from 'vitest/config';
4096 import base from './base';
4097 export default defineConfig(mergeConfig(base, { test: { environment: "jsdom" } }));
4098 "#;
4099 assert_eq!(
4100 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4101 Some("jsdom")
4102 );
4103 }
4104
4105 #[test]
4108 fn inline_object_at_argument_zero_still_wins() {
4109 let source = r#"
4110 const unrelated = { pageExtensions: ["wrong.tsx"] };
4111 module.exports = withSentryConfig({ pageExtensions: ["right.tsx"] }, { org: "o" });
4112 "#;
4113 assert_eq!(
4114 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
4115 vec!["right.tsx"]
4116 );
4117 }
4118
4119 #[test]
4123 fn conditional_config_callback_extracts_branch_return() {
4124 let source = r#"
4125 import { defineConfig } from 'vite';
4126 export default defineConfig(({ command }) => {
4127 if (command === "serve") {
4128 return { test: { environment: "jsdom" } };
4129 } else {
4130 return { test: { environment: "node" } };
4131 }
4132 });
4133 "#;
4134 assert_eq!(
4135 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4136 Some("jsdom"),
4137 "with no top-level return, the first branch in source order wins"
4138 );
4139 }
4140
4141 #[test]
4142 fn try_block_return_is_extracted() {
4143 let source = r#"
4144 export default (() => {
4145 try {
4146 return { test: { environment: "jsdom" } };
4147 } catch (e) {
4148 return { test: { environment: "node" } };
4149 }
4150 });
4151 "#;
4152 assert_eq!(
4153 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4154 Some("jsdom")
4155 );
4156 }
4157
4158 #[test]
4159 fn switch_case_return_is_extracted() {
4160 let source = r#"
4161 import { defineConfig } from 'vite';
4162 export default defineConfig(({ mode }) => {
4163 switch (mode) {
4164 case "test":
4165 return { test: { environment: "jsdom" } };
4166 default:
4167 return { test: { environment: "node" } };
4168 }
4169 });
4170 "#;
4171 assert_eq!(
4172 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4173 Some("jsdom")
4174 );
4175 }
4176
4177 #[test]
4181 fn guard_clause_does_not_shadow_the_top_level_return() {
4182 let source = r#"
4183 import { defineConfig } from 'vite';
4184 export default defineConfig(({ mode }) => {
4185 if (!mode) {
4186 return {};
4187 }
4188 return { test: { environment: "jsdom" } };
4189 });
4190 "#;
4191 assert_eq!(
4192 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4193 Some("jsdom"),
4194 "a return at the callback's own level is the main config"
4195 );
4196 }
4197
4198 #[test]
4201 fn else_if_chain_does_not_shadow_the_top_level_return() {
4202 let source = r#"
4203 import { defineConfig } from 'vite';
4204 export default defineConfig(({ mode }) => {
4205 if (mode === "a") {
4206 return { base: "/a/" };
4207 } else if (mode === "b") {
4208 return { base: "/b/" };
4209 }
4210 return { test: { environment: "jsdom" } };
4211 });
4212 "#;
4213 assert_eq!(
4214 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
4215 Some("jsdom")
4216 );
4217 }
4218
4219 #[test]
4220 fn extract_config_from_default_export_function_declaration() {
4221 let source = r#"
4222 export default function createConfig() {
4223 return {
4224 clientModules: ["./src/client/global.js"]
4225 };
4226 }
4227 "#;
4228
4229 let client_modules = extract_config_string_array(source, &ts_path(), &["clientModules"]);
4230 assert_eq!(client_modules, vec!["./src/client/global.js"]);
4231 }
4232
4233 #[test]
4234 fn extract_config_from_default_export_async_function_declaration() {
4235 let source = r#"
4236 export default async function createConfigAsync() {
4237 return {
4238 docs: {
4239 path: "knowledge"
4240 }
4241 };
4242 }
4243 "#;
4244
4245 let docs_path = extract_config_string(source, &ts_path(), &["docs", "path"]);
4246 assert_eq!(docs_path, Some("knowledge".to_string()));
4247 }
4248
4249 #[test]
4250 fn extract_config_from_exported_arrow_function_identifier() {
4251 let source = r#"
4252 const config = async () => {
4253 return {
4254 themes: ["classic"]
4255 };
4256 };
4257
4258 export default config;
4259 "#;
4260
4261 let themes = extract_config_shallow_strings(source, &ts_path(), "themes");
4262 assert_eq!(themes, vec!["classic"]);
4263 }
4264
4265 #[test]
4266 fn module_exports_nested_string() {
4267 let source = r#"
4268 module.exports = {
4269 resolve: {
4270 alias: {
4271 "@": "./src"
4272 }
4273 }
4274 };
4275 "#;
4276 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
4277 assert_eq!(val, Some("./src".to_string()));
4278 }
4279
4280 #[test]
4281 fn property_strings_nested_objects() {
4282 let source = r#"
4283 export default {
4284 plugins: {
4285 group1: { a: "val-a" },
4286 group2: { b: "val-b" }
4287 }
4288 };
4289 "#;
4290 let values = extract_config_property_strings(source, &js_path(), "plugins");
4291 assert!(values.contains(&"val-a".to_string()));
4292 assert!(values.contains(&"val-b".to_string()));
4293 }
4294
4295 #[test]
4296 fn property_strings_missing_key_returns_empty() {
4297 let source = r#"export default { other: "value" };"#;
4298 let values = extract_config_property_strings(source, &js_path(), "missing");
4299 assert!(values.is_empty());
4300 }
4301
4302 #[test]
4303 fn shallow_strings_tuple_array() {
4304 let source = r#"
4305 module.exports = {
4306 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
4307 };
4308 "#;
4309 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
4310 assert_eq!(values, vec!["default", "jest-junit"]);
4311 assert!(!values.contains(&"reports".to_string()));
4312 }
4313
4314 #[test]
4315 fn shallow_strings_single_string() {
4316 let source = r#"export default { preset: "ts-jest" };"#;
4317 let values = extract_config_shallow_strings(source, &js_path(), "preset");
4318 assert_eq!(values, vec!["ts-jest"]);
4319 }
4320
4321 #[test]
4322 fn shallow_strings_missing_key() {
4323 let source = r#"export default { other: "val" };"#;
4324 let values = extract_config_shallow_strings(source, &js_path(), "missing");
4325 assert!(values.is_empty());
4326 }
4327
4328 #[test]
4329 fn shallow_strings_or_object_property_alias_objects() {
4330 let source = r#"
4331 export default {
4332 jsPlugins: [
4333 "eslint-plugin-playwright",
4334 ["eslint-plugin-regexp", { rules: {} }],
4335 { name: "short", specifier: "eslint-plugin-with-long-name" }
4336 ]
4337 };
4338 "#;
4339 let values = extract_config_shallow_strings_or_object_property(
4340 source,
4341 &ts_path(),
4342 "jsPlugins",
4343 "specifier",
4344 );
4345 assert_eq!(
4346 values,
4347 vec![
4348 "eslint-plugin-playwright",
4349 "eslint-plugin-regexp",
4350 "eslint-plugin-with-long-name"
4351 ]
4352 );
4353 }
4354
4355 #[test]
4356 fn nested_shallow_strings_vitest_reporters() {
4357 let source = r#"
4358 export default {
4359 test: {
4360 reporters: ["default", "vitest-sonar-reporter"]
4361 }
4362 };
4363 "#;
4364 let values =
4365 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4366 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
4367 }
4368
4369 #[test]
4370 fn nested_shallow_strings_tuple_format() {
4371 let source = r#"
4372 export default {
4373 test: {
4374 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
4375 }
4376 };
4377 "#;
4378 let values =
4379 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4380 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
4381 }
4382
4383 #[test]
4384 fn nested_shallow_strings_missing_outer() {
4385 let source = r"export default { other: {} };";
4386 let values =
4387 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4388 assert!(values.is_empty());
4389 }
4390
4391 #[test]
4392 fn nested_shallow_strings_missing_inner() {
4393 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
4394 let values =
4395 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
4396 assert!(values.is_empty());
4397 }
4398
4399 #[test]
4400 fn string_or_array_missing_path() {
4401 let source = r"export default {};";
4402 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
4403 assert!(result.is_empty());
4404 }
4405
4406 #[test]
4407 fn string_or_array_non_string_values() {
4408 let source = r"export default { entry: [42, true] };";
4409 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
4410 assert!(result.is_empty());
4411 }
4412
4413 #[test]
4414 fn array_nested_extraction() {
4415 let source = r#"
4416 export default defineConfig({
4417 test: {
4418 projects: [
4419 {
4420 test: {
4421 setupFiles: ["./test/setup-a.ts"]
4422 }
4423 },
4424 {
4425 test: {
4426 setupFiles: "./test/setup-b.ts"
4427 }
4428 }
4429 ]
4430 }
4431 });
4432 "#;
4433 let results = extract_config_array_nested_string_or_array(
4434 source,
4435 &ts_path(),
4436 &["test", "projects"],
4437 &["test", "setupFiles"],
4438 );
4439 assert!(results.contains(&"./test/setup-a.ts".to_string()));
4440 assert!(results.contains(&"./test/setup-b.ts".to_string()));
4441 }
4442
4443 #[test]
4444 fn array_nested_empty_when_no_array() {
4445 let source = r#"export default { test: { projects: "not-an-array" } };"#;
4446 let results = extract_config_array_nested_string_or_array(
4447 source,
4448 &js_path(),
4449 &["test", "projects"],
4450 &["test", "setupFiles"],
4451 );
4452 assert!(results.is_empty());
4453 }
4454
4455 #[test]
4456 fn object_nested_extraction() {
4457 let source = r#"{
4458 "projects": {
4459 "app-one": {
4460 "architect": {
4461 "build": {
4462 "options": {
4463 "styles": ["src/styles.css"]
4464 }
4465 }
4466 }
4467 }
4468 }
4469 }"#;
4470 let results = extract_config_object_nested_string_or_array(
4471 source,
4472 &json_path(),
4473 &["projects"],
4474 &["architect", "build", "options", "styles"],
4475 );
4476 assert_eq!(results, vec!["src/styles.css"]);
4477 }
4478
4479 #[test]
4480 fn array_with_object_input_form_extracted() {
4481 let source = r#"{
4482 "projects": {
4483 "app": {
4484 "architect": {
4485 "build": {
4486 "options": {
4487 "styles": [
4488 "src/styles.scss",
4489 { "input": "src/theme.scss", "bundleName": "theme", "inject": false },
4490 { "bundleName": "lazy-only" }
4491 ]
4492 }
4493 }
4494 }
4495 }
4496 }
4497 }"#;
4498 let results = extract_config_object_nested_string_or_array(
4499 source,
4500 &json_path(),
4501 &["projects"],
4502 &["architect", "build", "options", "styles"],
4503 );
4504 assert!(
4505 results.contains(&"src/styles.scss".to_string()),
4506 "string form must still work: {results:?}"
4507 );
4508 assert!(
4509 results.contains(&"src/theme.scss".to_string()),
4510 "object form with `input` must be extracted: {results:?}"
4511 );
4512 assert!(
4513 !results.contains(&"lazy-only".to_string()),
4514 "bundleName must not be misinterpreted as a path: {results:?}"
4515 );
4516 assert!(
4517 !results.contains(&"theme".to_string()),
4518 "bundleName from full object must not leak: {results:?}"
4519 );
4520 }
4521
4522 #[test]
4523 fn object_nested_strings_extraction() {
4524 let source = r#"{
4525 "targets": {
4526 "build": {
4527 "executor": "@angular/build:application"
4528 },
4529 "test": {
4530 "executor": "@nx/vite:test"
4531 }
4532 }
4533 }"#;
4534 let results =
4535 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
4536 assert!(results.contains(&"@angular/build:application".to_string()));
4537 assert!(results.contains(&"@nx/vite:test".to_string()));
4538 }
4539
4540 #[test]
4541 fn require_strings_direct_call() {
4542 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
4543 let deps = extract_config_require_strings(source, &js_path(), "adapter");
4544 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
4545 }
4546
4547 #[test]
4548 fn require_strings_no_matching_key() {
4549 let source = r"module.exports = { other: require('something') };";
4550 let deps = extract_config_require_strings(source, &js_path(), "plugins");
4551 assert!(deps.is_empty());
4552 }
4553
4554 #[test]
4555 fn extract_imports_no_imports() {
4556 let source = r"export default {};";
4557 let imports = extract_imports(source, &js_path());
4558 assert!(imports.is_empty());
4559 }
4560
4561 #[test]
4562 fn extract_imports_side_effect_import() {
4563 let source = r"
4564 import 'polyfill';
4565 import './local-setup';
4566 export default {};
4567 ";
4568 let imports = extract_imports(source, &js_path());
4569 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
4570 }
4571
4572 #[test]
4573 fn extract_imports_mixed_specifiers() {
4574 let source = r"
4575 import defaultExport from 'module-a';
4576 import { named } from 'module-b';
4577 import * as ns from 'module-c';
4578 export default {};
4579 ";
4580 let imports = extract_imports(source, &js_path());
4581 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
4582 }
4583
4584 #[test]
4585 fn template_literal_in_string_or_array() {
4586 let source = r"export default { entry: `./src/index.ts` };";
4587 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
4588 assert_eq!(result, vec!["./src/index.ts"]);
4589 }
4590
4591 #[test]
4592 fn template_literal_in_config_string() {
4593 let source = r"export default { testDir: `./tests` };";
4594 let val = extract_config_string(source, &js_path(), &["testDir"]);
4595 assert_eq!(val, Some("./tests".to_string()));
4596 }
4597
4598 #[test]
4599 fn template_literal_command_recovers_static_command_tokens() {
4600 let source = r"
4601 const PORT = 3000;
4602 export default {
4603 webServer: {
4604 command: `pnpm exec srvx --port ${PORT} --hostname 127.0.0.1`
4605 }
4606 };
4607 ";
4608 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
4609 assert_eq!(
4610 val,
4611 Some("pnpm exec srvx --port --hostname 127.0.0.1".to_string())
4612 );
4613 }
4614
4615 #[test]
4616 fn template_literal_command_skips_dynamic_prefix() {
4617 let source = r"
4618 export default {
4619 webServer: { command: `${serverCommand} && pnpm exec srvx` }
4620 };
4621 ";
4622 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
4623 assert!(val.is_none());
4624 }
4625
4626 #[test]
4627 fn template_literal_command_skips_split_static_token() {
4628 let source = r"
4629 export default {
4630 webServer: { command: `pnpm exec sr${part}vx --port 3000` }
4631 };
4632 ";
4633 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
4634 assert!(val.is_none());
4635 }
4636
4637 #[test]
4638 fn array_object_command_pairs_recover_template_command() {
4639 let source = r"
4640 const PORT = 3000;
4641 export default {
4642 webServer: [
4643 {
4644 command: `pnpm exec srvx --port ${PORT}`,
4645 cwd: 'apps/web'
4646 }
4647 ]
4648 };
4649 ";
4650 let pairs = extract_config_array_object_command_pairs(
4651 source,
4652 &ts_path(),
4653 &["webServer"],
4654 "command",
4655 "cwd",
4656 );
4657 assert_eq!(
4658 pairs,
4659 vec![(
4660 "pnpm exec srvx --port ".to_string(),
4661 Some("apps/web".to_string())
4662 )]
4663 );
4664 }
4665
4666 #[test]
4667 fn nested_string_array_empty_path() {
4668 let source = r#"export default { items: ["a", "b"] };"#;
4669 let result = extract_config_string_array(source, &js_path(), &[]);
4670 assert!(result.is_empty());
4671 }
4672
4673 #[test]
4674 fn nested_string_empty_path() {
4675 let source = r#"export default { key: "val" };"#;
4676 let result = extract_config_string(source, &js_path(), &[]);
4677 assert!(result.is_none());
4678 }
4679
4680 #[test]
4681 fn object_keys_empty_path() {
4682 let source = r"export default { plugins: {} };";
4683 let result = extract_config_object_keys(source, &js_path(), &[]);
4684 assert!(result.is_empty());
4685 }
4686
4687 #[test]
4688 fn no_config_object_returns_empty() {
4689 let source = r"const x = 42;";
4690 let result = extract_config_string(source, &js_path(), &["key"]);
4691 assert!(result.is_none());
4692
4693 let arr = extract_config_string_array(source, &js_path(), &["items"]);
4694 assert!(arr.is_empty());
4695
4696 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
4697 assert!(keys.is_empty());
4698 }
4699
4700 #[test]
4701 fn property_with_string_key() {
4702 let source = r#"export default { "string-key": "value" };"#;
4703 let val = extract_config_string(source, &js_path(), &["string-key"]);
4704 assert_eq!(val, Some("value".to_string()));
4705 }
4706
4707 #[test]
4708 fn nested_navigation_through_non_object() {
4709 let source = r#"export default { level1: "not-an-object" };"#;
4710 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
4711 assert!(val.is_none());
4712 }
4713
4714 #[test]
4715 fn variable_reference_untyped() {
4716 let source = r#"
4717 const config = {
4718 testDir: "./tests"
4719 };
4720 export default config;
4721 "#;
4722 let val = extract_config_string(source, &js_path(), &["testDir"]);
4723 assert_eq!(val, Some("./tests".to_string()));
4724 }
4725
4726 #[test]
4727 fn variable_reference_to_an_exported_const() {
4728 for source in [
4729 r#"
4730 export const config = { testDir: "./tests" };
4731 export default config;
4732 "#,
4733 r#"
4734 export const config = { testDir: "./tests" };
4735 module.exports = config;
4736 "#,
4737 ] {
4738 let val = extract_config_string(source, &js_path(), &["testDir"]);
4739 assert_eq!(val, Some("./tests".to_string()), "source: {source}");
4740 }
4741 }
4742
4743 #[test]
4744 fn variable_reference_with_type_annotation() {
4745 let source = r#"
4746 import type { StorybookConfig } from '@storybook/react-vite';
4747 const config: StorybookConfig = {
4748 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
4749 framework: "@storybook/react-vite"
4750 };
4751 export default config;
4752 "#;
4753 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
4754 assert_eq!(
4755 addons,
4756 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
4757 );
4758
4759 let framework = extract_config_string(source, &ts_path(), &["framework"]);
4760 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
4761 }
4762
4763 #[test]
4764 fn variable_reference_with_define_config() {
4765 let source = r#"
4766 import { defineConfig } from 'vitest/config';
4767 const config = defineConfig({
4768 test: {
4769 include: ["**/*.test.ts"]
4770 }
4771 });
4772 export default config;
4773 "#;
4774 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
4775 assert_eq!(include, vec!["**/*.test.ts"]);
4776 }
4777
4778 #[test]
4779 fn ts_satisfies_direct_export() {
4780 let source = r#"
4781 export default {
4782 testDir: "./tests"
4783 } satisfies PlaywrightTestConfig;
4784 "#;
4785 let val = extract_config_string(source, &ts_path(), &["testDir"]);
4786 assert_eq!(val, Some("./tests".to_string()));
4787 }
4788
4789 #[test]
4790 fn ts_as_direct_export() {
4791 let source = r#"
4792 export default {
4793 testDir: "./tests"
4794 } as const;
4795 "#;
4796 let val = extract_config_string(source, &ts_path(), &["testDir"]);
4797 assert_eq!(val, Some("./tests".to_string()));
4798 }
4799
4800 fn aliases(source: &str) -> Vec<(String, String)> {
4803 extract_config_aliases(source, &js_path(), &["resolve", "alias"])
4804 }
4805
4806 #[test]
4807 fn aliases_inline_object_still_extracted() {
4808 let source = r#"
4810 export default defineConfig({
4811 resolve: { alias: { "@": "./src", utils: "../../utils" } }
4812 });
4813 "#;
4814 let mut got = aliases(source);
4815 got.sort();
4816 assert_eq!(
4817 got,
4818 vec![
4819 ("@".to_string(), "./src".to_string()),
4820 ("utils".to_string(), "../../utils".to_string()),
4821 ]
4822 );
4823 }
4824
4825 #[test]
4826 fn aliases_inline_array_still_extracted() {
4827 let source = r#"
4828 export default defineConfig({
4829 resolve: { alias: [{ find: "@", replacement: "./src" }] }
4830 });
4831 "#;
4832 assert_eq!(
4833 aliases(source),
4834 vec![("@".to_string(), "./src".to_string())]
4835 );
4836 }
4837
4838 #[test]
4839 fn aliases_local_const_array_identifier() {
4840 let source = r#"
4841 const sharedAliases = [{ find: "@", replacement: "./src" }];
4842 export default defineConfig({ resolve: { alias: sharedAliases } });
4843 "#;
4844 assert_eq!(
4845 aliases(source),
4846 vec![("@".to_string(), "./src".to_string())]
4847 );
4848 }
4849
4850 #[test]
4851 fn aliases_local_const_object_identifier() {
4852 let source = r#"
4853 const sharedAliases = { "@": "./src" };
4854 export default defineConfig({ resolve: { alias: sharedAliases } });
4855 "#;
4856 assert_eq!(
4857 aliases(source),
4858 vec![("@".to_string(), "./src".to_string())]
4859 );
4860 }
4861
4862 #[test]
4863 fn aliases_array_spread_of_identifiers_and_inline() {
4864 let source = r##"
4865 const a = [{ find: "@", replacement: "./src" }];
4866 const b = [{ find: "~", replacement: "./lib" }];
4867 export default defineConfig({
4868 resolve: { alias: [...a, ...b, { find: "#", replacement: "./test" }] }
4869 });
4870 "##;
4871 let mut got = aliases(source);
4872 got.sort();
4873 assert_eq!(
4874 got,
4875 vec![
4876 ("#".to_string(), "./test".to_string()),
4877 ("@".to_string(), "./src".to_string()),
4878 ("~".to_string(), "./lib".to_string()),
4879 ]
4880 );
4881 }
4882
4883 #[test]
4884 fn aliases_object_spread_of_identifier_and_inline() {
4885 let source = r#"
4886 const base = { "@": "./src" };
4887 export default defineConfig({
4888 resolve: { alias: { ...base, "~": "./lib" } }
4889 });
4890 "#;
4891 let mut got = aliases(source);
4892 got.sort();
4893 assert_eq!(
4894 got,
4895 vec![
4896 ("@".to_string(), "./src".to_string()),
4897 ("~".to_string(), "./lib".to_string()),
4898 ]
4899 );
4900 }
4901
4902 #[test]
4903 fn aliases_local_const_chained_identifier() {
4904 let source = r#"
4906 const real = [{ find: "@", replacement: "./src" }];
4907 const alias2 = real;
4908 export default defineConfig({ resolve: { alias: alias2 } });
4909 "#;
4910 assert_eq!(
4911 aliases(source),
4912 vec![("@".to_string(), "./src".to_string())]
4913 );
4914 }
4915
4916 #[test]
4917 fn aliases_imported_named_identifier_from_sibling() {
4918 let dir = tempfile::tempdir().unwrap();
4919 std::fs::write(
4920 dir.path().join("vite.shared.js"),
4921 r#"export const sharedAliases = [
4922 { find: "@", replacement: new URL("./src", import.meta.url).pathname },
4923 ];"#,
4924 )
4925 .unwrap();
4926 let config = dir.path().join("vite.config.js");
4927 let source = r#"
4928 import { defineConfig } from "vite";
4929 import { sharedAliases } from "./vite.shared.js";
4930 export default defineConfig({ resolve: { alias: sharedAliases } });
4931 "#;
4932 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4933 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4934 }
4935
4936 #[test]
4937 fn aliases_imported_extensionless_specifier_probed() {
4938 let dir = tempfile::tempdir().unwrap();
4939 std::fs::write(
4940 dir.path().join("aliases.mjs"),
4941 r#"export const sharedAliases = { "@": "./src" };"#,
4942 )
4943 .unwrap();
4944 let config = dir.path().join("vite.config.ts");
4945 let source = r#"
4946 import { sharedAliases } from "./aliases";
4947 export default defineConfig({ resolve: { alias: sharedAliases } });
4948 "#;
4949 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4950 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4951 }
4952
4953 #[test]
4954 fn aliases_imported_default_export_from_sibling() {
4955 let dir = tempfile::tempdir().unwrap();
4956 std::fs::write(
4957 dir.path().join("aliases.js"),
4958 r#"export default [{ find: "@", replacement: "./src" }];"#,
4959 )
4960 .unwrap();
4961 let config = dir.path().join("vite.config.js");
4962 let source = r#"
4963 import sharedAliases from "./aliases.js";
4964 export default defineConfig({ resolve: { alias: sharedAliases } });
4965 "#;
4966 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4967 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4968 }
4969
4970 #[test]
4971 fn aliases_imported_spread_from_two_siblings() {
4972 let dir = tempfile::tempdir().unwrap();
4973 std::fs::write(
4974 dir.path().join("a.js"),
4975 r#"export const a = [{ find: "@", replacement: "./src" }];"#,
4976 )
4977 .unwrap();
4978 std::fs::write(
4979 dir.path().join("b.js"),
4980 r#"export const b = [{ find: "~", replacement: "./lib" }];"#,
4981 )
4982 .unwrap();
4983 let config = dir.path().join("vite.config.js");
4984 let source = r#"
4985 import { a } from "./a.js";
4986 import { b } from "./b.js";
4987 export default defineConfig({ resolve: { alias: [...a, ...b] } });
4988 "#;
4989 let mut got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4990 got.sort();
4991 assert_eq!(
4992 got,
4993 vec![
4994 ("@".to_string(), "./src".to_string()),
4995 ("~".to_string(), "./lib".to_string()),
4996 ]
4997 );
4998 }
4999
5000 #[test]
5001 fn aliases_import_cycle_terminates() {
5002 let dir = tempfile::tempdir().unwrap();
5005 std::fs::write(
5006 dir.path().join("a.js"),
5007 r#"import { b } from "./b.js";
5008 export const a = [{ find: "@", replacement: "./src" }, ...b];"#,
5009 )
5010 .unwrap();
5011 std::fs::write(
5012 dir.path().join("b.js"),
5013 r#"import { a } from "./a.js";
5014 export const b = [...a];"#,
5015 )
5016 .unwrap();
5017 let config = dir.path().join("vite.config.js");
5018 let source = r#"
5019 import { a } from "./a.js";
5020 export default defineConfig({ resolve: { alias: a } });
5021 "#;
5022 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
5023 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
5024 }
5025
5026 #[test]
5027 fn aliases_non_relative_import_not_followed() {
5028 let source = r#"
5031 import { sharedAliases } from "some-pkg";
5032 export default defineConfig({ resolve: { alias: sharedAliases } });
5033 "#;
5034 let dir = tempfile::tempdir().unwrap();
5035 let config = dir.path().join("vite.config.js");
5036 assert!(extract_config_aliases(source, &config, &["resolve", "alias"]).is_empty());
5037 }
5038
5039 #[test]
5040 fn aliases_object_array_value_takes_first_entry() {
5041 let source = r#"
5046 export default {
5047 compilerOptions: { paths: { "@/*": ["./src/*"], "~/*": ["./lib/*", "./vendor/*"] } }
5048 };
5049 "#;
5050 let mut got = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
5051 got.sort();
5052 assert_eq!(
5053 got,
5054 vec![
5055 ("@/*".to_string(), "./src/*".to_string()),
5056 ("~/*".to_string(), "./lib/*".to_string()),
5057 ]
5058 );
5059 }
5060
5061 #[test]
5062 fn aliases_kinded_preserves_is_bare_through_resolution() {
5063 let source = r#"
5066 const a = [{ find: "lodash-es", replacement: "lodash" }];
5067 export default defineConfig({
5068 resolve: { alias: [...a, { find: "@", replacement: "./src" }] }
5069 });
5070 "#;
5071 let mut got = extract_config_aliases_kinded(source, &js_path(), &["resolve", "alias"]);
5072 got.sort();
5073 assert_eq!(
5074 got,
5075 vec![
5076 ("@".to_string(), "./src".to_string(), false),
5077 ("lodash-es".to_string(), "lodash".to_string(), true),
5078 ]
5079 );
5080 }
5081
5082 #[test]
5083 fn aliases_kinded_preserves_is_bare_through_imported_spread() {
5084 let dir = tempfile::tempdir().unwrap();
5085 std::fs::write(
5086 dir.path().join("aliases.js"),
5087 r#"export const packageAliases = [{ find: "lodash-es", replacement: "lodash" }];"#,
5088 )
5089 .unwrap();
5090 let config = dir.path().join("vite.config.js");
5091 let source = r#"
5092 import { packageAliases } from "./aliases.js";
5093 export default defineConfig({
5094 resolve: { alias: [...packageAliases, { find: "@", replacement: "./src" }] }
5095 });
5096 "#;
5097 let mut got = extract_config_aliases_kinded(source, &config, &["resolve", "alias"]);
5098 got.sort();
5099 assert_eq!(
5100 got,
5101 vec![
5102 ("@".to_string(), "./src".to_string(), false),
5103 ("lodash-es".to_string(), "lodash".to_string(), true),
5104 ]
5105 );
5106 }
5107
5108 #[test]
5111 fn extract_command_string_literal() {
5112 let source = r#"export default { start: "node server.js" };"#;
5113 let val = extract_config_command(source, &js_path(), &["start"]);
5114 assert_eq!(val, Some("node server.js".to_string()));
5115 }
5116
5117 #[test]
5118 fn extract_command_nested_path() {
5119 let source = r#"
5120 export default {
5121 scripts: {
5122 dev: "vite dev"
5123 }
5124 };
5125 "#;
5126 let val = extract_config_command(source, &js_path(), &["scripts", "dev"]);
5127 assert_eq!(val, Some("vite dev".to_string()));
5128 }
5129
5130 #[test]
5131 fn extract_command_missing_key_returns_none() {
5132 let source = r#"export default { other: "val" };"#;
5133 let val = extract_config_command(source, &js_path(), &["start"]);
5134 assert!(val.is_none());
5135 }
5136
5137 #[test]
5138 fn extract_command_ts_as_expression() {
5139 let source = r#"export default { start: "node server.js" as string };"#;
5140 let val = extract_config_command(source, &ts_path(), &["start"]);
5141 assert_eq!(val, Some("node server.js".to_string()));
5142 }
5143
5144 #[test]
5145 fn extract_command_ts_satisfies_expression() {
5146 let source = r#"export default { start: "node server.js" satisfies string };"#;
5147 let val = extract_config_command(source, &ts_path(), &["start"]);
5148 assert_eq!(val, Some("node server.js".to_string()));
5149 }
5150
5151 #[test]
5152 fn extract_command_parenthesized_expression() {
5153 let source = r#"export default { start: ("node server.js") };"#;
5154 let val = extract_config_command(source, &js_path(), &["start"]);
5155 assert_eq!(val, Some("node server.js".to_string()));
5156 }
5157
5158 #[test]
5159 fn extract_command_empty_path_returns_none() {
5160 let source = r#"export default { start: "node server.js" };"#;
5161 let val = extract_config_command(source, &js_path(), &[]);
5162 assert!(val.is_none());
5163 }
5164
5165 #[test]
5168 fn truthy_bool_or_object_with_true_value() {
5169 let source = r"export default { typescript: true };";
5170 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5171 assert!(result);
5172 }
5173
5174 #[test]
5175 fn truthy_bool_or_object_with_false_value() {
5176 let source = r"export default { typescript: false };";
5177 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5178 assert!(!result);
5179 }
5180
5181 #[test]
5182 fn truthy_bool_or_object_with_object_value() {
5183 let source = r#"export default { typescript: { reactDocgen: "react-docgen" } };"#;
5184 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5185 assert!(result);
5186 }
5187
5188 #[test]
5189 fn truthy_bool_or_object_missing_key_returns_false() {
5190 let source = r"export default { other: true };";
5191 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5192 assert!(!result);
5193 }
5194
5195 #[test]
5196 fn truthy_bool_or_object_with_string_value_returns_false() {
5197 let source = r#"export default { typescript: "yes" };"#;
5199 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5200 assert!(!result);
5201 }
5202
5203 #[test]
5204 fn truthy_bool_or_object_ts_satisfies_wrapper() {
5205 let source = r"export default { typescript: (true satisfies boolean) };";
5206 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5207 assert!(result);
5208 }
5209
5210 #[test]
5211 fn truthy_bool_or_object_ts_as_wrapper() {
5212 let source = r"export default { typescript: (true as boolean) };";
5213 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5214 assert!(result);
5215 }
5216
5217 #[test]
5218 fn truthy_bool_or_object_parenthesized_wrapper() {
5219 let source = r"export default { typescript: (true) };";
5220 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
5221 assert!(result);
5222 }
5223
5224 #[test]
5230 fn static_dir_entries_object_form_exercises_property_string() {
5231 let source = r#"
5234 export default {
5235 staticDirs: [
5236 { from: "./media", to: "/assets" }
5237 ]
5238 };
5239 "#;
5240 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5241 assert_eq!(
5242 entries,
5243 vec![("./media".to_string(), Some("/assets".to_string()))]
5244 );
5245 }
5246
5247 #[test]
5250 fn expression_to_path_values_array_form_via_config_path() {
5251 let source = r#"export default { entries: ["./src/a.ts", "./src/b.ts"] };"#;
5254 let result = extract_config_string_or_array(source, &js_path(), &["entries"]);
5255 assert_eq!(result, vec!["./src/a.ts", "./src/b.ts"]);
5256 }
5257
5258 #[test]
5261 fn array_nested_aliases_object_form() {
5262 let source = r#"
5263 export default {
5264 test: {
5265 projects: [
5266 {
5267 resolve: {
5268 alias: { "@": "./src" }
5269 }
5270 }
5271 ]
5272 }
5273 };
5274 "#;
5275 let aliases = extract_config_array_nested_aliases_kinded(
5276 source,
5277 &ts_path(),
5278 &["test", "projects"],
5279 &["resolve", "alias"],
5280 );
5281 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
5282 }
5283
5284 #[test]
5285 fn array_nested_aliases_empty_when_path_is_not_array() {
5286 let source = r#"export default { test: { projects: "not-an-array" } };"#;
5287 let aliases = extract_config_array_nested_aliases_kinded(
5288 source,
5289 &ts_path(),
5290 &["test", "projects"],
5291 &["resolve", "alias"],
5292 );
5293 assert!(aliases.is_empty());
5294 }
5295
5296 #[test]
5297 fn array_nested_aliases_kinded_tracks_is_bare() {
5298 let source = r#"
5299 export default {
5300 projects: [
5301 {
5302 resolve: {
5303 alias: [
5304 { find: "lodash-es", replacement: "lodash" },
5305 { find: "@", replacement: "./src" }
5306 ]
5307 }
5308 }
5309 ]
5310 };
5311 "#;
5312 let mut aliases = extract_config_array_nested_aliases_kinded(
5313 source,
5314 &ts_path(),
5315 &["projects"],
5316 &["resolve", "alias"],
5317 );
5318 aliases.sort();
5319 assert_eq!(
5320 aliases,
5321 vec![
5322 ("@".to_string(), "./src".to_string(), false),
5323 ("lodash-es".to_string(), "lodash".to_string(), true),
5324 ]
5325 );
5326 }
5327
5328 #[test]
5331 fn default_export_array_aliases_kinded_extracts_from_workspace_config() {
5332 let source = r#"
5333 export default [
5334 {
5335 resolve: {
5336 alias: { "@": "./src" }
5337 }
5338 },
5339 {
5340 resolve: {
5341 alias: [{ find: "~", replacement: "./lib" }]
5342 }
5343 }
5344 ];
5345 "#;
5346 let mut aliases =
5347 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5348 aliases.sort();
5349 assert_eq!(
5350 aliases,
5351 vec![
5352 ("@".to_string(), "./src".to_string(), false),
5353 ("~".to_string(), "./lib".to_string(), false),
5354 ]
5355 );
5356 }
5357
5358 #[test]
5359 fn default_export_array_aliases_kinded_define_workspace_wrapper() {
5360 let source = r#"
5361 export default defineWorkspace([
5362 {
5363 resolve: { alias: { "@": "./src" } }
5364 }
5365 ]);
5366 "#;
5367 let aliases =
5368 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5369 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
5370 }
5371
5372 #[test]
5373 fn default_export_array_aliases_kinded_empty_when_no_alias_path() {
5374 let source = r#"
5375 export default [
5376 { test: { include: ["**/*.test.ts"] } }
5377 ];
5378 "#;
5379 let aliases =
5380 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5381 assert!(aliases.is_empty());
5382 }
5383
5384 #[test]
5387 fn config_default_export_unreachable_when_no_export() {
5388 let source = r"const x = 42;";
5389 assert!(config_default_export_unreachable(source, &js_path()));
5390 }
5391
5392 #[test]
5393 fn config_default_export_unreachable_false_for_object_export() {
5394 let source = r#"export default { key: "value" };"#;
5395 assert!(!config_default_export_unreachable(source, &js_path()));
5396 }
5397
5398 #[test]
5399 fn config_default_export_unreachable_false_for_array_export() {
5400 let source = r#"export default ["a", "b"];"#;
5401 assert!(!config_default_export_unreachable(source, &js_path()));
5402 }
5403
5404 #[test]
5405 fn config_default_export_unreachable_true_for_function_without_return_object() {
5406 let source = r"export default function config() { return 42; }";
5408 assert!(config_default_export_unreachable(source, &js_path()));
5409 }
5410
5411 #[test]
5414 fn static_dir_entries_string_and_object_form() {
5415 let source = r#"
5416 export default {
5417 staticDirs: [
5418 "./public",
5419 { from: "../assets", to: "/static" }
5420 ]
5421 };
5422 "#;
5423 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5424 assert_eq!(
5425 entries,
5426 vec![
5427 ("./public".to_string(), None),
5428 ("../assets".to_string(), Some("/static".to_string())),
5429 ]
5430 );
5431 }
5432
5433 #[test]
5434 fn static_dir_entries_object_without_to() {
5435 let source = r#"
5436 export default {
5437 staticDirs: [
5438 { from: "./media" }
5439 ]
5440 };
5441 "#;
5442 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5443 assert_eq!(entries, vec![("./media".to_string(), None)]);
5444 }
5445
5446 #[test]
5447 fn static_dir_entries_object_missing_from_skipped() {
5448 let source = r#"
5450 export default {
5451 staticDirs: [
5452 { to: "/target" },
5453 "./public"
5454 ]
5455 };
5456 "#;
5457 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5458 assert_eq!(entries, vec![("./public".to_string(), None)]);
5459 }
5460
5461 #[test]
5462 fn static_dir_entries_empty_when_not_array() {
5463 let source = r#"export default { staticDirs: "./public" };"#;
5464 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5465 assert!(entries.is_empty());
5466 }
5467
5468 #[test]
5471 fn aliases_array_form_missing_find_or_replacement_skipped() {
5472 let source = r#"
5474 export default {
5475 resolve: {
5476 alias: [
5477 { replacement: "./src" },
5478 { find: "@" },
5479 { find: "~", replacement: "./lib" }
5480 ]
5481 }
5482 };
5483 "#;
5484 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5485 assert_eq!(aliases, vec![("~".to_string(), "./lib".to_string())]);
5486 }
5487
5488 #[test]
5489 fn aliases_object_form_computed_key_skipped() {
5490 let source = r#"
5492 const k = "@";
5493 export default {
5494 resolve: {
5495 alias: {
5496 [k]: "./src",
5497 "~": "./lib"
5498 }
5499 }
5500 };
5501 "#;
5502 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5503 assert_eq!(aliases, vec![("~".to_string(), "./lib".to_string())]);
5505 }
5506
5507 #[test]
5508 fn aliases_kinded_array_form_path_replacement_is_not_bare() {
5509 let source = r#"
5510 export default {
5511 resolve: {
5512 alias: [{ find: "@", replacement: "./src" }]
5513 }
5514 };
5515 "#;
5516 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5517 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
5518 }
5519
5520 #[test]
5521 fn aliases_kinded_object_form_bare_and_path_discrimination() {
5522 let source = r#"
5523 export default {
5524 resolve: {
5525 alias: {
5526 "lodash-es": "lodash",
5527 "@": "./src"
5528 }
5529 }
5530 };
5531 "#;
5532 let mut aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5533 aliases.sort();
5534 assert_eq!(
5535 aliases,
5536 vec![
5537 ("@".to_string(), "./src".to_string(), false),
5538 ("lodash-es".to_string(), "lodash".to_string(), true),
5539 ]
5540 );
5541 }
5542
5543 #[test]
5544 fn aliases_kinded_parent_relative_replacement_is_not_bare() {
5545 let source = r#"
5546 export default {
5547 resolve: { alias: { "@": "../shared/src" } }
5548 };
5549 "#;
5550 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5551 assert_eq!(
5552 aliases,
5553 vec![("@".to_string(), "../shared/src".to_string(), false)]
5554 );
5555 }
5556
5557 #[test]
5558 fn aliases_kinded_absolute_replacement_is_not_bare() {
5559 let source = r#"
5560 export default {
5561 resolve: { alias: { "@": "/absolute/path" } }
5562 };
5563 "#;
5564 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5565 assert_eq!(
5566 aliases,
5567 vec![("@".to_string(), "/absolute/path".to_string(), false)]
5568 );
5569 }
5570
5571 #[test]
5574 fn default_export_array_ts_as_wrapper() {
5575 let source = r"export default [] as string[];";
5577 assert!(!config_default_export_unreachable(source, &js_path()));
5578 }
5579
5580 #[test]
5581 fn default_export_array_ts_satisfies_wrapper() {
5582 let source = r"export default [] satisfies string[];";
5583 assert!(!config_default_export_unreachable(source, &ts_path()));
5584 }
5585
5586 #[test]
5587 fn default_export_array_define_config_call_wrapper() {
5588 let source = r#"export default defineConfig(["**/*.test.ts"]);"#;
5589 assert!(!config_default_export_unreachable(source, &ts_path()));
5590 }
5591
5592 #[test]
5595 fn shallow_strings_object_with_string_values() {
5596 let source = r#"
5598 export default {
5599 plugins: {
5600 autoprefixer: "autoprefixer",
5601 tailwindcss: "tailwindcss"
5602 }
5603 };
5604 "#;
5605 let vals = extract_config_shallow_strings(source, &js_path(), "plugins");
5606 assert!(vals.contains(&"autoprefixer".to_string()));
5607 assert!(vals.contains(&"tailwindcss".to_string()));
5608 }
5609
5610 #[test]
5611 fn shallow_strings_object_with_sub_array_first_element() {
5612 let source = r#"
5614 export default {
5615 reporters: {
5616 main: ["jest-junit", { outputFile: "report.xml" }],
5617 alt: ["html-reporter"]
5618 }
5619 };
5620 "#;
5621 let vals = extract_config_shallow_strings(source, &js_path(), "reporters");
5622 assert!(vals.contains(&"jest-junit".to_string()));
5623 assert!(vals.contains(&"html-reporter".to_string()));
5624 }
5625
5626 #[test]
5629 fn shallow_strings_or_object_property_non_array_single_string() {
5630 let source = r#"export default { jsPlugins: "eslint-plugin-foo" };"#;
5632 let vals = extract_config_shallow_strings_or_object_property(
5633 source,
5634 &ts_path(),
5635 "jsPlugins",
5636 "specifier",
5637 );
5638 assert_eq!(vals, vec!["eslint-plugin-foo"]);
5639 }
5640
5641 #[test]
5642 fn shallow_strings_or_object_property_ts_satisfies_array_element() {
5643 let source = r#"
5645 export default {
5646 jsPlugins: [
5647 ("eslint-plugin-a" satisfies string)
5648 ]
5649 };
5650 "#;
5651 let vals = extract_config_shallow_strings_or_object_property(
5652 source,
5653 &ts_path(),
5654 "jsPlugins",
5655 "specifier",
5656 );
5657 assert_eq!(vals, vec!["eslint-plugin-a"]);
5658 }
5659
5660 #[test]
5661 fn shallow_strings_or_object_property_ts_as_array_element() {
5662 let source = r#"
5663 export default {
5664 jsPlugins: [
5665 ("eslint-plugin-b" as string)
5666 ]
5667 };
5668 "#;
5669 let vals = extract_config_shallow_strings_or_object_property(
5670 source,
5671 &ts_path(),
5672 "jsPlugins",
5673 "specifier",
5674 );
5675 assert_eq!(vals, vec!["eslint-plugin-b"]);
5676 }
5677
5678 #[test]
5679 fn shallow_strings_or_object_property_sub_array_first_element_string() {
5680 let source = r#"
5682 export default {
5683 jsPlugins: [
5684 ["eslint-plugin-tuple-pkg", { options: true }]
5685 ]
5686 };
5687 "#;
5688 let vals = extract_config_shallow_strings_or_object_property(
5689 source,
5690 &ts_path(),
5691 "jsPlugins",
5692 "specifier",
5693 );
5694 assert_eq!(vals, vec!["eslint-plugin-tuple-pkg"]);
5695 }
5696
5697 #[test]
5700 fn array_object_command_pairs_basic() {
5701 let source = r#"
5702 export default {
5703 webServer: [
5704 { command: "node server.js", cwd: "packages/api" },
5705 { command: "vite dev" }
5706 ]
5707 };
5708 "#;
5709 let pairs = extract_config_array_object_command_pairs(
5710 source,
5711 &ts_path(),
5712 &["webServer"],
5713 "command",
5714 "cwd",
5715 );
5716 assert_eq!(
5717 pairs,
5718 vec![
5719 (
5720 "node server.js".to_string(),
5721 Some("packages/api".to_string())
5722 ),
5723 ("vite dev".to_string(), None),
5724 ]
5725 );
5726 }
5727
5728 #[test]
5729 fn array_object_command_pairs_skips_missing_command() {
5730 let source = r#"
5731 export default {
5732 webServer: [
5733 { cwd: "packages/api" },
5734 { command: "vite dev", cwd: "apps/web" }
5735 ]
5736 };
5737 "#;
5738 let pairs = extract_config_array_object_command_pairs(
5739 source,
5740 &ts_path(),
5741 &["webServer"],
5742 "command",
5743 "cwd",
5744 );
5745 assert_eq!(
5746 pairs,
5747 vec![("vite dev".to_string(), Some("apps/web".to_string()))]
5748 );
5749 }
5750
5751 #[test]
5752 fn array_object_command_pairs_empty_when_not_array() {
5753 let source = r#"export default { webServer: { command: "vite dev" } };"#;
5754 let pairs = extract_config_array_object_command_pairs(
5755 source,
5756 &ts_path(),
5757 &["webServer"],
5758 "command",
5759 "cwd",
5760 );
5761 assert!(pairs.is_empty());
5762 }
5763
5764 #[test]
5767 fn normalize_config_path_empty_string_returns_none() {
5768 let config_path = PathBuf::from("/project/vite.config.ts");
5769 let root = PathBuf::from("/project");
5770 assert_eq!(normalize_config_path("", &config_path, &root), None);
5771 }
5772
5773 #[test]
5774 fn normalize_config_path_escapes_to_above_root_returns_none() {
5775 let config_path = PathBuf::from("/project/vite.config.ts");
5776 let root = PathBuf::from("/project");
5777 assert_eq!(
5779 normalize_config_path("../../etc", &config_path, &root),
5780 None
5781 );
5782 }
5783
5784 #[test]
5785 fn normalize_config_path_dot_slash_resolves_relative_to_config_dir() {
5786 let config_path = PathBuf::from("/project/packages/app/vite.config.ts");
5787 let root = PathBuf::from("/project");
5788 assert_eq!(
5789 normalize_config_path("./src", &config_path, &root),
5790 Some("packages/app/src".to_string())
5791 );
5792 }
5793
5794 #[test]
5797 fn json_config_array_of_arrays_via_shallow_strings() {
5798 let source = r#"{"reporters": ["default", ["jest-junit", {}]]}"#;
5800 let vals = extract_config_shallow_strings(source, &json_path(), "reporters");
5801 assert_eq!(vals, vec!["default", "jest-junit"]);
5802 }
5803
5804 #[test]
5807 fn extract_config_path_string_literal() {
5808 let source = r#"export default { outDir: "./dist" };"#;
5809 let path = extract_config_path(source, &js_path(), &["outDir"]);
5810 assert_eq!(
5811 path.map(|p| p.to_string_lossy().replace('\\', "/")),
5812 Some("./dist".to_string())
5813 );
5814 }
5815
5816 #[test]
5817 fn extract_config_path_with_resolve_call() {
5818 let source = r#"
5819 import { resolve } from "node:path";
5820 export default { outDir: resolve(__dirname, "dist") };
5821 "#;
5822 let path = extract_config_path(source, &js_path(), &["outDir"]);
5823 assert_eq!(
5824 path.map(|p| p.to_string_lossy().replace('\\', "/")),
5825 Some("dist".to_string())
5826 );
5827 }
5828
5829 #[test]
5830 fn extract_config_path_missing_key_returns_none() {
5831 let source = r#"export default { other: "val" };"#;
5832 let path = extract_config_path(source, &js_path(), &["outDir"]);
5833 assert!(path.is_none());
5834 }
5835
5836 #[test]
5839 fn extract_imports_and_requires_both_forms() {
5840 let source = r"
5841 import foo from 'foo-pkg';
5842 require('bar-pkg');
5843 export default {};
5844 ";
5845 let sources = extract_imports_and_requires(source, &js_path());
5846 assert!(sources.contains(&"foo-pkg".to_string()));
5847 assert!(sources.contains(&"bar-pkg".to_string()));
5848 }
5849
5850 #[test]
5851 fn extract_imports_and_requires_skips_non_require_calls() {
5852 let source = r"
5853 import foo from 'foo-pkg';
5854 someOtherCall('bar-pkg');
5855 export default {};
5856 ";
5857 let sources = extract_imports_and_requires(source, &js_path());
5858 assert_eq!(sources, vec!["foo-pkg"]);
5859 }
5860
5861 #[test]
5864 fn nested_shallow_strings_non_object_nested_returns_empty() {
5865 let source = r#"export default { test: "not-an-object" };"#;
5867 let vals =
5868 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
5869 assert!(vals.is_empty());
5870 }
5871
5872 #[test]
5875 fn vite_react_babel_dependencies_namespace_import() {
5876 let source = r#"
5877 import * as react from "@vitejs/plugin-react";
5878
5879 export default defineConfig({
5880 plugins: [
5881 react.default({
5882 babel: {
5883 plugins: ["babel-plugin-ns"],
5884 },
5885 }),
5886 ],
5887 });
5888 "#;
5889 let deps = extract_vite_plugin_option_dependencies(source, &ts_path());
5890 assert_eq!(deps, vec!["babel-plugin-ns".to_string()]);
5891 }
5892
5893 #[test]
5896 fn property_strings_deeply_nested_object_values() {
5897 let source = r#"
5899 export default {
5900 settings: {
5901 a: "val-a",
5902 b: {
5903 c: "val-c",
5904 d: ["val-d1", "val-d2"]
5905 }
5906 }
5907 };
5908 "#;
5909 let values = extract_config_property_strings(source, &js_path(), "settings");
5910 assert!(values.contains(&"val-a".to_string()));
5911 assert!(values.contains(&"val-c".to_string()));
5912 assert!(values.contains(&"val-d1".to_string()));
5913 assert!(values.contains(&"val-d2".to_string()));
5914 }
5915
5916 #[test]
5919 fn aliases_exported_const_form_resolves() {
5920 let source = r#"
5922 export const sharedAliases = { "@": "./src" };
5923 export default defineConfig({ resolve: { alias: sharedAliases } });
5924 "#;
5925 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5926 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
5927 }
5928
5929 #[test]
5932 fn aliases_imported_from_sibling_directory_index_file() {
5933 let dir = tempfile::tempdir().unwrap();
5936 let aliases_dir = dir.path().join("aliases");
5937 std::fs::create_dir_all(&aliases_dir).unwrap();
5938 std::fs::write(
5939 aliases_dir.join("index.js"),
5940 r#"export const aliases = [{ find: "@", replacement: "./src" }];"#,
5941 )
5942 .unwrap();
5943 let config = dir.path().join("vite.config.js");
5944 let source = r#"
5945 import { aliases } from "./aliases";
5946 export default defineConfig({ resolve: { alias: aliases } });
5947 "#;
5948 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
5949 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
5950 }
5951
5952 #[test]
5955 fn aliases_depth_limit_terminates_deep_chain() {
5956 let source = r#"
5959 const a9 = [{ find: "@", replacement: "./src" }];
5960 const a8 = a9;
5961 const a7 = a8;
5962 const a6 = a7;
5963 const a5 = a6;
5964 const a4 = a5;
5965 const a3 = a4;
5966 const a2 = a3;
5967 const a1 = a2;
5968 export default defineConfig({ resolve: { alias: a1 } });
5969 "#;
5970 let got = extract_config_aliases(source, &js_path(), &["resolve", "alias"]);
5972 let _ = got; }
5974
5975 #[test]
5978 fn extract_aliases_file_url_to_path_new_url() {
5979 let source = r#"
5981 import { fileURLToPath, URL } from 'node:url';
5982 export default {
5983 resolve: {
5984 alias: {
5985 "@": fileURLToPath(new URL("./src", import.meta.url))
5986 }
5987 }
5988 };
5989 "#;
5990 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5991 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
5992 }
5993
5994 #[test]
5995 fn extract_path_via_new_url_pathname_member() {
5996 let source = r#"
5998 export default {
5999 resolve: {
6000 alias: {
6001 "@": new URL("./src", import.meta.url).pathname
6002 }
6003 }
6004 };
6005 "#;
6006 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
6007 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
6008 }
6009
6010 #[test]
6013 fn truthy_bool_or_object_null_literal_returns_false() {
6014 let source = r"export default { typescript: null };";
6016 let result = extract_config_truthy_bool_or_object(source, &js_path(), &["typescript"]);
6017 assert!(!result);
6018 }
6019
6020 #[test]
6023 fn string_array_non_array_value_returns_empty() {
6024 let source = r#"export default { items: "not-an-array" };"#;
6025 let result = extract_config_string_array(source, &js_path(), &["items"]);
6026 assert!(result.is_empty());
6027 }
6028
6029 #[test]
6032 fn object_nested_empty_when_inner_value_is_not_object() {
6033 let source = r#"export default { targets: { build: "not-an-object" } };"#;
6035 let results =
6036 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
6037 assert!(results.is_empty());
6038 }
6039
6040 #[test]
6043 fn array_nested_string_or_array_missing_inner_path_returns_empty() {
6044 let source = r#"
6045 export default {
6046 test: {
6047 projects: [
6048 { test: { include: ["**/*.test.ts"] } }
6049 ]
6050 }
6051 };
6052 "#;
6053 let results = extract_config_array_nested_string_or_array(
6054 source,
6055 &ts_path(),
6056 &["test", "projects"],
6057 &["test", "setupFiles"],
6058 );
6059 assert!(results.is_empty());
6060 }
6061
6062 #[test]
6063 fn wrapped_named_const_default_export_resolves() {
6064 let source = r#"
6067 import createMDX from "@next/mdx";
6068 const nextConfig = { pageExtensions: ["ts", "tsx", "md", "mdx"] };
6069 const withMDX = createMDX({});
6070 export default withMDX(nextConfig);
6071 "#;
6072 let exts = extract_config_string_array(source, &ts_path(), &["pageExtensions"]);
6073 assert_eq!(exts, vec!["ts", "tsx", "md", "mdx"]);
6074 }
6075
6076 #[test]
6077 fn wrapped_named_const_module_exports_resolves() {
6078 let source = r#"
6080 const nextJest = require("next/jest");
6081 const createJestConfig = nextJest();
6082 const customConfig = { testMatch: ["**/*.test.ts"] };
6083 module.exports = createJestConfig(customConfig);
6084 "#;
6085 let matches = extract_config_string_array(source, &js_path(), &["testMatch"]);
6086 assert_eq!(matches, vec!["**/*.test.ts"]);
6087 }
6088
6089 #[test]
6090 fn wrapped_named_const_nested_and_curried_resolve() {
6091 let nested = r#"
6092 const nextConfig = { pageExtensions: ["mdx"] };
6093 const withMDX = (c) => c;
6094 const withFoo = (c) => c;
6095 export default withMDX(withFoo(nextConfig));
6096 "#;
6097 assert_eq!(
6098 extract_config_string_array(nested, &js_path(), &["pageExtensions"]),
6099 vec!["mdx"]
6100 );
6101
6102 let curried = r#"
6103 const nextConfig = { pageExtensions: ["md"] };
6104 const compose = (..._p) => (c) => c;
6105 export default compose(a, b)(nextConfig);
6106 "#;
6107 assert_eq!(
6108 extract_config_string_array(curried, &js_path(), &["pageExtensions"]),
6109 vec!["md"]
6110 );
6111 }
6112
6113 #[test]
6114 fn wrapped_inline_object_still_resolves() {
6115 let source = r#"
6117 const withMDX = createMDX({});
6118 export default withMDX({ pageExtensions: ["mdx"] });
6119 "#;
6120 assert_eq!(
6121 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
6122 vec!["mdx"]
6123 );
6124 }
6125}