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