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