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