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