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