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