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