1use std::path::{Path, PathBuf};
6
7use fallow_extract::visitor::extract_import_from_callable;
8use oxc_allocator::Allocator;
9#[allow(clippy::wildcard_imports, reason = "many AST types used")]
10use oxc_ast::ast::*;
11use oxc_parser::Parser;
12use oxc_span::SourceType;
13use rustc_hash::FxHashSet;
14
15#[must_use]
17pub fn extract_imports(source: &str, path: &Path) -> Vec<String> {
18 extract_from_source(source, path, |program| {
19 let mut sources = Vec::new();
20 for stmt in &program.body {
21 if let Statement::ImportDeclaration(decl) = stmt {
22 sources.push(decl.source.value.to_string());
23 }
24 }
25 Some(sources)
26 })
27 .unwrap_or_default()
28}
29
30#[must_use]
32pub fn extract_imports_and_requires(source: &str, path: &Path) -> Vec<String> {
33 extract_from_source(source, path, |program| {
34 let mut sources = Vec::new();
35 for stmt in &program.body {
36 match stmt {
37 Statement::ImportDeclaration(decl) => {
38 sources.push(decl.source.value.to_string());
39 }
40 Statement::ExpressionStatement(expr) => {
41 if let Expression::CallExpression(call) = &expr.expression
42 && is_require_call(call)
43 && let Some(s) = get_require_source(call)
44 {
45 sources.push(s);
46 }
47 }
48 _ => {}
49 }
50 }
51 Some(sources)
52 })
53 .unwrap_or_default()
54}
55
56#[must_use]
58pub fn extract_config_string_array(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
59 extract_from_source(source, path, |program| {
60 let obj = find_config_object(program)?;
61 get_nested_string_array_from_object(obj, prop_path)
62 })
63 .unwrap_or_default()
64}
65
66#[must_use]
68pub fn extract_config_string(source: &str, path: &Path, prop_path: &[&str]) -> Option<String> {
69 extract_from_source(source, path, |program| {
70 let obj = find_config_object(program)?;
71 get_nested_string_from_object(obj, prop_path)
72 })
73}
74
75#[must_use]
77pub fn extract_config_command(source: &str, path: &Path, prop_path: &[&str]) -> Option<String> {
78 extract_from_source(source, path, |program| {
79 let obj = find_config_object(program)?;
80 get_nested_command_from_object(obj, prop_path)
81 })
82}
83
84#[must_use]
87pub fn extract_config_property_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
88 extract_from_source(source, path, |program| {
89 let obj = find_config_object(program)?;
90 let mut values = Vec::new();
91 if let Some(prop) = find_property(obj, key) {
92 collect_all_string_values(&prop.value, &mut values);
93 }
94 Some(values)
95 })
96 .unwrap_or_default()
97}
98
99#[must_use]
101pub fn extract_config_shallow_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
102 extract_from_source(source, path, |program| {
103 let obj = find_config_object(program)?;
104 let prop = find_property(obj, key)?;
105 Some(collect_shallow_string_values(&prop.value))
106 })
107 .unwrap_or_default()
108}
109
110#[must_use]
112pub fn extract_config_shallow_strings_or_object_property(
113 source: &str,
114 path: &Path,
115 key: &str,
116 object_property: &str,
117) -> Vec<String> {
118 extract_from_source(source, path, |program| {
119 let obj = find_config_object(program)?;
120 let prop = find_property(obj, key)?;
121 Some(collect_shallow_string_or_object_property_values(
122 &prop.value,
123 object_property,
124 ))
125 })
126 .unwrap_or_default()
127}
128
129#[must_use]
131pub fn extract_config_nested_shallow_strings(
132 source: &str,
133 path: &Path,
134 outer_path: &[&str],
135 key: &str,
136) -> Vec<String> {
137 extract_from_source(source, path, |program| {
138 let obj = find_config_object(program)?;
139 let nested = get_nested_expression(obj, outer_path)?;
140 if let Expression::ObjectExpression(nested_obj) = nested {
141 let prop = find_property(nested_obj, key)?;
142 Some(collect_shallow_string_values(&prop.value))
143 } else {
144 None
145 }
146 })
147 .unwrap_or_default()
148}
149
150pub fn find_config_object_pub<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
152 find_config_object(program)
153}
154
155pub(crate) fn property_expr<'a>(
157 obj: &'a ObjectExpression<'a>,
158 key: &str,
159) -> Option<&'a Expression<'a>> {
160 find_property(obj, key).map(|prop| &prop.value)
161}
162
163pub(crate) fn property_object<'a>(
165 obj: &'a ObjectExpression<'a>,
166 key: &str,
167) -> Option<&'a ObjectExpression<'a>> {
168 property_expr(obj, key).and_then(object_expression)
169}
170
171pub(crate) fn property_string(obj: &ObjectExpression<'_>, key: &str) -> Option<String> {
173 property_expr(obj, key).and_then(expression_to_string)
174}
175
176pub(crate) fn object_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ObjectExpression<'a>> {
178 match expr {
179 Expression::ObjectExpression(obj) => Some(obj),
180 Expression::ParenthesizedExpression(paren) => object_expression(&paren.expression),
181 Expression::TSSatisfiesExpression(ts_sat) => object_expression(&ts_sat.expression),
182 Expression::TSAsExpression(ts_as) => object_expression(&ts_as.expression),
183 _ => None,
184 }
185}
186
187pub(crate) fn array_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
189 match expr {
190 Expression::ArrayExpression(arr) => Some(arr),
191 Expression::ParenthesizedExpression(paren) => array_expression(&paren.expression),
192 Expression::TSSatisfiesExpression(ts_sat) => array_expression(&ts_sat.expression),
193 Expression::TSAsExpression(ts_as) => array_expression(&ts_as.expression),
194 _ => None,
195 }
196}
197
198pub(crate) fn path_from_config_string(raw: &str) -> PathBuf {
201 PathBuf::from(raw.replace('\\', "/"))
202}
203
204pub(crate) fn path_to_config_string(path: &Path) -> String {
206 path.to_string_lossy().replace('\\', "/")
207}
208
209pub(crate) fn expression_to_path(expr: &Expression<'_>) -> Option<PathBuf> {
211 expression_to_path_string(expr).map(|path| path_from_config_string(&path))
212}
213
214pub(crate) fn expression_to_path_values(expr: &Expression<'_>) -> Vec<PathBuf> {
216 match expr {
217 Expression::ArrayExpression(arr) => arr
218 .elements
219 .iter()
220 .filter_map(|element| element.as_expression().and_then(expression_to_path))
221 .collect(),
222 _ => expression_to_path(expr).into_iter().collect(),
223 }
224}
225
226pub(crate) fn is_disabled_expression(expr: &Expression<'_>) -> bool {
228 matches!(expr, Expression::BooleanLiteral(boolean) if !boolean.value)
229 || matches!(expr, Expression::NullLiteral(_))
230}
231
232#[must_use]
234pub fn extract_config_truthy_bool_or_object(source: &str, path: &Path, prop_path: &[&str]) -> bool {
235 extract_from_source(source, path, |program| {
236 let obj = find_config_object(program)?;
237 let expr = get_nested_expression(obj, prop_path)?;
238 Some(is_truthy_bool_or_object(expr))
239 })
240 .unwrap_or(false)
241}
242
243fn is_truthy_bool_or_object(expr: &Expression<'_>) -> bool {
244 match expr {
245 Expression::BooleanLiteral(boolean) => boolean.value,
246 Expression::ObjectExpression(_) => true,
247 Expression::ParenthesizedExpression(paren) => is_truthy_bool_or_object(&paren.expression),
248 Expression::TSSatisfiesExpression(ts_sat) => is_truthy_bool_or_object(&ts_sat.expression),
249 Expression::TSAsExpression(ts_as) => is_truthy_bool_or_object(&ts_as.expression),
250 _ => false,
251 }
252}
253
254#[must_use]
256pub fn extract_config_object_keys(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
257 extract_from_source(source, path, |program| {
258 let obj = find_config_object(program)?;
259 get_nested_object_keys(obj, prop_path)
260 })
261 .unwrap_or_default()
262}
263
264#[must_use]
267pub fn extract_config_string_or_array(
268 source: &str,
269 path: &Path,
270 prop_path: &[&str],
271) -> Vec<String> {
272 extract_from_source(source, path, |program| {
273 let obj = find_config_object(program)?;
274 get_nested_string_or_array(obj, prop_path)
275 })
276 .unwrap_or_default()
277}
278
279#[must_use]
281pub fn extract_config_path(source: &str, path: &Path, prop_path: &[&str]) -> Option<PathBuf> {
282 extract_from_source(source, path, |program| {
283 let obj = find_config_object(program)?;
284 let expr = get_nested_expression(obj, prop_path)?;
285 expression_to_path(expr)
286 })
287}
288
289#[must_use]
291pub fn extract_config_array_nested_string_or_array(
292 source: &str,
293 path: &Path,
294 array_path: &[&str],
295 inner_path: &[&str],
296) -> Vec<String> {
297 extract_from_source(source, path, |program| {
298 let obj = find_config_object(program)?;
299 let array_expr = get_nested_expression(obj, array_path)?;
300 let Expression::ArrayExpression(arr) = array_expr else {
301 return None;
302 };
303 let mut results = Vec::new();
304 for element in &arr.elements {
305 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
306 && let Some(values) = get_nested_string_or_array(element_obj, inner_path)
307 {
308 results.extend(values);
309 }
310 }
311 if results.is_empty() {
312 None
313 } else {
314 Some(results)
315 }
316 })
317 .unwrap_or_default()
318}
319
320#[must_use]
322pub fn extract_config_object_nested_string_or_array(
323 source: &str,
324 path: &Path,
325 object_path: &[&str],
326 inner_path: &[&str],
327) -> Vec<String> {
328 extract_config_object_nested(source, path, object_path, |value_obj| {
329 get_nested_string_or_array(value_obj, inner_path)
330 })
331}
332
333#[must_use]
335pub fn extract_config_object_nested_strings(
336 source: &str,
337 path: &Path,
338 object_path: &[&str],
339 inner_path: &[&str],
340) -> Vec<String> {
341 extract_config_object_nested(source, path, object_path, |value_obj| {
342 get_nested_string_from_object(value_obj, inner_path).map(|s| vec![s])
343 })
344}
345
346fn extract_config_object_nested(
348 source: &str,
349 path: &Path,
350 object_path: &[&str],
351 extract_fn: impl Fn(&ObjectExpression<'_>) -> Option<Vec<String>>,
352) -> Vec<String> {
353 extract_from_source(source, path, |program| {
354 let obj = find_config_object(program)?;
355 let obj_expr = get_nested_expression(obj, object_path)?;
356 let Expression::ObjectExpression(target_obj) = obj_expr else {
357 return None;
358 };
359 let mut results = Vec::new();
360 for prop in &target_obj.properties {
361 if let ObjectPropertyKind::ObjectProperty(p) = prop
362 && let Expression::ObjectExpression(value_obj) = &p.value
363 && let Some(values) = extract_fn(value_obj)
364 {
365 results.extend(values);
366 }
367 }
368 if results.is_empty() {
369 None
370 } else {
371 Some(results)
372 }
373 })
374 .unwrap_or_default()
375}
376
377#[must_use]
379pub fn extract_config_require_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
380 extract_from_source(source, path, |program| {
381 let obj = find_config_object(program)?;
382 let prop = find_property(obj, key)?;
383 Some(collect_require_sources(&prop.value))
384 })
385 .unwrap_or_default()
386}
387
388#[must_use]
390pub fn extract_config_aliases(
391 source: &str,
392 path: &Path,
393 prop_path: &[&str],
394) -> Vec<(String, String)> {
395 extract_config_aliases_kinded(source, path, prop_path)
396 .into_iter()
397 .map(|(find, replacement, _is_bare)| (find, replacement))
398 .collect()
399}
400
401#[must_use]
403pub fn extract_config_path_aliases(
404 source: &str,
405 path: &Path,
406 prop_path: &[&str],
407) -> Vec<(String, PathBuf)> {
408 extract_config_aliases_kinded(source, path, prop_path)
409 .into_iter()
410 .map(|(find, replacement, _is_bare)| (find, path_from_config_string(&replacement)))
411 .collect()
412}
413
414#[must_use]
416pub fn extract_config_array_nested_aliases(
417 source: &str,
418 path: &Path,
419 array_path: &[&str],
420 alias_path: &[&str],
421) -> Vec<(String, String)> {
422 extract_from_source(source, path, |program| {
423 let obj = find_config_object(program)?;
424 let array_expr = get_nested_expression(obj, array_path)?;
425 let Expression::ArrayExpression(arr) = array_expr else {
426 return None;
427 };
428 let mut results = Vec::new();
429 for element in &arr.elements {
430 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
431 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
432 {
433 results.extend(expression_to_alias_pairs(alias_expr));
434 }
435 }
436 (!results.is_empty()).then_some(results)
437 })
438 .unwrap_or_default()
439}
440
441#[must_use]
443pub fn extract_config_aliases_kinded(
444 source: &str,
445 path: &Path,
446 prop_path: &[&str],
447) -> Vec<(String, String, bool)> {
448 extract_from_source(source, path, |program| {
449 let obj = find_config_object(program)?;
450 let expr = get_nested_expression(obj, prop_path)?;
451 let mut visited = FxHashSet::default();
452 let aliases = resolve_alias_pairs_kinded(program, path, expr, &mut visited, 0);
453 (!aliases.is_empty()).then_some(aliases)
454 })
455 .unwrap_or_default()
456}
457
458#[must_use]
460pub fn extract_config_array_nested_aliases_kinded(
461 source: &str,
462 path: &Path,
463 array_path: &[&str],
464 alias_path: &[&str],
465) -> Vec<(String, String, bool)> {
466 extract_from_source(source, path, |program| {
467 let obj = find_config_object(program)?;
468 let array_expr = get_nested_expression(obj, array_path)?;
469 let Expression::ArrayExpression(arr) = array_expr else {
470 return None;
471 };
472 let mut results = Vec::new();
473 for element in &arr.elements {
474 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
475 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
476 {
477 results.extend(expression_to_alias_pairs_kinded(alias_expr));
478 }
479 }
480 (!results.is_empty()).then_some(results)
481 })
482 .unwrap_or_default()
483}
484
485#[must_use]
487pub fn extract_default_export_array_aliases_kinded(
488 source: &str,
489 path: &Path,
490 alias_path: &[&str],
491) -> Vec<(String, String, bool)> {
492 extract_from_source(source, path, |program| {
493 let arr = find_default_export_array(program)?;
494 let mut results = Vec::new();
495 for element in &arr.elements {
496 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
497 && let Some(alias_expr) = get_nested_expression(element_obj, alias_path)
498 {
499 results.extend(expression_to_alias_pairs_kinded(alias_expr));
500 }
501 }
502 (!results.is_empty()).then_some(results)
503 })
504 .unwrap_or_default()
505}
506
507#[must_use]
509pub fn config_default_export_unreachable(source: &str, path: &Path) -> bool {
510 extract_from_source(source, path, |program| {
511 let reachable =
512 find_config_object(program).is_some() || find_default_export_array(program).is_some();
513 Some(reachable)
514 })
515 .is_some_and(|reachable| !reachable)
516}
517
518#[must_use]
524pub fn extract_config_array_object_strings(
525 source: &str,
526 path: &Path,
527 array_path: &[&str],
528 key: &str,
529) -> Vec<String> {
530 extract_from_source(source, path, |program| {
531 let obj = find_config_object(program)?;
532 let array_expr = get_nested_expression(obj, array_path)?;
533 let Expression::ArrayExpression(arr) = array_expr else {
534 return None;
535 };
536
537 let mut results = Vec::new();
538 for element in &arr.elements {
539 let Some(expr) = element.as_expression() else {
540 continue;
541 };
542 match expr {
543 Expression::ObjectExpression(item) => {
544 if let Some(prop) = find_property(item, key)
545 && let Some(value) = expression_to_path_string(&prop.value)
546 {
547 results.push(value);
548 }
549 }
550 _ => {
551 if let Some(value) = expression_to_path_string(expr) {
552 results.push(value);
553 }
554 }
555 }
556 }
557
558 (!results.is_empty()).then_some(results)
559 })
560 .unwrap_or_default()
561}
562
563#[must_use]
568pub fn extract_config_static_dir_entries(
569 source: &str,
570 path: &Path,
571 array_path: &[&str],
572) -> Vec<(String, Option<String>)> {
573 extract_from_source(source, path, |program| {
574 let obj = find_config_object(program)?;
575 let array_expr = get_nested_expression(obj, array_path)?;
576 let Expression::ArrayExpression(arr) = array_expr else {
577 return None;
578 };
579
580 let mut results = Vec::new();
581 for element in &arr.elements {
582 let Some(expr) = element.as_expression() else {
583 continue;
584 };
585 match expr {
586 Expression::ObjectExpression(item) => {
587 if let Some(from) = property_string(item, "from") {
588 let to = property_string(item, "to");
589 results.push((from, to));
590 }
591 }
592 _ => {
593 if let Some(from) = expression_to_path_string(expr) {
594 results.push((from, None));
595 }
596 }
597 }
598 }
599
600 (!results.is_empty()).then_some(results)
601 })
602 .unwrap_or_default()
603}
604
605#[must_use]
616pub fn extract_config_array_object_string_pairs(
617 source: &str,
618 path: &Path,
619 array_path: &[&str],
620 primary_key: &str,
621 secondary_key: &str,
622) -> Vec<(String, Option<String>)> {
623 extract_from_source(source, path, |program| {
624 let obj = find_config_object(program)?;
625 let array_expr = get_nested_expression(obj, array_path)?;
626 let Expression::ArrayExpression(arr) = array_expr else {
627 return None;
628 };
629
630 let mut results = Vec::new();
631 for element in &arr.elements {
632 let Some(Expression::ObjectExpression(item)) = element.as_expression() else {
633 continue;
634 };
635 let Some(primary) = find_property(item, primary_key)
636 .and_then(|prop| expression_to_path_string(&prop.value))
637 else {
638 continue;
639 };
640 let secondary = find_property(item, secondary_key)
641 .and_then(|prop| expression_to_path_string(&prop.value));
642 results.push((primary, secondary));
643 }
644
645 (!results.is_empty()).then_some(results)
646 })
647 .unwrap_or_default()
648}
649
650#[must_use]
652pub fn extract_config_array_object_command_pairs(
653 source: &str,
654 path: &Path,
655 array_path: &[&str],
656 primary_key: &str,
657 secondary_key: &str,
658) -> Vec<(String, Option<String>)> {
659 extract_from_source(source, path, |program| {
660 let obj = find_config_object(program)?;
661 let array_expr = get_nested_expression(obj, array_path)?;
662 let Expression::ArrayExpression(arr) = array_expr else {
663 return None;
664 };
665
666 let mut results = Vec::new();
667 for element in &arr.elements {
668 let Some(Expression::ObjectExpression(item)) = element.as_expression() else {
669 continue;
670 };
671 let Some(primary) = find_property(item, primary_key)
672 .and_then(|prop| expression_to_command(&prop.value))
673 else {
674 continue;
675 };
676 let secondary = find_property(item, secondary_key)
677 .and_then(|prop| expression_to_path_string(&prop.value));
678 results.push((primary, secondary));
679 }
680
681 (!results.is_empty()).then_some(results)
682 })
683 .unwrap_or_default()
684}
685
686#[must_use]
732pub fn extract_lazy_imports_in_array(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
733 extract_from_source(source, path, |program| {
734 let obj = find_config_object(program)?;
735 let array_expr = get_nested_expression(obj, prop_path)?;
736 let Expression::ArrayExpression(arr) = array_expr else {
737 return None;
738 };
739 let mut specs = Vec::new();
740 for element in &arr.elements {
741 let Some(expr) = element.as_expression() else {
742 continue;
743 };
744 if let Some(spec) = lazy_import_specifier(expr) {
745 specs.push(spec);
746 }
747 }
748 (!specs.is_empty()).then_some(specs)
749 })
750 .unwrap_or_default()
751}
752
753fn lazy_import_specifier(expr: &Expression<'_>) -> Option<String> {
766 let callable = match expr {
767 Expression::ObjectExpression(obj) => &find_property(obj, "file")?.value,
768 _ => expr,
769 };
770 let import_expr = extract_import_from_callable(callable)?;
771 expression_to_string(&import_expr.source)
772}
773
774#[must_use]
781pub fn extract_config_plugin_option_string(
782 source: &str,
783 path: &Path,
784 plugins_path: &[&str],
785 plugin_name: &str,
786 option_key: &str,
787) -> Option<String> {
788 extract_from_source(source, path, |program| {
789 let obj = find_config_object(program)?;
790 let plugins_expr = get_nested_expression(obj, plugins_path)?;
791 let Expression::ArrayExpression(plugins) = plugins_expr else {
792 return None;
793 };
794
795 for entry in &plugins.elements {
796 let Some(Expression::ArrayExpression(tuple)) = entry.as_expression() else {
797 continue;
798 };
799 let Some(plugin_expr) = tuple
800 .elements
801 .first()
802 .and_then(ArrayExpressionElement::as_expression)
803 else {
804 continue;
805 };
806 if expression_to_string(plugin_expr).as_deref() != Some(plugin_name) {
807 continue;
808 }
809
810 let Some(options_expr) = tuple
811 .elements
812 .get(1)
813 .and_then(ArrayExpressionElement::as_expression)
814 else {
815 continue;
816 };
817 let Expression::ObjectExpression(options_obj) = options_expr else {
818 continue;
819 };
820 let option = find_property(options_obj, option_key)?;
821 return expression_to_path_string(&option.value);
822 }
823
824 None
825 })
826}
827
828#[must_use]
830pub fn extract_config_plugin_option_string_from_paths(
831 source: &str,
832 path: &Path,
833 plugin_paths: &[&[&str]],
834 plugin_name: &str,
835 option_key: &str,
836) -> Option<String> {
837 plugin_paths.iter().find_map(|plugins_path| {
838 extract_config_plugin_option_string(source, path, plugins_path, plugin_name, option_key)
839 })
840}
841
842#[must_use]
845pub fn extract_vite_react_babel_dependencies(source: &str, path: &Path) -> Vec<String> {
846 extract_from_source(source, path, |program| {
847 let react_plugin_imports = collect_vite_react_plugin_imports(program);
848 if react_plugin_imports.is_empty() {
849 return None;
850 }
851
852 let obj = find_config_object(program)?;
853 let plugins = get_nested_expression(obj, &["plugins"])?;
854 let Expression::ArrayExpression(plugin_array) = plugins else {
855 return None;
856 };
857
858 let mut deps = Vec::new();
859 for element in &plugin_array.elements {
860 let Some(Expression::CallExpression(call)) = element.as_expression() else {
861 continue;
862 };
863 if !is_vite_react_plugin_call(call, &react_plugin_imports) {
864 continue;
865 }
866 let Some(Expression::ObjectExpression(options)) =
867 call.arguments.first().and_then(Argument::as_expression)
868 else {
869 continue;
870 };
871 collect_vite_react_babel_dependencies(options, &mut deps);
872 }
873
874 (!deps.is_empty()).then_some(deps)
875 })
876 .unwrap_or_default()
877}
878
879#[must_use]
884pub fn normalize_config_path_buf(
885 raw: impl AsRef<Path>,
886 config_path: &Path,
887 root: &Path,
888) -> Option<PathBuf> {
889 let raw = raw.as_ref();
890 if raw.as_os_str().is_empty() {
891 return None;
892 }
893
894 let raw_string = path_to_config_string(raw);
895 let raw_path = Path::new(&raw_string);
896 let candidate = if let Some(stripped) = raw_string.strip_prefix('/') {
897 lexical_normalize(&root.join(stripped))
898 } else if raw_path.is_absolute() {
899 lexical_normalize(raw_path)
900 } else {
901 let base = config_path.parent().unwrap_or(root);
902 lexical_normalize(&base.join(raw_path))
903 };
904
905 let relative = candidate.strip_prefix(root).ok()?;
906 (!relative.as_os_str().is_empty()).then(|| relative.to_path_buf())
907}
908
909#[must_use]
911pub fn normalize_config_path(
912 raw: impl AsRef<Path>,
913 config_path: &Path,
914 root: &Path,
915) -> Option<String> {
916 normalize_config_path_buf(raw, config_path, root).map(|path| path_to_config_string(&path))
917}
918
919pub(crate) fn extract_from_source<T>(
926 source: &str,
927 path: &Path,
928 extractor: impl FnOnce(&Program) -> Option<T>,
929) -> Option<T> {
930 let source_type = SourceType::from_path(path).unwrap_or_default();
931 let alloc = Allocator::default();
932
933 let is_json = path
934 .extension()
935 .is_some_and(|ext| ext == "json" || ext == "jsonc");
936 if is_json {
937 let wrapped = format!("({source})");
938 let parsed = Parser::new(&alloc, &wrapped, SourceType::mjs()).parse();
939 return extractor(&parsed.program);
940 }
941
942 let parsed = Parser::new(&alloc, source, source_type).parse();
943 extractor(&parsed.program)
944}
945
946#[derive(Default)]
947struct ViteReactPluginImports {
948 callables: Vec<String>,
949 namespaces: Vec<String>,
950}
951
952impl ViteReactPluginImports {
953 fn is_empty(&self) -> bool {
954 self.callables.is_empty() && self.namespaces.is_empty()
955 }
956}
957
958fn collect_vite_react_plugin_imports(program: &Program<'_>) -> ViteReactPluginImports {
959 let mut imports = ViteReactPluginImports::default();
960
961 for stmt in &program.body {
962 let Statement::ImportDeclaration(decl) = stmt else {
963 continue;
964 };
965 if decl.source.value != "@vitejs/plugin-react" {
966 continue;
967 }
968 let Some(specifiers) = &decl.specifiers else {
969 continue;
970 };
971 for specifier in specifiers {
972 match specifier {
973 ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
974 push_unique_string(&mut imports.callables, specifier.local.name.to_string());
975 }
976 ImportDeclarationSpecifier::ImportSpecifier(specifier)
977 if specifier.imported.name().as_ref() == "default" =>
978 {
979 push_unique_string(&mut imports.callables, specifier.local.name.to_string());
980 }
981 ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
982 push_unique_string(&mut imports.namespaces, specifier.local.name.to_string());
983 }
984 ImportDeclarationSpecifier::ImportSpecifier(_) => {}
985 }
986 }
987 }
988
989 imports
990}
991
992fn is_vite_react_plugin_call(call: &CallExpression<'_>, imports: &ViteReactPluginImports) -> bool {
993 match &call.callee {
994 Expression::Identifier(identifier) => imports
995 .callables
996 .iter()
997 .any(|name| name == identifier.name.as_str()),
998 Expression::StaticMemberExpression(member) if matches!(&member.object, Expression::Identifier(object) if imports.namespaces.iter().any(|name| name == object.name.as_str())) => {
999 member.property.name == "default"
1000 }
1001 _ => false,
1002 }
1003}
1004
1005fn collect_vite_react_babel_dependencies(options: &ObjectExpression<'_>, deps: &mut Vec<String>) {
1006 let Some(babel) = property_object(options, "babel") else {
1007 return;
1008 };
1009 for key in ["plugins", "presets"] {
1010 let Some(prop) = find_property(babel, key) else {
1011 continue;
1012 };
1013 for raw in collect_shallow_string_values(&prop.value) {
1014 if let Some(dep) = vite_react_babel_dependency_name(&raw) {
1015 push_unique_string(deps, dep);
1016 }
1017 }
1018 }
1019}
1020
1021fn vite_react_babel_dependency_name(raw: &str) -> Option<String> {
1022 let raw = raw.trim();
1023 let specifier = raw.strip_prefix("module:").unwrap_or(raw).trim();
1024 if specifier.is_empty()
1025 || specifier.starts_with('.')
1026 || specifier.starts_with('/')
1027 || specifier.contains(':')
1028 || specifier.contains('\\')
1029 {
1030 return None;
1031 }
1032 Some(crate::resolve::extract_package_name(specifier))
1033}
1034
1035fn push_unique_string(items: &mut Vec<String>, value: String) {
1036 if !items.contains(&value) {
1037 items.push(value);
1038 }
1039}
1040
1041fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
1053 for stmt in &program.body {
1054 match stmt {
1055 Statement::ExportDefaultDeclaration(decl) => {
1056 let expr: Option<&Expression> = match &decl.declaration {
1057 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
1058 return Some(obj);
1059 }
1060 ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
1061 return extract_object_from_function(func);
1062 }
1063 _ => decl.declaration.as_expression(),
1064 };
1065 if let Some(expr) = expr {
1066 if let Some(obj) = extract_object_from_expression(expr) {
1067 return Some(obj);
1068 }
1069 if let Some(name) = unwrap_to_identifier_name(expr) {
1070 return find_variable_init_object(program, name);
1071 }
1072 }
1073 }
1074 Statement::ExpressionStatement(expr_stmt) => {
1075 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
1076 && is_module_exports_target(&assign.left)
1077 {
1078 return extract_object_from_expression(&assign.right);
1079 }
1080 }
1081 _ => {}
1082 }
1083 }
1084
1085 if program.body.len() == 1
1086 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
1087 {
1088 match &expr_stmt.expression {
1089 Expression::ObjectExpression(obj) => return Some(obj),
1090 Expression::ParenthesizedExpression(paren) => {
1091 if let Expression::ObjectExpression(obj) = &paren.expression {
1092 return Some(obj);
1093 }
1094 }
1095 _ => {}
1096 }
1097 }
1098
1099 None
1100}
1101
1102fn extract_object_from_expression<'a>(
1104 expr: &'a Expression<'a>,
1105) -> Option<&'a ObjectExpression<'a>> {
1106 match expr {
1107 Expression::ObjectExpression(obj) => Some(obj),
1108 Expression::CallExpression(call) => {
1109 for arg in &call.arguments {
1110 match arg {
1111 Argument::ObjectExpression(obj) => return Some(obj),
1112 Argument::ArrowFunctionExpression(arrow) => {
1113 if arrow.expression
1114 && !arrow.body.statements.is_empty()
1115 && let Statement::ExpressionStatement(expr_stmt) =
1116 &arrow.body.statements[0]
1117 {
1118 return extract_object_from_expression(&expr_stmt.expression);
1119 }
1120 }
1121 _ => {}
1122 }
1123 }
1124 None
1125 }
1126 Expression::ParenthesizedExpression(paren) => {
1127 extract_object_from_expression(&paren.expression)
1128 }
1129 Expression::TSSatisfiesExpression(ts_sat) => {
1130 extract_object_from_expression(&ts_sat.expression)
1131 }
1132 Expression::TSAsExpression(ts_as) => extract_object_from_expression(&ts_as.expression),
1133 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
1134 Expression::FunctionExpression(func) => extract_object_from_function(func),
1135 _ => None,
1136 }
1137}
1138
1139fn extract_object_from_arrow_function<'a>(
1140 arrow: &'a ArrowFunctionExpression<'a>,
1141) -> Option<&'a ObjectExpression<'a>> {
1142 if arrow.expression {
1143 arrow.body.statements.first().and_then(|stmt| {
1144 if let Statement::ExpressionStatement(expr_stmt) = stmt {
1145 extract_object_from_expression(&expr_stmt.expression)
1146 } else {
1147 None
1148 }
1149 })
1150 } else {
1151 extract_object_from_function_body(&arrow.body)
1152 }
1153}
1154
1155fn extract_object_from_function<'a>(func: &'a Function<'a>) -> Option<&'a ObjectExpression<'a>> {
1156 func.body
1157 .as_ref()
1158 .and_then(|body| extract_object_from_function_body(body))
1159}
1160
1161fn extract_object_from_function_body<'a>(
1162 body: &'a FunctionBody<'a>,
1163) -> Option<&'a ObjectExpression<'a>> {
1164 for stmt in &body.statements {
1165 if let Statement::ReturnStatement(ret) = stmt
1166 && let Some(argument) = &ret.argument
1167 && let Some(obj) = extract_object_from_expression(argument)
1168 {
1169 return Some(obj);
1170 }
1171 }
1172 None
1173}
1174
1175fn is_module_exports_target(target: &AssignmentTarget) -> bool {
1177 if let AssignmentTarget::StaticMemberExpression(member) = target
1178 && let Expression::Identifier(obj) = &member.object
1179 {
1180 return obj.name == "module" && member.property.name == "exports";
1181 }
1182 false
1183}
1184
1185fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
1189 match expr {
1190 Expression::Identifier(id) => Some(&id.name),
1191 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
1192 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
1193 _ => None,
1194 }
1195}
1196
1197fn find_variable_init_object<'a>(
1202 program: &'a Program,
1203 name: &str,
1204) -> Option<&'a ObjectExpression<'a>> {
1205 for stmt in &program.body {
1206 if let Statement::VariableDeclaration(decl) = stmt {
1207 for declarator in &decl.declarations {
1208 if let BindingPattern::BindingIdentifier(id) = &declarator.id
1209 && id.name == name
1210 && let Some(init) = &declarator.init
1211 {
1212 return extract_object_from_expression(init);
1213 }
1214 }
1215 }
1216 }
1217 None
1218}
1219
1220pub(crate) fn find_property<'a>(
1222 obj: &'a ObjectExpression<'a>,
1223 key: &str,
1224) -> Option<&'a ObjectProperty<'a>> {
1225 for prop in &obj.properties {
1226 if let ObjectPropertyKind::ObjectProperty(p) = prop
1227 && property_key_matches(&p.key, key)
1228 {
1229 return Some(p);
1230 }
1231 }
1232 None
1233}
1234
1235pub(crate) fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
1237 match key {
1238 PropertyKey::StaticIdentifier(id) => id.name == name,
1239 PropertyKey::StringLiteral(s) => s.value == name,
1240 _ => false,
1241 }
1242}
1243
1244fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
1246 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
1247}
1248
1249fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
1251 find_property(obj, key)
1252 .map(|p| expression_to_string_array(&p.value))
1253 .unwrap_or_default()
1254}
1255
1256fn get_nested_string_array_from_object(
1258 obj: &ObjectExpression,
1259 path: &[&str],
1260) -> Option<Vec<String>> {
1261 if path.is_empty() {
1262 return None;
1263 }
1264 if path.len() == 1 {
1265 return Some(get_object_string_array_property(obj, path[0]));
1266 }
1267 let prop = find_property(obj, path[0])?;
1268 if let Expression::ObjectExpression(nested) = &prop.value {
1269 get_nested_string_array_from_object(nested, &path[1..])
1270 } else {
1271 None
1272 }
1273}
1274
1275fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
1277 if path.is_empty() {
1278 return None;
1279 }
1280 if path.len() == 1 {
1281 return get_object_string_property(obj, path[0]);
1282 }
1283 let prop = find_property(obj, path[0])?;
1284 if let Expression::ObjectExpression(nested) = &prop.value {
1285 get_nested_string_from_object(nested, &path[1..])
1286 } else {
1287 None
1288 }
1289}
1290
1291fn get_nested_command_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
1293 if path.is_empty() {
1294 return None;
1295 }
1296 if path.len() == 1 {
1297 return find_property(obj, path[0]).and_then(|prop| expression_to_command(&prop.value));
1298 }
1299 let prop = find_property(obj, path[0])?;
1300 if let Expression::ObjectExpression(nested) = &prop.value {
1301 get_nested_command_from_object(nested, &path[1..])
1302 } else {
1303 None
1304 }
1305}
1306
1307pub(crate) fn expression_to_string(expr: &Expression) -> Option<String> {
1309 match expr {
1310 Expression::StringLiteral(s) => Some(s.value.to_string()),
1311 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
1312 t.quasis.first().map(|q| q.value.raw.to_string())
1313 }
1314 _ => None,
1315 }
1316}
1317
1318fn expression_to_command(expr: &Expression) -> Option<String> {
1320 match expr {
1321 Expression::StringLiteral(s) => Some(s.value.to_string()),
1322 Expression::TemplateLiteral(template) => template_literal_to_command(template),
1323 Expression::ParenthesizedExpression(paren) => expression_to_command(&paren.expression),
1324 Expression::TSAsExpression(ts_as) => expression_to_command(&ts_as.expression),
1325 Expression::TSSatisfiesExpression(ts_sat) => expression_to_command(&ts_sat.expression),
1326 _ => None,
1327 }
1328}
1329
1330fn template_literal_to_command(template: &TemplateLiteral<'_>) -> Option<String> {
1331 let first = template.quasis.first()?.value.raw.as_str();
1332 if first.trim_start().is_empty() {
1333 return None;
1334 }
1335
1336 let mut command = String::new();
1337 for (idx, quasi) in template.quasis.iter().enumerate() {
1338 command.push_str(quasi.value.raw.as_str());
1339 if idx < template.expressions.len() {
1340 let next = template
1341 .quasis
1342 .get(idx + 1)
1343 .map_or("", |next| next.value.raw.as_str());
1344 if dynamic_template_boundary_splits_static_token(quasi.value.raw.as_str(), next) {
1345 return None;
1346 }
1347 command.push(' ');
1348 }
1349 }
1350
1351 Some(command)
1352}
1353
1354fn dynamic_template_boundary_splits_static_token(before: &str, after: &str) -> bool {
1355 before
1356 .chars()
1357 .next_back()
1358 .is_some_and(is_command_token_char)
1359 && after.chars().next().is_some_and(is_command_token_char)
1360}
1361
1362fn is_command_token_char(ch: char) -> bool {
1363 !ch.is_whitespace() && !matches!(ch, '&' | '|' | ';' | '"' | '\'')
1364}
1365
1366pub(crate) fn expression_to_path_string(expr: &Expression) -> Option<String> {
1368 match expr {
1369 Expression::ParenthesizedExpression(paren) => expression_to_path_string(&paren.expression),
1370 Expression::TSAsExpression(ts_as) => expression_to_path_string(&ts_as.expression),
1371 Expression::TSSatisfiesExpression(ts_sat) => expression_to_path_string(&ts_sat.expression),
1372 Expression::StaticMemberExpression(member) if member.property.name == "pathname" => {
1373 expression_to_path_string(&member.object)
1374 }
1375 Expression::CallExpression(call) => call_expression_to_path_string(call),
1376 Expression::NewExpression(new_expr) => new_expression_to_path_string(new_expr),
1377 _ => expression_to_string(expr),
1378 }
1379}
1380
1381fn call_expression_to_path_string(call: &CallExpression) -> Option<String> {
1382 if matches!(&call.callee, Expression::Identifier(id) if id.name == "fileURLToPath") {
1383 return call
1384 .arguments
1385 .first()
1386 .and_then(Argument::as_expression)
1387 .and_then(expression_to_path_string);
1388 }
1389
1390 let callee_name = match &call.callee {
1391 Expression::Identifier(id) => Some(id.name.as_str()),
1392 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
1393 _ => None,
1394 }?;
1395
1396 if !matches!(callee_name, "resolve" | "join") {
1397 return None;
1398 }
1399
1400 let mut segments = Vec::new();
1401 for (index, arg) in call.arguments.iter().enumerate() {
1402 let expr = arg.as_expression()?;
1403
1404 if is_dirname_anchor(expr) {
1405 if index == 0 {
1406 continue;
1407 }
1408 return None;
1409 }
1410
1411 segments.push(expression_to_string(expr)?);
1412 }
1413
1414 (!segments.is_empty()).then(|| join_path_segments(&segments))
1415}
1416
1417fn is_dirname_anchor(expr: &Expression) -> bool {
1422 match expr {
1423 Expression::Identifier(id) => id.name == "__dirname",
1424 Expression::StaticMemberExpression(member) => {
1425 member.property.name == "dirname" && is_import_meta_expression(&member.object)
1426 }
1427 _ => false,
1428 }
1429}
1430
1431fn is_import_meta_expression(expr: &Expression) -> bool {
1433 matches!(
1434 expr,
1435 Expression::MetaProperty(meta) if meta.meta.name == "import" && meta.property.name == "meta"
1436 )
1437}
1438
1439fn new_expression_to_path_string(new_expr: &NewExpression) -> Option<String> {
1440 if !matches!(&new_expr.callee, Expression::Identifier(id) if id.name == "URL") {
1441 return None;
1442 }
1443
1444 let source = new_expr
1445 .arguments
1446 .first()
1447 .and_then(Argument::as_expression)
1448 .and_then(expression_to_string)?;
1449
1450 let base = new_expr
1451 .arguments
1452 .get(1)
1453 .and_then(Argument::as_expression)?;
1454 is_import_meta_url_expression(base).then_some(source)
1455}
1456
1457fn is_import_meta_url_expression(expr: &Expression) -> bool {
1458 if let Expression::StaticMemberExpression(member) = expr {
1459 member.property.name == "url" && matches!(member.object, Expression::MetaProperty(_))
1460 } else {
1461 false
1462 }
1463}
1464
1465fn join_path_segments(segments: &[String]) -> String {
1466 let mut joined = PathBuf::new();
1467 for segment in segments {
1468 joined.push(segment);
1469 }
1470 joined.to_string_lossy().replace('\\', "/")
1471}
1472
1473fn expression_to_alias_pairs(expr: &Expression) -> Vec<(String, String)> {
1474 match expr {
1475 Expression::ObjectExpression(obj) => obj
1476 .properties
1477 .iter()
1478 .filter_map(|prop| {
1479 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
1480 return None;
1481 };
1482 let find = property_key_to_string(&prop.key)?;
1483 let replacement = expression_to_path_values(&prop.value)
1484 .into_iter()
1485 .next()
1486 .map(|path| path_to_config_string(&path))?;
1487 Some((find, replacement))
1488 })
1489 .collect(),
1490 Expression::ArrayExpression(arr) => arr
1491 .elements
1492 .iter()
1493 .filter_map(|element| {
1494 let Expression::ObjectExpression(obj) = element.as_expression()? else {
1495 return None;
1496 };
1497 let find = find_property(obj, "find")
1498 .and_then(|prop| expression_to_string(&prop.value))?;
1499 let replacement = find_property(obj, "replacement")
1500 .and_then(|prop| expression_to_path_string(&prop.value))?;
1501 Some((find, replacement))
1502 })
1503 .collect(),
1504 _ => Vec::new(),
1505 }
1506}
1507
1508fn expression_to_alias_pairs_kinded(expr: &Expression) -> Vec<(String, String, bool)> {
1512 match expr {
1513 Expression::ObjectExpression(obj) => obj
1514 .properties
1515 .iter()
1516 .filter_map(|prop| {
1517 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
1518 return None;
1519 };
1520 let find = property_key_to_string(&prop.key)?;
1521 let (replacement, is_bare) = alias_replacement_kinded(&prop.value)?;
1522 Some((find, replacement, is_bare))
1523 })
1524 .collect(),
1525 Expression::ArrayExpression(arr) => arr
1526 .elements
1527 .iter()
1528 .filter_map(|element| {
1529 let Expression::ObjectExpression(obj) = element.as_expression()? else {
1530 return None;
1531 };
1532 let find = find_property(obj, "find")
1533 .and_then(|prop| expression_to_string(&prop.value))?;
1534 let (replacement, is_bare) = find_property(obj, "replacement")
1535 .and_then(|prop| alias_replacement_kinded(&prop.value))?;
1536 Some((find, replacement, is_bare))
1537 })
1538 .collect(),
1539 _ => Vec::new(),
1540 }
1541}
1542
1543fn alias_replacement_kinded(expr: &Expression) -> Option<(String, bool)> {
1550 match expr {
1551 Expression::ParenthesizedExpression(paren) => alias_replacement_kinded(&paren.expression),
1552 Expression::TSAsExpression(ts_as) => alias_replacement_kinded(&ts_as.expression),
1553 Expression::TSSatisfiesExpression(ts_sat) => alias_replacement_kinded(&ts_sat.expression),
1554 Expression::StringLiteral(s) => {
1555 let value = s.value.to_string();
1556 let is_bare =
1557 !value.starts_with("./") && !value.starts_with("../") && !value.starts_with('/');
1558 Some((value, is_bare))
1559 }
1560 Expression::ArrayExpression(arr) => arr
1564 .elements
1565 .iter()
1566 .find_map(ArrayExpressionElement::as_expression)
1567 .and_then(alias_replacement_kinded),
1568 _ => expression_to_path_string(expr).map(|value| (value, false)),
1569 }
1570}
1571
1572const MAX_ALIAS_RESOLVE_DEPTH: usize = 8;
1578
1579const ALIAS_SIBLING_EXTS: [&str; 6] = ["js", "mjs", "cjs", "ts", "mts", "cts"];
1586
1587fn resolve_alias_pairs_kinded(
1604 program: &Program,
1605 config_path: &Path,
1606 expr: &Expression,
1607 visited: &mut FxHashSet<PathBuf>,
1608 depth: usize,
1609) -> Vec<(String, String, bool)> {
1610 match expr {
1611 Expression::ParenthesizedExpression(paren) => {
1612 resolve_alias_pairs_kinded(program, config_path, &paren.expression, visited, depth)
1613 }
1614 Expression::TSAsExpression(ts_as) => {
1615 resolve_alias_pairs_kinded(program, config_path, &ts_as.expression, visited, depth)
1616 }
1617 Expression::TSSatisfiesExpression(ts_sat) => {
1618 resolve_alias_pairs_kinded(program, config_path, &ts_sat.expression, visited, depth)
1619 }
1620 Expression::ObjectExpression(obj) => {
1621 resolve_object_alias_pairs_kinded(program, config_path, obj, visited, depth)
1622 }
1623 Expression::ArrayExpression(arr) => {
1624 resolve_array_alias_pairs_kinded(program, config_path, arr, visited, depth)
1625 }
1626 Expression::Identifier(id) => {
1627 resolve_identifier_alias_pairs(program, config_path, id.name.as_str(), visited, depth)
1628 }
1629 _ => Vec::new(),
1630 }
1631}
1632
1633fn resolve_object_alias_pairs_kinded(
1636 program: &Program,
1637 config_path: &Path,
1638 obj: &ObjectExpression,
1639 visited: &mut FxHashSet<PathBuf>,
1640 depth: usize,
1641) -> Vec<(String, String, bool)> {
1642 let mut pairs = Vec::new();
1643 for prop in &obj.properties {
1644 match prop {
1645 ObjectPropertyKind::ObjectProperty(prop) => {
1646 if let Some(find) = property_key_to_string(&prop.key)
1647 && let Some((replacement, is_bare)) = alias_replacement_kinded(&prop.value)
1648 {
1649 pairs.push((find, replacement, is_bare));
1650 }
1651 }
1652 ObjectPropertyKind::SpreadProperty(spread) => {
1654 pairs.extend(resolve_alias_pairs_kinded(
1655 program,
1656 config_path,
1657 &spread.argument,
1658 visited,
1659 depth,
1660 ));
1661 }
1662 }
1663 }
1664 pairs
1665}
1666
1667fn resolve_array_alias_pairs_kinded(
1670 program: &Program,
1671 config_path: &Path,
1672 arr: &ArrayExpression,
1673 visited: &mut FxHashSet<PathBuf>,
1674 depth: usize,
1675) -> Vec<(String, String, bool)> {
1676 let mut pairs = Vec::new();
1677 for element in &arr.elements {
1678 match element {
1679 ArrayExpressionElement::SpreadElement(spread) => {
1681 pairs.extend(resolve_alias_pairs_kinded(
1682 program,
1683 config_path,
1684 &spread.argument,
1685 visited,
1686 depth,
1687 ));
1688 }
1689 _ => {
1690 if let Some(Expression::ObjectExpression(obj)) = element.as_expression()
1691 && let Some(find) = find_property(obj, "find")
1692 .and_then(|prop| expression_to_string(&prop.value))
1693 && let Some((replacement, is_bare)) = find_property(obj, "replacement")
1694 .and_then(|prop| alias_replacement_kinded(&prop.value))
1695 {
1696 pairs.push((find, replacement, is_bare));
1697 }
1698 }
1699 }
1700 }
1701 pairs
1702}
1703
1704fn resolve_identifier_alias_pairs(
1707 program: &Program,
1708 config_path: &Path,
1709 name: &str,
1710 visited: &mut FxHashSet<PathBuf>,
1711 depth: usize,
1712) -> Vec<(String, String, bool)> {
1713 if depth >= MAX_ALIAS_RESOLVE_DEPTH {
1714 return Vec::new();
1715 }
1716 if let Some(init) = find_variable_init_expression(program, name) {
1718 return resolve_alias_pairs_kinded(program, config_path, init, visited, depth + 1);
1719 }
1720 let Some((specifier, imported_name)) = find_relative_import_binding(program, name) else {
1722 return Vec::new();
1723 };
1724 resolve_imported_alias_pairs(
1725 config_path,
1726 &specifier,
1727 imported_name.as_deref(),
1728 visited,
1729 depth + 1,
1730 )
1731}
1732
1733fn resolve_imported_alias_pairs(
1736 config_path: &Path,
1737 specifier: &str,
1738 imported_name: Option<&str>,
1739 visited: &mut FxHashSet<PathBuf>,
1740 depth: usize,
1741) -> Vec<(String, String, bool)> {
1742 let Some((sibling_path, sibling_source)) = resolve_sibling_module(config_path, specifier)
1743 else {
1744 return Vec::new();
1745 };
1746 if !visited.insert(sibling_path.clone()) {
1747 return Vec::new();
1748 }
1749 extract_from_source(&sibling_source, &sibling_path, |program| {
1750 let init = find_exported_init(program, imported_name)?;
1751 let pairs = resolve_alias_pairs_kinded(program, &sibling_path, init, visited, depth);
1752 (!pairs.is_empty()).then_some(pairs)
1753 })
1754 .unwrap_or_default()
1755}
1756
1757fn find_variable_init_expression<'a>(
1762 program: &'a Program<'a>,
1763 name: &str,
1764) -> Option<&'a Expression<'a>> {
1765 for stmt in &program.body {
1766 let decl = match stmt {
1767 Statement::VariableDeclaration(decl) => decl,
1768 Statement::ExportNamedDeclaration(export) => match &export.declaration {
1769 Some(Declaration::VariableDeclaration(decl)) => decl,
1770 _ => continue,
1771 },
1772 _ => continue,
1773 };
1774 for declarator in &decl.declarations {
1775 if let BindingPattern::BindingIdentifier(id) = &declarator.id
1776 && id.name == name
1777 && let Some(init) = &declarator.init
1778 {
1779 return Some(init);
1780 }
1781 }
1782 }
1783 None
1784}
1785
1786fn find_exported_init<'a>(
1791 program: &'a Program<'a>,
1792 name: Option<&str>,
1793) -> Option<&'a Expression<'a>> {
1794 match name {
1795 Some(name) => find_variable_init_expression(program, name),
1796 None => program.body.iter().find_map(|stmt| {
1797 if let Statement::ExportDefaultDeclaration(decl) = stmt {
1798 decl.declaration.as_expression()
1799 } else {
1800 None
1801 }
1802 }),
1803 }
1804}
1805
1806fn find_relative_import_binding(program: &Program, name: &str) -> Option<(String, Option<String>)> {
1811 for stmt in &program.body {
1812 let Statement::ImportDeclaration(decl) = stmt else {
1813 continue;
1814 };
1815 let specifier = decl.source.value.as_str();
1816 if !is_relative_specifier(specifier) {
1817 continue;
1818 }
1819 let Some(specifiers) = &decl.specifiers else {
1820 continue;
1821 };
1822 for spec in specifiers {
1823 match spec {
1824 ImportDeclarationSpecifier::ImportSpecifier(spec) if spec.local.name == name => {
1825 return Some((
1826 specifier.to_string(),
1827 Some(spec.imported.name().to_string()),
1828 ));
1829 }
1830 ImportDeclarationSpecifier::ImportDefaultSpecifier(spec)
1831 if spec.local.name == name =>
1832 {
1833 return Some((specifier.to_string(), None));
1834 }
1835 _ => {}
1836 }
1837 }
1838 }
1839 None
1840}
1841
1842fn is_relative_specifier(specifier: &str) -> bool {
1845 specifier.starts_with("./") || specifier.starts_with("../") || specifier.starts_with('/')
1846}
1847
1848fn resolve_sibling_module(config_path: &Path, specifier: &str) -> Option<(PathBuf, String)> {
1854 let parent = config_path.parent().unwrap_or(config_path);
1855 let direct = parent.join(specifier);
1856 if let Ok(source) = std::fs::read_to_string(&direct) {
1857 return Some((direct, source));
1858 }
1859 for ext in ALIAS_SIBLING_EXTS {
1860 let candidate = parent.join(format!("{specifier}.{ext}"));
1861 if let Ok(source) = std::fs::read_to_string(&candidate) {
1862 return Some((candidate, source));
1863 }
1864 }
1865 for ext in ALIAS_SIBLING_EXTS {
1866 let candidate = direct.join(format!("index.{ext}"));
1867 if let Ok(source) = std::fs::read_to_string(&candidate) {
1868 return Some((candidate, source));
1869 }
1870 }
1871 None
1872}
1873
1874fn find_default_export_array<'a>(program: &'a Program<'a>) -> Option<&'a ArrayExpression<'a>> {
1879 for stmt in &program.body {
1880 if let Statement::ExportDefaultDeclaration(decl) = stmt
1881 && let Some(expr) = decl.declaration.as_expression()
1882 {
1883 return array_from_expression(expr);
1884 }
1885 }
1886 None
1887}
1888
1889fn array_from_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
1890 match expr {
1891 Expression::ArrayExpression(arr) => Some(arr),
1892 Expression::ParenthesizedExpression(paren) => array_from_expression(&paren.expression),
1893 Expression::TSAsExpression(ts_as) => array_from_expression(&ts_as.expression),
1894 Expression::TSSatisfiesExpression(ts_sat) => array_from_expression(&ts_sat.expression),
1895 Expression::CallExpression(call) => call
1896 .arguments
1897 .first()
1898 .and_then(Argument::as_expression)
1899 .and_then(array_from_expression),
1900 _ => None,
1901 }
1902}
1903
1904pub(crate) fn lexical_normalize(path: &Path) -> PathBuf {
1905 let mut normalized = PathBuf::new();
1906
1907 for component in path.components() {
1908 match component {
1909 std::path::Component::CurDir => {}
1910 std::path::Component::ParentDir => {
1911 normalized.pop();
1912 }
1913 _ => normalized.push(component.as_os_str()),
1914 }
1915 }
1916
1917 normalized
1918}
1919
1920fn expression_to_string_array(expr: &Expression) -> Vec<String> {
1922 match expr {
1923 Expression::ArrayExpression(arr) => arr
1924 .elements
1925 .iter()
1926 .filter_map(|el| match el {
1927 ArrayExpressionElement::SpreadElement(_) => None,
1928 _ => el.as_expression().and_then(expression_to_string),
1929 })
1930 .collect(),
1931 _ => vec![],
1932 }
1933}
1934
1935fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
1940 let mut values = Vec::new();
1941 match expr {
1942 Expression::StringLiteral(s) => {
1943 values.push(s.value.to_string());
1944 }
1945 Expression::ArrayExpression(arr) => {
1946 for el in &arr.elements {
1947 if let Some(inner) = el.as_expression() {
1948 match inner {
1949 Expression::StringLiteral(s) => {
1950 values.push(s.value.to_string());
1951 }
1952 Expression::ArrayExpression(sub_arr) => {
1953 if let Some(first) = sub_arr.elements.first()
1954 && let Some(first_expr) = first.as_expression()
1955 && let Some(s) = expression_to_string(first_expr)
1956 {
1957 values.push(s);
1958 }
1959 }
1960 _ => {}
1961 }
1962 }
1963 }
1964 }
1965 Expression::ObjectExpression(obj) => {
1966 for prop in &obj.properties {
1967 if let ObjectPropertyKind::ObjectProperty(p) = prop {
1968 match &p.value {
1969 Expression::StringLiteral(s) => {
1970 values.push(s.value.to_string());
1971 }
1972 Expression::ArrayExpression(sub_arr) => {
1973 if let Some(first) = sub_arr.elements.first()
1974 && let Some(first_expr) = first.as_expression()
1975 && let Some(s) = expression_to_string(first_expr)
1976 {
1977 values.push(s);
1978 }
1979 }
1980 _ => {}
1981 }
1982 }
1983 }
1984 }
1985 _ => {}
1986 }
1987 values
1988}
1989
1990fn collect_shallow_string_or_object_property_values(
1992 expr: &Expression,
1993 object_property: &str,
1994) -> Vec<String> {
1995 match expr {
1996 Expression::ArrayExpression(arr) => arr
1997 .elements
1998 .iter()
1999 .filter_map(|element| {
2000 element
2001 .as_expression()
2002 .and_then(|expr| shallow_string_or_object_property(expr, object_property))
2003 })
2004 .collect(),
2005 _ => shallow_string_or_object_property(expr, object_property)
2006 .into_iter()
2007 .collect(),
2008 }
2009}
2010
2011fn shallow_string_or_object_property(expr: &Expression, object_property: &str) -> Option<String> {
2012 match expr {
2013 Expression::ParenthesizedExpression(paren) => {
2014 shallow_string_or_object_property(&paren.expression, object_property)
2015 }
2016 Expression::TSSatisfiesExpression(ts_sat) => {
2017 shallow_string_or_object_property(&ts_sat.expression, object_property)
2018 }
2019 Expression::TSAsExpression(ts_as) => {
2020 shallow_string_or_object_property(&ts_as.expression, object_property)
2021 }
2022 Expression::ArrayExpression(sub_arr) => sub_arr
2023 .elements
2024 .first()
2025 .and_then(ArrayExpressionElement::as_expression)
2026 .and_then(expression_to_string),
2027 Expression::ObjectExpression(obj) => {
2028 find_property(obj, object_property).and_then(|prop| expression_to_string(&prop.value))
2029 }
2030 _ => expression_to_string(expr),
2031 }
2032}
2033
2034fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
2036 match expr {
2037 Expression::StringLiteral(s) => {
2038 values.push(s.value.to_string());
2039 }
2040 Expression::ArrayExpression(arr) => {
2041 for el in &arr.elements {
2042 if let Some(expr) = el.as_expression() {
2043 collect_all_string_values(expr, values);
2044 }
2045 }
2046 }
2047 Expression::ObjectExpression(obj) => {
2048 for prop in &obj.properties {
2049 if let ObjectPropertyKind::ObjectProperty(p) = prop {
2050 collect_all_string_values(&p.value, values);
2051 }
2052 }
2053 }
2054 _ => {}
2055 }
2056}
2057
2058fn property_key_to_string(key: &PropertyKey) -> Option<String> {
2060 match key {
2061 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
2062 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
2063 _ => None,
2064 }
2065}
2066
2067fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
2069 if path.is_empty() {
2070 return None;
2071 }
2072 let prop = find_property(obj, path[0])?;
2073 if path.len() == 1 {
2074 if let Expression::ObjectExpression(nested) = &prop.value {
2075 let keys = nested
2076 .properties
2077 .iter()
2078 .filter_map(|p| {
2079 if let ObjectPropertyKind::ObjectProperty(p) = p {
2080 property_key_to_string(&p.key)
2081 } else {
2082 None
2083 }
2084 })
2085 .collect();
2086 return Some(keys);
2087 }
2088 return None;
2089 }
2090 if let Expression::ObjectExpression(nested) = &prop.value {
2091 get_nested_object_keys(nested, &path[1..])
2092 } else {
2093 None
2094 }
2095}
2096
2097fn get_nested_expression<'a>(
2099 obj: &'a ObjectExpression<'a>,
2100 path: &[&str],
2101) -> Option<&'a Expression<'a>> {
2102 if path.is_empty() {
2103 return None;
2104 }
2105 let prop = find_property(obj, path[0])?;
2106 if path.len() == 1 {
2107 return Some(&prop.value);
2108 }
2109 if let Expression::ObjectExpression(nested) = &prop.value {
2110 get_nested_expression(nested, &path[1..])
2111 } else {
2112 None
2113 }
2114}
2115
2116fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
2118 if path.is_empty() {
2119 return None;
2120 }
2121 if path.len() == 1 {
2122 let prop = find_property(obj, path[0])?;
2123 return Some(expression_to_string_or_array(&prop.value));
2124 }
2125 let prop = find_property(obj, path[0])?;
2126 if let Expression::ObjectExpression(nested) = &prop.value {
2127 get_nested_string_or_array(nested, &path[1..])
2128 } else {
2129 None
2130 }
2131}
2132
2133fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
2141 match expr {
2142 Expression::StringLiteral(s) => vec![s.value.to_string()],
2143 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
2144 .quasis
2145 .first()
2146 .map(|q| vec![q.value.raw.to_string()])
2147 .unwrap_or_default(),
2148 Expression::ArrayExpression(arr) => arr
2149 .elements
2150 .iter()
2151 .filter_map(|el| el.as_expression())
2152 .flat_map(|e| match e {
2153 Expression::ObjectExpression(obj) => find_property(obj, "input")
2154 .map(|p| expression_to_string_or_array(&p.value))
2155 .unwrap_or_default(),
2156 _ => expression_to_path_string(e).into_iter().collect(),
2157 })
2158 .collect(),
2159 Expression::ObjectExpression(obj) => obj
2160 .properties
2161 .iter()
2162 .flat_map(|p| {
2163 if let ObjectPropertyKind::ObjectProperty(p) = p {
2164 match &p.value {
2165 Expression::ArrayExpression(_) => expression_to_string_or_array(&p.value),
2166 Expression::ObjectExpression(value_obj) => {
2167 find_property(value_obj, "import")
2168 .map(|import_prop| {
2169 expression_to_string_or_array(&import_prop.value)
2170 })
2171 .unwrap_or_default()
2172 }
2173 _ => expression_to_path_string(&p.value).into_iter().collect(),
2174 }
2175 } else {
2176 Vec::new()
2177 }
2178 })
2179 .collect(),
2180 _ => expression_to_path_string(expr).into_iter().collect(),
2181 }
2182}
2183
2184fn collect_require_sources(expr: &Expression) -> Vec<String> {
2186 let mut sources = Vec::new();
2187 match expr {
2188 Expression::CallExpression(call) if is_require_call(call) => {
2189 if let Some(s) = get_require_source(call) {
2190 sources.push(s);
2191 }
2192 }
2193 Expression::ArrayExpression(arr) => {
2194 for el in &arr.elements {
2195 if let Some(inner) = el.as_expression() {
2196 match inner {
2197 Expression::CallExpression(call) if is_require_call(call) => {
2198 if let Some(s) = get_require_source(call) {
2199 sources.push(s);
2200 }
2201 }
2202 Expression::ArrayExpression(sub_arr) => {
2203 if let Some(first) = sub_arr.elements.first()
2204 && let Some(Expression::CallExpression(call)) =
2205 first.as_expression()
2206 && is_require_call(call)
2207 && let Some(s) = get_require_source(call)
2208 {
2209 sources.push(s);
2210 }
2211 }
2212 _ => {}
2213 }
2214 }
2215 }
2216 }
2217 _ => {}
2218 }
2219 sources
2220}
2221
2222fn is_require_call(call: &CallExpression) -> bool {
2224 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
2225}
2226
2227fn get_require_source(call: &CallExpression) -> Option<String> {
2229 call.arguments.first().and_then(|arg| {
2230 if let Argument::StringLiteral(s) = arg {
2231 Some(s.value.to_string())
2232 } else {
2233 None
2234 }
2235 })
2236}
2237
2238#[cfg(test)]
2239mod tests {
2240 use super::*;
2241 use std::path::PathBuf;
2242
2243 fn js_path() -> PathBuf {
2244 PathBuf::from("config.js")
2245 }
2246
2247 fn ts_path() -> PathBuf {
2248 PathBuf::from("config.ts")
2249 }
2250
2251 #[test]
2252 fn extract_lazy_imports_bare_arrows() {
2253 let source = r"
2254 import { defineConfig } from '@adonisjs/core/app'
2255 export default defineConfig({
2256 preloads: [
2257 () => import('#start/routes'),
2258 () => import('#start/kernel'),
2259 ],
2260 })
2261 ";
2262 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["preloads"]);
2263 assert_eq!(specs, vec!["#start/routes", "#start/kernel"]);
2264 }
2265
2266 #[test]
2267 fn extract_lazy_imports_object_form_with_file_key() {
2268 let source = r"
2269 export default defineConfig({
2270 providers: [
2271 () => import('@adonisjs/core/providers/app_provider'),
2272 {
2273 file: () => import('@adonisjs/core/providers/repl_provider'),
2274 environment: ['repl', 'test'],
2275 },
2276 ],
2277 })
2278 ";
2279 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
2280 assert_eq!(
2281 specs,
2282 vec![
2283 "@adonisjs/core/providers/app_provider",
2284 "@adonisjs/core/providers/repl_provider",
2285 ]
2286 );
2287 }
2288
2289 #[test]
2290 fn extract_lazy_imports_block_body_with_return() {
2291 let source = r"
2292 export default defineConfig({
2293 commands: [
2294 () => { return import('@adonisjs/core/commands') },
2295 ],
2296 })
2297 ";
2298 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
2299 assert_eq!(specs, vec!["@adonisjs/core/commands"]);
2300 }
2301
2302 #[test]
2303 fn extract_lazy_imports_skips_unknown_element_shapes() {
2304 let source = r"
2305 export default defineConfig({
2306 commands: [
2307 'string-entry',
2308 42,
2309 { other: 'value' },
2310 () => import('@adonisjs/lucid/commands'),
2311 ],
2312 })
2313 ";
2314 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
2315 assert_eq!(specs, vec!["@adonisjs/lucid/commands"]);
2316 }
2317
2318 #[test]
2319 fn extract_lazy_imports_missing_property_returns_empty() {
2320 let source = r"
2321 export default defineConfig({
2322 preloads: [() => import('#start/routes')],
2323 })
2324 ";
2325 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
2326 assert!(specs.is_empty());
2327 }
2328
2329 #[test]
2330 fn extract_imports_basic() {
2331 let source = r"
2332 import foo from 'foo-pkg';
2333 import { bar } from '@scope/bar';
2334 export default {};
2335 ";
2336 let imports = extract_imports(source, &js_path());
2337 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
2338 }
2339
2340 #[test]
2341 fn extract_default_export_object_property() {
2342 let source = r#"export default { testDir: "./tests" };"#;
2343 let val = extract_config_string(source, &js_path(), &["testDir"]);
2344 assert_eq!(val, Some("./tests".to_string()));
2345 }
2346
2347 #[test]
2348 fn extract_define_config_property() {
2349 let source = r#"
2350 import { defineConfig } from 'vitest/config';
2351 export default defineConfig({
2352 test: {
2353 include: ["**/*.test.ts", "**/*.spec.ts"],
2354 setupFiles: ["./test/setup.ts"]
2355 }
2356 });
2357 "#;
2358 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
2359 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
2360
2361 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
2362 assert_eq!(setup, vec!["./test/setup.ts"]);
2363 }
2364
2365 #[test]
2366 fn extract_module_exports_property() {
2367 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
2368 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
2369 assert_eq!(val, Some("jsdom".to_string()));
2370 }
2371
2372 #[test]
2373 fn extract_nested_string_array() {
2374 let source = r#"
2375 export default {
2376 resolve: {
2377 alias: {
2378 "@": "./src"
2379 }
2380 },
2381 test: {
2382 include: ["src/**/*.test.ts"]
2383 }
2384 };
2385 "#;
2386 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
2387 assert_eq!(include, vec!["src/**/*.test.ts"]);
2388 }
2389
2390 #[test]
2391 fn extract_addons_array() {
2392 let source = r#"
2393 export default {
2394 addons: [
2395 "@storybook/addon-a11y",
2396 "@storybook/addon-docs",
2397 "@storybook/addon-links"
2398 ]
2399 };
2400 "#;
2401 let addons = extract_config_property_strings(source, &ts_path(), "addons");
2402 assert_eq!(
2403 addons,
2404 vec![
2405 "@storybook/addon-a11y",
2406 "@storybook/addon-docs",
2407 "@storybook/addon-links"
2408 ]
2409 );
2410 }
2411
2412 #[test]
2413 fn handle_empty_config() {
2414 let source = "";
2415 let result = extract_config_string(source, &js_path(), &["key"]);
2416 assert_eq!(result, None);
2417 }
2418
2419 #[test]
2420 fn object_keys_postcss_plugins() {
2421 let source = r"
2422 module.exports = {
2423 plugins: {
2424 autoprefixer: {},
2425 tailwindcss: {},
2426 'postcss-import': {}
2427 }
2428 };
2429 ";
2430 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2431 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
2432 }
2433
2434 #[test]
2435 fn object_keys_nested_path() {
2436 let source = r"
2437 export default {
2438 build: {
2439 plugins: {
2440 minify: {},
2441 compress: {}
2442 }
2443 }
2444 };
2445 ";
2446 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
2447 assert_eq!(keys, vec!["minify", "compress"]);
2448 }
2449
2450 #[test]
2451 fn object_keys_empty_object() {
2452 let source = r"export default { plugins: {} };";
2453 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2454 assert!(keys.is_empty());
2455 }
2456
2457 #[test]
2458 fn object_keys_non_object_returns_empty() {
2459 let source = r#"export default { plugins: ["a", "b"] };"#;
2460 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2461 assert!(keys.is_empty());
2462 }
2463
2464 #[test]
2465 fn string_or_array_single_string() {
2466 let source = r#"export default { entry: "./src/index.js" };"#;
2467 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2468 assert_eq!(result, vec!["./src/index.js"]);
2469 }
2470
2471 #[test]
2472 fn string_or_array_array() {
2473 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
2474 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2475 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
2476 }
2477
2478 #[test]
2479 fn string_or_array_object_values() {
2480 let source =
2481 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
2482 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2483 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
2484 }
2485
2486 #[test]
2487 fn string_or_array_object_array_values() {
2488 let source = r#"export default { entry: { app: ["./src/polyfill.js", "./src/app.js"] } };"#;
2489 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2490 assert_eq!(result, vec!["./src/polyfill.js", "./src/app.js"]);
2491 }
2492
2493 #[test]
2494 fn string_or_array_webpack_entry_descriptors() {
2495 let source = r#"
2496 export default {
2497 entry: {
2498 app: {
2499 import: "./src/app.js",
2500 filename: "pages/app.js",
2501 dependOn: "shared",
2502 },
2503 admin: {
2504 import: ["./src/admin-polyfill.js", "./src/admin.js"],
2505 runtime: "runtime",
2506 },
2507 shared: ["react", "react-dom"],
2508 },
2509 };
2510 "#;
2511 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2512 assert_eq!(
2513 result,
2514 vec![
2515 "./src/app.js",
2516 "./src/admin-polyfill.js",
2517 "./src/admin.js",
2518 "react",
2519 "react-dom"
2520 ]
2521 );
2522 }
2523
2524 #[test]
2525 fn string_or_array_nested_path() {
2526 let source = r#"
2527 export default {
2528 build: {
2529 rollupOptions: {
2530 input: ["./index.html", "./about.html"]
2531 }
2532 }
2533 };
2534 "#;
2535 let result = extract_config_string_or_array(
2536 source,
2537 &js_path(),
2538 &["build", "rollupOptions", "input"],
2539 );
2540 assert_eq!(result, vec!["./index.html", "./about.html"]);
2541 }
2542
2543 #[test]
2544 fn string_or_array_template_literal() {
2545 let source = r"export default { entry: `./src/index.js` };";
2546 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2547 assert_eq!(result, vec!["./src/index.js"]);
2548 }
2549
2550 #[test]
2551 fn string_or_array_object_path_helper_values() {
2552 let source = r#"
2553 import { resolve, join } from "node:path";
2554 import path from "node:path";
2555 export default {
2556 build: {
2557 rollupOptions: {
2558 input: {
2559 app: resolve(__dirname, "src/app.ts"),
2560 modal: path.resolve(__dirname, "src/modal.ts"),
2561 tabs: join(__dirname, "src/tabs.ts"),
2562 styles: resolve(__dirname, "src/index.css"),
2563 },
2564 },
2565 },
2566 };
2567 "#;
2568 let result = extract_config_string_or_array(
2569 source,
2570 &js_path(),
2571 &["build", "rollupOptions", "input"],
2572 );
2573 assert_eq!(
2574 result,
2575 vec!["src/app.ts", "src/modal.ts", "src/tabs.ts", "src/index.css"]
2576 );
2577 }
2578
2579 #[test]
2580 fn string_or_array_array_path_helper_values() {
2581 let source = r#"
2582 import { resolve } from "node:path";
2583 export default {
2584 build: {
2585 rollupOptions: {
2586 input: [resolve(__dirname, "src/a.ts"), "./src/b.ts"],
2587 },
2588 },
2589 };
2590 "#;
2591 let result = extract_config_string_or_array(
2592 source,
2593 &js_path(),
2594 &["build", "rollupOptions", "input"],
2595 );
2596 assert_eq!(result, vec!["src/a.ts", "./src/b.ts"]);
2597 }
2598
2599 #[test]
2600 fn string_or_array_top_level_path_helper_call() {
2601 let source = r#"
2602 import { resolve } from "node:path";
2603 export default { build: { lib: { entry: resolve(__dirname, "src/index.ts") } } };
2604 "#;
2605 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
2606 assert_eq!(result, vec!["src/index.ts"]);
2607 }
2608
2609 #[test]
2610 fn string_or_array_import_meta_dirname_anchor() {
2611 let source = r#"
2612 import { resolve } from "node:path";
2613 export default {
2614 build: { lib: { entry: resolve(import.meta.dirname, "src/index.ts") } },
2615 };
2616 "#;
2617 let result = extract_config_string_or_array(source, &ts_path(), &["build", "lib", "entry"]);
2618 assert_eq!(result, vec!["src/index.ts"]);
2619 }
2620
2621 #[test]
2622 fn string_or_array_non_literal_path_helper_args_dropped() {
2623 let source = r#"
2624 import { resolve } from "node:path";
2625 export default { build: { lib: { entry: resolve(baseDir, "src/index.ts") } } };
2626 "#;
2627 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
2628 assert!(
2629 result.is_empty(),
2630 "non-literal path-helper args must be dropped: {result:?}"
2631 );
2632 }
2633
2634 #[test]
2635 fn require_strings_array() {
2636 let source = r"
2637 module.exports = {
2638 plugins: [
2639 require('autoprefixer'),
2640 require('postcss-import')
2641 ]
2642 };
2643 ";
2644 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2645 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
2646 }
2647
2648 #[test]
2649 fn require_strings_with_tuples() {
2650 let source = r"
2651 module.exports = {
2652 plugins: [
2653 require('autoprefixer'),
2654 [require('postcss-preset-env'), { stage: 3 }]
2655 ]
2656 };
2657 ";
2658 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2659 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
2660 }
2661
2662 #[test]
2663 fn require_strings_empty_array() {
2664 let source = r"module.exports = { plugins: [] };";
2665 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2666 assert!(deps.is_empty());
2667 }
2668
2669 #[test]
2670 fn require_strings_no_require_calls() {
2671 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
2672 let deps = extract_config_require_strings(source, &js_path(), "plugins");
2673 assert!(deps.is_empty());
2674 }
2675
2676 #[test]
2677 fn extract_aliases_from_object_with_file_url_to_path() {
2678 let source = r#"
2679 import { defineConfig } from 'vite';
2680 import { fileURLToPath, URL } from 'node:url';
2681
2682 export default defineConfig({
2683 resolve: {
2684 alias: {
2685 "@": fileURLToPath(new URL("./src", import.meta.url))
2686 }
2687 }
2688 });
2689 "#;
2690
2691 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
2692 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
2693 }
2694
2695 #[test]
2696 fn extract_aliases_from_array_form() {
2697 let source = r#"
2698 export default {
2699 resolve: {
2700 alias: [
2701 { find: "@", replacement: "./src" },
2702 { find: "$utils", replacement: "src/lib/utils" }
2703 ]
2704 }
2705 };
2706 "#;
2707
2708 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
2709 assert_eq!(
2710 aliases,
2711 vec![
2712 ("@".to_string(), "./src".to_string()),
2713 ("$utils".to_string(), "src/lib/utils".to_string())
2714 ]
2715 );
2716 }
2717
2718 #[test]
2719 fn extract_aliases_from_object_with_array_values() {
2720 let source = r#"
2721 ({
2722 compilerOptions: {
2723 paths: {
2724 "@/*": ["./src/*"],
2725 "@shared/*": ["./shared/*", "./fallback/*"]
2726 }
2727 }
2728 })
2729 "#;
2730
2731 let aliases = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
2732 assert_eq!(
2733 aliases,
2734 vec![
2735 ("@/*".to_string(), "./src/*".to_string()),
2736 ("@shared/*".to_string(), "./shared/*".to_string())
2737 ]
2738 );
2739 }
2740
2741 #[test]
2742 fn extract_array_object_strings_mixed_forms() {
2743 let source = r#"
2744 export default {
2745 components: [
2746 "~/components",
2747 { path: "@/feature-components" }
2748 ]
2749 };
2750 "#;
2751
2752 let values =
2753 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
2754 assert_eq!(
2755 values,
2756 vec![
2757 "~/components".to_string(),
2758 "@/feature-components".to_string()
2759 ]
2760 );
2761 }
2762
2763 #[test]
2764 fn extract_array_object_string_pairs_with_and_without_secondary() {
2765 let source = r#"
2766 export default {
2767 webServer: [
2768 { command: "tsx scripts/api.ts", cwd: "packages/api" },
2769 { command: "tsx scripts/web.ts" }
2770 ]
2771 };
2772 "#;
2773
2774 let pairs = extract_config_array_object_string_pairs(
2775 source,
2776 &ts_path(),
2777 &["webServer"],
2778 "command",
2779 "cwd",
2780 );
2781 assert_eq!(
2782 pairs,
2783 vec![
2784 (
2785 "tsx scripts/api.ts".to_string(),
2786 Some("packages/api".to_string())
2787 ),
2788 ("tsx scripts/web.ts".to_string(), None),
2789 ]
2790 );
2791 }
2792
2793 #[test]
2794 fn extract_array_object_string_pairs_skips_elements_missing_primary() {
2795 let source = r#"
2796 export default {
2797 webServer: [
2798 { cwd: "packages/api" },
2799 { command: "srvx --port 3000" }
2800 ]
2801 };
2802 "#;
2803
2804 let pairs = extract_config_array_object_string_pairs(
2805 source,
2806 &ts_path(),
2807 &["webServer"],
2808 "command",
2809 "cwd",
2810 );
2811 assert_eq!(pairs, vec![("srvx --port 3000".to_string(), None)]);
2812 }
2813
2814 #[test]
2815 fn extract_array_object_string_pairs_empty_for_object_form() {
2816 let source = r#"
2817 export default {
2818 webServer: { command: "srvx --port 3000" }
2819 };
2820 "#;
2821
2822 let pairs = extract_config_array_object_string_pairs(
2823 source,
2824 &ts_path(),
2825 &["webServer"],
2826 "command",
2827 "cwd",
2828 );
2829 assert!(pairs.is_empty());
2830 }
2831
2832 #[test]
2833 fn extract_config_plugin_option_string_from_json() {
2834 let source = r#"{
2835 "expo": {
2836 "plugins": [
2837 ["expo-router", { "root": "src/app" }]
2838 ]
2839 }
2840 }"#;
2841
2842 let value = extract_config_plugin_option_string(
2843 source,
2844 &json_path(),
2845 &["expo", "plugins"],
2846 "expo-router",
2847 "root",
2848 );
2849
2850 assert_eq!(value, Some("src/app".to_string()));
2851 }
2852
2853 #[test]
2854 fn extract_config_plugin_option_string_from_top_level_plugins() {
2855 let source = r#"{
2856 "plugins": [
2857 ["expo-router", { "root": "./src/routes" }]
2858 ]
2859 }"#;
2860
2861 let value = extract_config_plugin_option_string_from_paths(
2862 source,
2863 &json_path(),
2864 &[&["plugins"], &["expo", "plugins"]],
2865 "expo-router",
2866 "root",
2867 );
2868
2869 assert_eq!(value, Some("./src/routes".to_string()));
2870 }
2871
2872 #[test]
2873 fn extract_config_plugin_option_string_from_ts_config() {
2874 let source = r"
2875 export default {
2876 expo: {
2877 plugins: [
2878 ['expo-router', { root: './src/app' }]
2879 ]
2880 }
2881 };
2882 ";
2883
2884 let value = extract_config_plugin_option_string(
2885 source,
2886 &ts_path(),
2887 &["expo", "plugins"],
2888 "expo-router",
2889 "root",
2890 );
2891
2892 assert_eq!(value, Some("./src/app".to_string()));
2893 }
2894
2895 #[test]
2896 fn extract_config_plugin_option_string_returns_none_when_plugin_missing() {
2897 let source = r#"{
2898 "expo": {
2899 "plugins": [
2900 ["expo-font", {}]
2901 ]
2902 }
2903 }"#;
2904
2905 let value = extract_config_plugin_option_string(
2906 source,
2907 &json_path(),
2908 &["expo", "plugins"],
2909 "expo-router",
2910 "root",
2911 );
2912
2913 assert_eq!(value, None);
2914 }
2915
2916 #[test]
2917 fn vite_react_babel_dependencies_extract_plain_tuple_and_prefixed_entries() {
2918 let source = r#"
2919 import react from "@vitejs/plugin-react";
2920
2921 export default defineConfig({
2922 plugins: [
2923 react({
2924 babel: {
2925 plugins: [
2926 "babel-plugin-plain",
2927 ["module:@preact/signals-react-transform", { mode: "auto" }],
2928 ],
2929 presets: [["@babel/preset-react", { runtime: "automatic" }]],
2930 },
2931 }),
2932 ],
2933 });
2934 "#;
2935
2936 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
2937
2938 assert_eq!(
2939 deps,
2940 vec![
2941 "babel-plugin-plain".to_string(),
2942 "@preact/signals-react-transform".to_string(),
2943 "@babel/preset-react".to_string(),
2944 ]
2945 );
2946 }
2947
2948 #[test]
2949 fn vite_react_babel_dependencies_support_default_alias_import() {
2950 let source = r#"
2951 import { default as viteReact } from "@vitejs/plugin-react";
2952
2953 export default {
2954 plugins: [
2955 viteReact({
2956 babel: {
2957 plugins: [["module:@scope/pkg/plugin", {}]],
2958 },
2959 }),
2960 ],
2961 };
2962 "#;
2963
2964 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
2965
2966 assert_eq!(deps, vec!["@scope/pkg".to_string()]);
2967 }
2968
2969 #[test]
2970 fn vite_react_babel_dependencies_ignore_unrelated_plugin_calls() {
2971 let source = r#"
2972 import vue from "@vitejs/plugin-vue";
2973
2974 export default {
2975 plugins: [
2976 vue({
2977 babel: {
2978 plugins: ["@preact/signals-react-transform"],
2979 },
2980 }),
2981 ],
2982 };
2983 "#;
2984
2985 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
2986
2987 assert!(deps.is_empty());
2988 }
2989
2990 #[test]
2991 fn vite_react_babel_dependencies_skip_relative_and_protocol_entries() {
2992 let source = r#"
2993 import react from "@vitejs/plugin-react";
2994
2995 export default {
2996 plugins: [
2997 react({
2998 babel: {
2999 plugins: ["./local-plugin", "module:./local-prefixed", "http://example.com/plugin"],
3000 },
3001 }),
3002 ],
3003 };
3004 "#;
3005
3006 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3007
3008 assert!(deps.is_empty());
3009 }
3010
3011 #[test]
3012 fn normalize_config_path_relative_to_root() {
3013 let config_path = PathBuf::from("/project/vite.config.ts");
3014 let root = PathBuf::from("/project");
3015
3016 assert_eq!(
3017 normalize_config_path("./src/lib", &config_path, &root),
3018 Some("src/lib".to_string())
3019 );
3020 assert_eq!(
3021 normalize_config_path("/src/lib", &config_path, &root),
3022 Some("src/lib".to_string())
3023 );
3024 }
3025
3026 #[test]
3027 fn normalize_config_path_mixed_separators_and_parent_dirs() {
3028 let config_path = PathBuf::from("/project/config/vite.config.ts");
3029 let root = PathBuf::from("/project");
3030
3031 assert_eq!(
3032 normalize_config_path(".\\src\\..\\app\\lib", &config_path, &root),
3033 Some("config/app/lib".to_string())
3034 );
3035 }
3036
3037 #[test]
3038 fn normalize_config_path_leading_slash_stays_project_relative() {
3039 let config_path = PathBuf::from("/project/vite.config.ts");
3040 let root = PathBuf::from("/project");
3041
3042 assert_eq!(
3043 normalize_config_path("/src\\lib", &config_path, &root),
3044 Some("src/lib".to_string())
3045 );
3046 }
3047
3048 #[test]
3049 fn json_wrapped_in_parens_string() {
3050 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
3051 let val = extract_config_string(source, &js_path(), &["extends"]);
3052 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
3053 }
3054
3055 #[test]
3056 fn json_wrapped_in_parens_nested_array() {
3057 let source =
3058 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
3059 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
3060 assert_eq!(types, vec!["node", "jest"]);
3061
3062 let include = extract_config_string_array(source, &js_path(), &["include"]);
3063 assert_eq!(include, vec!["src/**/*"]);
3064 }
3065
3066 #[test]
3067 fn json_wrapped_in_parens_object_keys() {
3068 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
3069 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3070 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
3071 }
3072
3073 fn json_path() -> PathBuf {
3074 PathBuf::from("config.json")
3075 }
3076
3077 #[test]
3078 fn json_file_parsed_correctly() {
3079 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
3080 let val = extract_config_string(source, &json_path(), &["key"]);
3081 assert_eq!(val, Some("value".to_string()));
3082
3083 let list = extract_config_string_array(source, &json_path(), &["list"]);
3084 assert_eq!(list, vec!["a", "b"]);
3085 }
3086
3087 #[test]
3088 fn jsonc_file_parsed_correctly() {
3089 let source = r#"{"key": "value"}"#;
3090 let path = PathBuf::from("tsconfig.jsonc");
3091 let val = extract_config_string(source, &path, &["key"]);
3092 assert_eq!(val, Some("value".to_string()));
3093 }
3094
3095 #[test]
3096 fn extract_define_config_arrow_function() {
3097 let source = r#"
3098 import { defineConfig } from 'vite';
3099 export default defineConfig(() => ({
3100 test: {
3101 include: ["**/*.test.ts"]
3102 }
3103 }));
3104 "#;
3105 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
3106 assert_eq!(include, vec!["**/*.test.ts"]);
3107 }
3108
3109 #[test]
3110 fn extract_config_from_default_export_function_declaration() {
3111 let source = r#"
3112 export default function createConfig() {
3113 return {
3114 clientModules: ["./src/client/global.js"]
3115 };
3116 }
3117 "#;
3118
3119 let client_modules = extract_config_string_array(source, &ts_path(), &["clientModules"]);
3120 assert_eq!(client_modules, vec!["./src/client/global.js"]);
3121 }
3122
3123 #[test]
3124 fn extract_config_from_default_export_async_function_declaration() {
3125 let source = r#"
3126 export default async function createConfigAsync() {
3127 return {
3128 docs: {
3129 path: "knowledge"
3130 }
3131 };
3132 }
3133 "#;
3134
3135 let docs_path = extract_config_string(source, &ts_path(), &["docs", "path"]);
3136 assert_eq!(docs_path, Some("knowledge".to_string()));
3137 }
3138
3139 #[test]
3140 fn extract_config_from_exported_arrow_function_identifier() {
3141 let source = r#"
3142 const config = async () => {
3143 return {
3144 themes: ["classic"]
3145 };
3146 };
3147
3148 export default config;
3149 "#;
3150
3151 let themes = extract_config_shallow_strings(source, &ts_path(), "themes");
3152 assert_eq!(themes, vec!["classic"]);
3153 }
3154
3155 #[test]
3156 fn module_exports_nested_string() {
3157 let source = r#"
3158 module.exports = {
3159 resolve: {
3160 alias: {
3161 "@": "./src"
3162 }
3163 }
3164 };
3165 "#;
3166 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
3167 assert_eq!(val, Some("./src".to_string()));
3168 }
3169
3170 #[test]
3171 fn property_strings_nested_objects() {
3172 let source = r#"
3173 export default {
3174 plugins: {
3175 group1: { a: "val-a" },
3176 group2: { b: "val-b" }
3177 }
3178 };
3179 "#;
3180 let values = extract_config_property_strings(source, &js_path(), "plugins");
3181 assert!(values.contains(&"val-a".to_string()));
3182 assert!(values.contains(&"val-b".to_string()));
3183 }
3184
3185 #[test]
3186 fn property_strings_missing_key_returns_empty() {
3187 let source = r#"export default { other: "value" };"#;
3188 let values = extract_config_property_strings(source, &js_path(), "missing");
3189 assert!(values.is_empty());
3190 }
3191
3192 #[test]
3193 fn shallow_strings_tuple_array() {
3194 let source = r#"
3195 module.exports = {
3196 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
3197 };
3198 "#;
3199 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
3200 assert_eq!(values, vec!["default", "jest-junit"]);
3201 assert!(!values.contains(&"reports".to_string()));
3202 }
3203
3204 #[test]
3205 fn shallow_strings_single_string() {
3206 let source = r#"export default { preset: "ts-jest" };"#;
3207 let values = extract_config_shallow_strings(source, &js_path(), "preset");
3208 assert_eq!(values, vec!["ts-jest"]);
3209 }
3210
3211 #[test]
3212 fn shallow_strings_missing_key() {
3213 let source = r#"export default { other: "val" };"#;
3214 let values = extract_config_shallow_strings(source, &js_path(), "missing");
3215 assert!(values.is_empty());
3216 }
3217
3218 #[test]
3219 fn shallow_strings_or_object_property_alias_objects() {
3220 let source = r#"
3221 export default {
3222 jsPlugins: [
3223 "eslint-plugin-playwright",
3224 ["eslint-plugin-regexp", { rules: {} }],
3225 { name: "short", specifier: "eslint-plugin-with-long-name" }
3226 ]
3227 };
3228 "#;
3229 let values = extract_config_shallow_strings_or_object_property(
3230 source,
3231 &ts_path(),
3232 "jsPlugins",
3233 "specifier",
3234 );
3235 assert_eq!(
3236 values,
3237 vec![
3238 "eslint-plugin-playwright",
3239 "eslint-plugin-regexp",
3240 "eslint-plugin-with-long-name"
3241 ]
3242 );
3243 }
3244
3245 #[test]
3246 fn nested_shallow_strings_vitest_reporters() {
3247 let source = r#"
3248 export default {
3249 test: {
3250 reporters: ["default", "vitest-sonar-reporter"]
3251 }
3252 };
3253 "#;
3254 let values =
3255 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3256 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
3257 }
3258
3259 #[test]
3260 fn nested_shallow_strings_tuple_format() {
3261 let source = r#"
3262 export default {
3263 test: {
3264 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
3265 }
3266 };
3267 "#;
3268 let values =
3269 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3270 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
3271 }
3272
3273 #[test]
3274 fn nested_shallow_strings_missing_outer() {
3275 let source = r"export default { other: {} };";
3276 let values =
3277 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3278 assert!(values.is_empty());
3279 }
3280
3281 #[test]
3282 fn nested_shallow_strings_missing_inner() {
3283 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
3284 let values =
3285 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3286 assert!(values.is_empty());
3287 }
3288
3289 #[test]
3290 fn string_or_array_missing_path() {
3291 let source = r"export default {};";
3292 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3293 assert!(result.is_empty());
3294 }
3295
3296 #[test]
3297 fn string_or_array_non_string_values() {
3298 let source = r"export default { entry: [42, true] };";
3299 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3300 assert!(result.is_empty());
3301 }
3302
3303 #[test]
3304 fn array_nested_extraction() {
3305 let source = r#"
3306 export default defineConfig({
3307 test: {
3308 projects: [
3309 {
3310 test: {
3311 setupFiles: ["./test/setup-a.ts"]
3312 }
3313 },
3314 {
3315 test: {
3316 setupFiles: "./test/setup-b.ts"
3317 }
3318 }
3319 ]
3320 }
3321 });
3322 "#;
3323 let results = extract_config_array_nested_string_or_array(
3324 source,
3325 &ts_path(),
3326 &["test", "projects"],
3327 &["test", "setupFiles"],
3328 );
3329 assert!(results.contains(&"./test/setup-a.ts".to_string()));
3330 assert!(results.contains(&"./test/setup-b.ts".to_string()));
3331 }
3332
3333 #[test]
3334 fn array_nested_empty_when_no_array() {
3335 let source = r#"export default { test: { projects: "not-an-array" } };"#;
3336 let results = extract_config_array_nested_string_or_array(
3337 source,
3338 &js_path(),
3339 &["test", "projects"],
3340 &["test", "setupFiles"],
3341 );
3342 assert!(results.is_empty());
3343 }
3344
3345 #[test]
3346 fn object_nested_extraction() {
3347 let source = r#"{
3348 "projects": {
3349 "app-one": {
3350 "architect": {
3351 "build": {
3352 "options": {
3353 "styles": ["src/styles.css"]
3354 }
3355 }
3356 }
3357 }
3358 }
3359 }"#;
3360 let results = extract_config_object_nested_string_or_array(
3361 source,
3362 &json_path(),
3363 &["projects"],
3364 &["architect", "build", "options", "styles"],
3365 );
3366 assert_eq!(results, vec!["src/styles.css"]);
3367 }
3368
3369 #[test]
3370 fn array_with_object_input_form_extracted() {
3371 let source = r#"{
3372 "projects": {
3373 "app": {
3374 "architect": {
3375 "build": {
3376 "options": {
3377 "styles": [
3378 "src/styles.scss",
3379 { "input": "src/theme.scss", "bundleName": "theme", "inject": false },
3380 { "bundleName": "lazy-only" }
3381 ]
3382 }
3383 }
3384 }
3385 }
3386 }
3387 }"#;
3388 let results = extract_config_object_nested_string_or_array(
3389 source,
3390 &json_path(),
3391 &["projects"],
3392 &["architect", "build", "options", "styles"],
3393 );
3394 assert!(
3395 results.contains(&"src/styles.scss".to_string()),
3396 "string form must still work: {results:?}"
3397 );
3398 assert!(
3399 results.contains(&"src/theme.scss".to_string()),
3400 "object form with `input` must be extracted: {results:?}"
3401 );
3402 assert!(
3403 !results.contains(&"lazy-only".to_string()),
3404 "bundleName must not be misinterpreted as a path: {results:?}"
3405 );
3406 assert!(
3407 !results.contains(&"theme".to_string()),
3408 "bundleName from full object must not leak: {results:?}"
3409 );
3410 }
3411
3412 #[test]
3413 fn object_nested_strings_extraction() {
3414 let source = r#"{
3415 "targets": {
3416 "build": {
3417 "executor": "@angular/build:application"
3418 },
3419 "test": {
3420 "executor": "@nx/vite:test"
3421 }
3422 }
3423 }"#;
3424 let results =
3425 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
3426 assert!(results.contains(&"@angular/build:application".to_string()));
3427 assert!(results.contains(&"@nx/vite:test".to_string()));
3428 }
3429
3430 #[test]
3431 fn require_strings_direct_call() {
3432 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
3433 let deps = extract_config_require_strings(source, &js_path(), "adapter");
3434 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
3435 }
3436
3437 #[test]
3438 fn require_strings_no_matching_key() {
3439 let source = r"module.exports = { other: require('something') };";
3440 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3441 assert!(deps.is_empty());
3442 }
3443
3444 #[test]
3445 fn extract_imports_no_imports() {
3446 let source = r"export default {};";
3447 let imports = extract_imports(source, &js_path());
3448 assert!(imports.is_empty());
3449 }
3450
3451 #[test]
3452 fn extract_imports_side_effect_import() {
3453 let source = r"
3454 import 'polyfill';
3455 import './local-setup';
3456 export default {};
3457 ";
3458 let imports = extract_imports(source, &js_path());
3459 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
3460 }
3461
3462 #[test]
3463 fn extract_imports_mixed_specifiers() {
3464 let source = r"
3465 import defaultExport from 'module-a';
3466 import { named } from 'module-b';
3467 import * as ns from 'module-c';
3468 export default {};
3469 ";
3470 let imports = extract_imports(source, &js_path());
3471 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
3472 }
3473
3474 #[test]
3475 fn template_literal_in_string_or_array() {
3476 let source = r"export default { entry: `./src/index.ts` };";
3477 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
3478 assert_eq!(result, vec!["./src/index.ts"]);
3479 }
3480
3481 #[test]
3482 fn template_literal_in_config_string() {
3483 let source = r"export default { testDir: `./tests` };";
3484 let val = extract_config_string(source, &js_path(), &["testDir"]);
3485 assert_eq!(val, Some("./tests".to_string()));
3486 }
3487
3488 #[test]
3489 fn template_literal_command_recovers_static_command_tokens() {
3490 let source = r"
3491 const PORT = 3000;
3492 export default {
3493 webServer: {
3494 command: `pnpm exec srvx --port ${PORT} --hostname 127.0.0.1`
3495 }
3496 };
3497 ";
3498 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
3499 assert_eq!(
3500 val,
3501 Some("pnpm exec srvx --port --hostname 127.0.0.1".to_string())
3502 );
3503 }
3504
3505 #[test]
3506 fn template_literal_command_skips_dynamic_prefix() {
3507 let source = r"
3508 export default {
3509 webServer: { command: `${serverCommand} && pnpm exec srvx` }
3510 };
3511 ";
3512 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
3513 assert!(val.is_none());
3514 }
3515
3516 #[test]
3517 fn template_literal_command_skips_split_static_token() {
3518 let source = r"
3519 export default {
3520 webServer: { command: `pnpm exec sr${part}vx --port 3000` }
3521 };
3522 ";
3523 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
3524 assert!(val.is_none());
3525 }
3526
3527 #[test]
3528 fn array_object_command_pairs_recover_template_command() {
3529 let source = r"
3530 const PORT = 3000;
3531 export default {
3532 webServer: [
3533 {
3534 command: `pnpm exec srvx --port ${PORT}`,
3535 cwd: 'apps/web'
3536 }
3537 ]
3538 };
3539 ";
3540 let pairs = extract_config_array_object_command_pairs(
3541 source,
3542 &ts_path(),
3543 &["webServer"],
3544 "command",
3545 "cwd",
3546 );
3547 assert_eq!(
3548 pairs,
3549 vec![(
3550 "pnpm exec srvx --port ".to_string(),
3551 Some("apps/web".to_string())
3552 )]
3553 );
3554 }
3555
3556 #[test]
3557 fn nested_string_array_empty_path() {
3558 let source = r#"export default { items: ["a", "b"] };"#;
3559 let result = extract_config_string_array(source, &js_path(), &[]);
3560 assert!(result.is_empty());
3561 }
3562
3563 #[test]
3564 fn nested_string_empty_path() {
3565 let source = r#"export default { key: "val" };"#;
3566 let result = extract_config_string(source, &js_path(), &[]);
3567 assert!(result.is_none());
3568 }
3569
3570 #[test]
3571 fn object_keys_empty_path() {
3572 let source = r"export default { plugins: {} };";
3573 let result = extract_config_object_keys(source, &js_path(), &[]);
3574 assert!(result.is_empty());
3575 }
3576
3577 #[test]
3578 fn no_config_object_returns_empty() {
3579 let source = r"const x = 42;";
3580 let result = extract_config_string(source, &js_path(), &["key"]);
3581 assert!(result.is_none());
3582
3583 let arr = extract_config_string_array(source, &js_path(), &["items"]);
3584 assert!(arr.is_empty());
3585
3586 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3587 assert!(keys.is_empty());
3588 }
3589
3590 #[test]
3591 fn property_with_string_key() {
3592 let source = r#"export default { "string-key": "value" };"#;
3593 let val = extract_config_string(source, &js_path(), &["string-key"]);
3594 assert_eq!(val, Some("value".to_string()));
3595 }
3596
3597 #[test]
3598 fn nested_navigation_through_non_object() {
3599 let source = r#"export default { level1: "not-an-object" };"#;
3600 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
3601 assert!(val.is_none());
3602 }
3603
3604 #[test]
3605 fn variable_reference_untyped() {
3606 let source = r#"
3607 const config = {
3608 testDir: "./tests"
3609 };
3610 export default config;
3611 "#;
3612 let val = extract_config_string(source, &js_path(), &["testDir"]);
3613 assert_eq!(val, Some("./tests".to_string()));
3614 }
3615
3616 #[test]
3617 fn variable_reference_with_type_annotation() {
3618 let source = r#"
3619 import type { StorybookConfig } from '@storybook/react-vite';
3620 const config: StorybookConfig = {
3621 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
3622 framework: "@storybook/react-vite"
3623 };
3624 export default config;
3625 "#;
3626 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
3627 assert_eq!(
3628 addons,
3629 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
3630 );
3631
3632 let framework = extract_config_string(source, &ts_path(), &["framework"]);
3633 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
3634 }
3635
3636 #[test]
3637 fn variable_reference_with_define_config() {
3638 let source = r#"
3639 import { defineConfig } from 'vitest/config';
3640 const config = defineConfig({
3641 test: {
3642 include: ["**/*.test.ts"]
3643 }
3644 });
3645 export default config;
3646 "#;
3647 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
3648 assert_eq!(include, vec!["**/*.test.ts"]);
3649 }
3650
3651 #[test]
3652 fn ts_satisfies_direct_export() {
3653 let source = r#"
3654 export default {
3655 testDir: "./tests"
3656 } satisfies PlaywrightTestConfig;
3657 "#;
3658 let val = extract_config_string(source, &ts_path(), &["testDir"]);
3659 assert_eq!(val, Some("./tests".to_string()));
3660 }
3661
3662 #[test]
3663 fn ts_as_direct_export() {
3664 let source = r#"
3665 export default {
3666 testDir: "./tests"
3667 } as const;
3668 "#;
3669 let val = extract_config_string(source, &ts_path(), &["testDir"]);
3670 assert_eq!(val, Some("./tests".to_string()));
3671 }
3672
3673 fn aliases(source: &str) -> Vec<(String, String)> {
3676 extract_config_aliases(source, &js_path(), &["resolve", "alias"])
3677 }
3678
3679 #[test]
3680 fn aliases_inline_object_still_extracted() {
3681 let source = r#"
3683 export default defineConfig({
3684 resolve: { alias: { "@": "./src", utils: "../../utils" } }
3685 });
3686 "#;
3687 let mut got = aliases(source);
3688 got.sort();
3689 assert_eq!(
3690 got,
3691 vec![
3692 ("@".to_string(), "./src".to_string()),
3693 ("utils".to_string(), "../../utils".to_string()),
3694 ]
3695 );
3696 }
3697
3698 #[test]
3699 fn aliases_inline_array_still_extracted() {
3700 let source = r#"
3701 export default defineConfig({
3702 resolve: { alias: [{ find: "@", replacement: "./src" }] }
3703 });
3704 "#;
3705 assert_eq!(
3706 aliases(source),
3707 vec![("@".to_string(), "./src".to_string())]
3708 );
3709 }
3710
3711 #[test]
3712 fn aliases_local_const_array_identifier() {
3713 let source = r#"
3714 const sharedAliases = [{ find: "@", replacement: "./src" }];
3715 export default defineConfig({ resolve: { alias: sharedAliases } });
3716 "#;
3717 assert_eq!(
3718 aliases(source),
3719 vec![("@".to_string(), "./src".to_string())]
3720 );
3721 }
3722
3723 #[test]
3724 fn aliases_local_const_object_identifier() {
3725 let source = r#"
3726 const sharedAliases = { "@": "./src" };
3727 export default defineConfig({ resolve: { alias: sharedAliases } });
3728 "#;
3729 assert_eq!(
3730 aliases(source),
3731 vec![("@".to_string(), "./src".to_string())]
3732 );
3733 }
3734
3735 #[test]
3736 fn aliases_array_spread_of_identifiers_and_inline() {
3737 let source = r##"
3738 const a = [{ find: "@", replacement: "./src" }];
3739 const b = [{ find: "~", replacement: "./lib" }];
3740 export default defineConfig({
3741 resolve: { alias: [...a, ...b, { find: "#", replacement: "./test" }] }
3742 });
3743 "##;
3744 let mut got = aliases(source);
3745 got.sort();
3746 assert_eq!(
3747 got,
3748 vec![
3749 ("#".to_string(), "./test".to_string()),
3750 ("@".to_string(), "./src".to_string()),
3751 ("~".to_string(), "./lib".to_string()),
3752 ]
3753 );
3754 }
3755
3756 #[test]
3757 fn aliases_object_spread_of_identifier_and_inline() {
3758 let source = r#"
3759 const base = { "@": "./src" };
3760 export default defineConfig({
3761 resolve: { alias: { ...base, "~": "./lib" } }
3762 });
3763 "#;
3764 let mut got = aliases(source);
3765 got.sort();
3766 assert_eq!(
3767 got,
3768 vec![
3769 ("@".to_string(), "./src".to_string()),
3770 ("~".to_string(), "./lib".to_string()),
3771 ]
3772 );
3773 }
3774
3775 #[test]
3776 fn aliases_local_const_chained_identifier() {
3777 let source = r#"
3779 const real = [{ find: "@", replacement: "./src" }];
3780 const alias2 = real;
3781 export default defineConfig({ resolve: { alias: alias2 } });
3782 "#;
3783 assert_eq!(
3784 aliases(source),
3785 vec![("@".to_string(), "./src".to_string())]
3786 );
3787 }
3788
3789 #[test]
3790 fn aliases_imported_named_identifier_from_sibling() {
3791 let dir = tempfile::tempdir().unwrap();
3792 std::fs::write(
3793 dir.path().join("vite.shared.js"),
3794 r#"export const sharedAliases = [
3795 { find: "@", replacement: new URL("./src", import.meta.url).pathname },
3796 ];"#,
3797 )
3798 .unwrap();
3799 let config = dir.path().join("vite.config.js");
3800 let source = r#"
3801 import { defineConfig } from "vite";
3802 import { sharedAliases } from "./vite.shared.js";
3803 export default defineConfig({ resolve: { alias: sharedAliases } });
3804 "#;
3805 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
3806 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
3807 }
3808
3809 #[test]
3810 fn aliases_imported_extensionless_specifier_probed() {
3811 let dir = tempfile::tempdir().unwrap();
3812 std::fs::write(
3813 dir.path().join("aliases.mjs"),
3814 r#"export const sharedAliases = { "@": "./src" };"#,
3815 )
3816 .unwrap();
3817 let config = dir.path().join("vite.config.ts");
3818 let source = r#"
3819 import { sharedAliases } from "./aliases";
3820 export default defineConfig({ resolve: { alias: sharedAliases } });
3821 "#;
3822 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
3823 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
3824 }
3825
3826 #[test]
3827 fn aliases_imported_default_export_from_sibling() {
3828 let dir = tempfile::tempdir().unwrap();
3829 std::fs::write(
3830 dir.path().join("aliases.js"),
3831 r#"export default [{ find: "@", replacement: "./src" }];"#,
3832 )
3833 .unwrap();
3834 let config = dir.path().join("vite.config.js");
3835 let source = r#"
3836 import sharedAliases from "./aliases.js";
3837 export default defineConfig({ resolve: { alias: sharedAliases } });
3838 "#;
3839 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
3840 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
3841 }
3842
3843 #[test]
3844 fn aliases_imported_spread_from_two_siblings() {
3845 let dir = tempfile::tempdir().unwrap();
3846 std::fs::write(
3847 dir.path().join("a.js"),
3848 r#"export const a = [{ find: "@", replacement: "./src" }];"#,
3849 )
3850 .unwrap();
3851 std::fs::write(
3852 dir.path().join("b.js"),
3853 r#"export const b = [{ find: "~", replacement: "./lib" }];"#,
3854 )
3855 .unwrap();
3856 let config = dir.path().join("vite.config.js");
3857 let source = r#"
3858 import { a } from "./a.js";
3859 import { b } from "./b.js";
3860 export default defineConfig({ resolve: { alias: [...a, ...b] } });
3861 "#;
3862 let mut got = extract_config_aliases(source, &config, &["resolve", "alias"]);
3863 got.sort();
3864 assert_eq!(
3865 got,
3866 vec![
3867 ("@".to_string(), "./src".to_string()),
3868 ("~".to_string(), "./lib".to_string()),
3869 ]
3870 );
3871 }
3872
3873 #[test]
3874 fn aliases_import_cycle_terminates() {
3875 let dir = tempfile::tempdir().unwrap();
3878 std::fs::write(
3879 dir.path().join("a.js"),
3880 r#"import { b } from "./b.js";
3881 export const a = [{ find: "@", replacement: "./src" }, ...b];"#,
3882 )
3883 .unwrap();
3884 std::fs::write(
3885 dir.path().join("b.js"),
3886 r#"import { a } from "./a.js";
3887 export const b = [...a];"#,
3888 )
3889 .unwrap();
3890 let config = dir.path().join("vite.config.js");
3891 let source = r#"
3892 import { a } from "./a.js";
3893 export default defineConfig({ resolve: { alias: a } });
3894 "#;
3895 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
3896 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
3897 }
3898
3899 #[test]
3900 fn aliases_non_relative_import_not_followed() {
3901 let source = r#"
3904 import { sharedAliases } from "some-pkg";
3905 export default defineConfig({ resolve: { alias: sharedAliases } });
3906 "#;
3907 let dir = tempfile::tempdir().unwrap();
3908 let config = dir.path().join("vite.config.js");
3909 assert!(extract_config_aliases(source, &config, &["resolve", "alias"]).is_empty());
3910 }
3911
3912 #[test]
3913 fn aliases_object_array_value_takes_first_entry() {
3914 let source = r#"
3919 export default {
3920 compilerOptions: { paths: { "@/*": ["./src/*"], "~/*": ["./lib/*", "./vendor/*"] } }
3921 };
3922 "#;
3923 let mut got = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
3924 got.sort();
3925 assert_eq!(
3926 got,
3927 vec![
3928 ("@/*".to_string(), "./src/*".to_string()),
3929 ("~/*".to_string(), "./lib/*".to_string()),
3930 ]
3931 );
3932 }
3933
3934 #[test]
3935 fn aliases_kinded_preserves_is_bare_through_resolution() {
3936 let source = r#"
3939 const a = [{ find: "lodash-es", replacement: "lodash" }];
3940 export default defineConfig({
3941 resolve: { alias: [...a, { find: "@", replacement: "./src" }] }
3942 });
3943 "#;
3944 let mut got = extract_config_aliases_kinded(source, &js_path(), &["resolve", "alias"]);
3945 got.sort();
3946 assert_eq!(
3947 got,
3948 vec![
3949 ("@".to_string(), "./src".to_string(), false),
3950 ("lodash-es".to_string(), "lodash".to_string(), true),
3951 ]
3952 );
3953 }
3954
3955 #[test]
3956 fn aliases_kinded_preserves_is_bare_through_imported_spread() {
3957 let dir = tempfile::tempdir().unwrap();
3958 std::fs::write(
3959 dir.path().join("aliases.js"),
3960 r#"export const packageAliases = [{ find: "lodash-es", replacement: "lodash" }];"#,
3961 )
3962 .unwrap();
3963 let config = dir.path().join("vite.config.js");
3964 let source = r#"
3965 import { packageAliases } from "./aliases.js";
3966 export default defineConfig({
3967 resolve: { alias: [...packageAliases, { find: "@", replacement: "./src" }] }
3968 });
3969 "#;
3970 let mut got = extract_config_aliases_kinded(source, &config, &["resolve", "alias"]);
3971 got.sort();
3972 assert_eq!(
3973 got,
3974 vec![
3975 ("@".to_string(), "./src".to_string(), false),
3976 ("lodash-es".to_string(), "lodash".to_string(), true),
3977 ]
3978 );
3979 }
3980}