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 if arrow.expression {
1254 arrow.body.statements.first().and_then(|stmt| {
1255 if let Statement::ExpressionStatement(expr_stmt) = stmt {
1256 extract_object_from_expression(&expr_stmt.expression)
1257 } else {
1258 None
1259 }
1260 })
1261 } else {
1262 extract_object_from_function_body(&arrow.body)
1263 }
1264}
1265
1266fn extract_object_from_function<'a>(func: &'a Function<'a>) -> Option<&'a ObjectExpression<'a>> {
1267 func.body
1268 .as_ref()
1269 .and_then(|body| extract_object_from_function_body(body))
1270}
1271
1272fn extract_object_from_function_body<'a>(
1281 body: &'a FunctionBody<'a>,
1282) -> Option<&'a ObjectExpression<'a>> {
1283 find_top_level_returned_object(&body.statements)
1284 .or_else(|| find_returned_object(&body.statements, MAX_CONFIG_BRANCH_DEPTH))
1285}
1286
1287const MAX_CONFIG_BRANCH_DEPTH: u8 = 4;
1289
1290fn find_top_level_returned_object<'a>(
1292 statements: &'a [Statement<'a>],
1293) -> Option<&'a ObjectExpression<'a>> {
1294 statements.iter().find_map(|stmt| match stmt {
1295 Statement::ReturnStatement(ret) => ret
1296 .argument
1297 .as_ref()
1298 .and_then(|argument| extract_object_from_expression(argument)),
1299 _ => None,
1300 })
1301}
1302
1303fn find_returned_object<'a>(
1312 statements: &'a [Statement<'a>],
1313 depth: u8,
1314) -> Option<&'a ObjectExpression<'a>> {
1315 if depth == 0 {
1316 return None;
1317 }
1318 for stmt in statements {
1319 let found = match stmt {
1320 Statement::ReturnStatement(ret) => ret
1321 .argument
1322 .as_ref()
1323 .and_then(|argument| extract_object_from_expression(argument)),
1324 Statement::BlockStatement(block) => find_returned_object(&block.body, depth - 1),
1325 Statement::IfStatement(if_stmt) => {
1326 find_returned_object_in_statement(&if_stmt.consequent, depth - 1).or_else(|| {
1327 if_stmt
1328 .alternate
1329 .as_ref()
1330 .and_then(|alt| find_returned_object_in_statement(alt, depth - 1))
1331 })
1332 }
1333 Statement::TryStatement(try_stmt) => {
1334 find_returned_object(&try_stmt.block.body, depth - 1)
1335 .or_else(|| {
1336 try_stmt
1337 .handler
1338 .as_ref()
1339 .and_then(|handler| find_returned_object(&handler.body.body, depth - 1))
1340 })
1341 .or_else(|| {
1342 try_stmt
1343 .finalizer
1344 .as_ref()
1345 .and_then(|finalizer| find_returned_object(&finalizer.body, depth - 1))
1346 })
1347 }
1348 Statement::SwitchStatement(switch) => switch
1349 .cases
1350 .iter()
1351 .find_map(|case| find_returned_object(&case.consequent, depth - 1)),
1352 _ => None,
1353 };
1354 if found.is_some() {
1355 return found;
1356 }
1357 }
1358 None
1359}
1360
1361fn find_returned_object_in_statement<'a>(
1362 stmt: &'a Statement<'a>,
1363 depth: u8,
1364) -> Option<&'a ObjectExpression<'a>> {
1365 match stmt {
1366 Statement::BlockStatement(block) => find_returned_object(&block.body, depth),
1367 other => find_returned_object(std::slice::from_ref(other), depth),
1368 }
1369}
1370
1371fn is_module_exports_target(target: &AssignmentTarget) -> bool {
1373 if let AssignmentTarget::StaticMemberExpression(member) = target
1374 && let Expression::Identifier(obj) = &member.object
1375 {
1376 return obj.name == "module" && member.property.name == "exports";
1377 }
1378 false
1379}
1380
1381fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
1385 match expr {
1386 Expression::Identifier(id) => Some(&id.name),
1387 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
1388 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
1389 _ => None,
1390 }
1391}
1392
1393pub(crate) fn find_variable_init_object<'a>(
1398 program: &'a Program,
1399 name: &str,
1400) -> Option<&'a ObjectExpression<'a>> {
1401 for decl in top_level_variable_declarations(program) {
1402 for declarator in &decl.declarations {
1403 if let BindingPattern::BindingIdentifier(id) = &declarator.id
1404 && id.name == name
1405 && let Some(init) = &declarator.init
1406 {
1407 return extract_object_from_expression(init);
1408 }
1409 }
1410 }
1411 None
1412}
1413
1414fn top_level_variable_declarations<'a>(
1417 program: &'a Program<'a>,
1418) -> impl Iterator<Item = &'a VariableDeclaration<'a>> {
1419 program.body.iter().filter_map(|stmt| match stmt {
1420 Statement::VariableDeclaration(decl) => Some(&**decl),
1421 Statement::ExportNamedDeclaration(export) => match &export.declaration {
1422 Some(Declaration::VariableDeclaration(decl)) => Some(&**decl),
1423 _ => None,
1424 },
1425 _ => None,
1426 })
1427}
1428
1429pub(crate) fn find_stable_binding_init<'a>(
1441 program: &'a Program<'a>,
1442 name: &str,
1443) -> Option<&'a Expression<'a>> {
1444 if !holds_one_stable_object(program, name) {
1445 return None;
1446 }
1447 find_variable_init_expression(program, name)
1448}
1449
1450fn holds_one_stable_object(program: &Program<'_>, name: &str) -> bool {
1453 if !declares_top_level_const_or_let(program, name) {
1454 return false;
1455 }
1456 let mut usage = NameUsage::new(name);
1457 usage.visit_program(program);
1458 usage.bindings == 1 && usage.writes == 0
1459}
1460
1461fn declares_top_level_const_or_let(program: &Program<'_>, name: &str) -> bool {
1464 top_level_variable_declarations(program).any(|decl| {
1465 matches!(
1466 decl.kind,
1467 VariableDeclarationKind::Const | VariableDeclarationKind::Let
1468 ) && decl.declarations.iter().any(|declarator| {
1469 matches!(&declarator.id, BindingPattern::BindingIdentifier(id) if id.name == name)
1470 })
1471 })
1472}
1473
1474struct NameUsage<'n> {
1477 name: &'n str,
1478 bindings: usize,
1479 writes: usize,
1480 write_depth: usize,
1481}
1482
1483impl<'n> NameUsage<'n> {
1484 const fn new(name: &'n str) -> Self {
1485 Self {
1486 name,
1487 bindings: 0,
1488 writes: 0,
1489 write_depth: 0,
1490 }
1491 }
1492}
1493
1494impl<'a> Visit<'a> for NameUsage<'_> {
1495 fn visit_binding_identifier(&mut self, identifier: &BindingIdentifier<'a>) {
1496 if identifier.name == self.name {
1497 self.bindings += 1;
1498 }
1499 }
1500
1501 fn visit_identifier_reference(&mut self, identifier: &IdentifierReference<'a>) {
1502 if self.write_depth > 0 && identifier.name == self.name {
1503 self.writes += 1;
1504 }
1505 }
1506
1507 fn visit_assignment_target(&mut self, target: &AssignmentTarget<'a>) {
1508 self.write_depth += 1;
1509 walk::walk_assignment_target(self, target);
1510 self.write_depth -= 1;
1511 }
1512
1513 fn visit_update_expression(&mut self, expression: &UpdateExpression<'a>) {
1514 self.write_depth += 1;
1515 walk::walk_update_expression(self, expression);
1516 self.write_depth -= 1;
1517 }
1518
1519 fn visit_unary_expression(&mut self, expression: &UnaryExpression<'a>) {
1520 if expression.operator != UnaryOperator::Delete {
1521 walk::walk_unary_expression(self, expression);
1522 return;
1523 }
1524 self.write_depth += 1;
1525 walk::walk_unary_expression(self, expression);
1526 self.write_depth -= 1;
1527 }
1528}
1529
1530fn resolve_wrapped_config_object<'a>(
1543 program: &'a Program,
1544 expr: &'a Expression<'a>,
1545) -> Option<&'a ObjectExpression<'a>> {
1546 let call = match expr {
1547 Expression::CallExpression(call) => call,
1548 Expression::ParenthesizedExpression(paren) => {
1549 return resolve_wrapped_config_object(program, &paren.expression);
1550 }
1551 Expression::TSSatisfiesExpression(ts_sat) => {
1552 return resolve_wrapped_config_object(program, &ts_sat.expression);
1553 }
1554 Expression::TSAsExpression(ts_as) => {
1555 return resolve_wrapped_config_object(program, &ts_as.expression);
1556 }
1557 _ => return None,
1558 };
1559 for arg in &call.arguments {
1560 let Some(arg_expr) = arg.as_expression() else {
1561 continue;
1562 };
1563 if let Some(name) = unwrap_to_identifier_name(arg_expr)
1564 && let Some(obj) = find_variable_init_object(program, name)
1565 {
1566 return Some(obj);
1567 }
1568 if let Some(obj) = resolve_wrapped_config_object(program, arg_expr) {
1569 return Some(obj);
1570 }
1571 }
1572 None
1573}
1574
1575const MAX_CONFIG_WRAPPER_DEPTH: u8 = 3;
1580
1581fn resolve_call_config_object<'a>(
1592 program: &'a Program,
1593 expr: &'a Expression<'a>,
1594 depth: u8,
1595) -> Option<&'a ObjectExpression<'a>> {
1596 if depth == 0 {
1597 return None;
1598 }
1599 let call = match expr {
1600 Expression::CallExpression(call) => call,
1601 Expression::ParenthesizedExpression(paren) => {
1602 return resolve_call_config_object(program, &paren.expression, depth);
1603 }
1604 Expression::TSSatisfiesExpression(ts_sat) => {
1605 return resolve_call_config_object(program, &ts_sat.expression, depth);
1606 }
1607 Expression::TSAsExpression(ts_as) => {
1608 return resolve_call_config_object(program, &ts_as.expression, depth);
1609 }
1610 _ => return None,
1611 };
1612
1613 call.arguments
1614 .iter()
1615 .filter_map(oxc_ast::ast::Argument::as_expression)
1616 .find_map(|arg| resolve_config_argument(program, arg, depth))
1617}
1618
1619fn resolve_config_argument<'a>(
1621 program: &'a Program,
1622 expr: &'a Expression<'a>,
1623 depth: u8,
1624) -> Option<&'a ObjectExpression<'a>> {
1625 match expr {
1626 Expression::ObjectExpression(obj) => Some(obj),
1627 Expression::ArrowFunctionExpression(arrow) => extract_object_from_arrow_function(arrow),
1628 Expression::FunctionExpression(func) => extract_object_from_function(func),
1629 Expression::ParenthesizedExpression(paren) => {
1630 resolve_config_argument(program, &paren.expression, depth)
1631 }
1632 Expression::TSSatisfiesExpression(ts_sat) => {
1633 resolve_config_argument(program, &ts_sat.expression, depth)
1634 }
1635 Expression::TSAsExpression(ts_as) => {
1636 resolve_config_argument(program, &ts_as.expression, depth)
1637 }
1638 Expression::CallExpression(_) => resolve_call_config_object(program, expr, depth - 1),
1639 _ => unwrap_to_identifier_name(expr)
1640 .and_then(|name| find_variable_init_object(program, name)),
1641 }
1642}
1643
1644pub(crate) fn find_property<'a>(
1646 obj: &'a ObjectExpression<'a>,
1647 key: &str,
1648) -> Option<&'a ObjectProperty<'a>> {
1649 for prop in &obj.properties {
1650 if let ObjectPropertyKind::ObjectProperty(p) = prop
1651 && property_key_matches(&p.key, key)
1652 {
1653 return Some(p);
1654 }
1655 }
1656 None
1657}
1658
1659fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
1661 match key {
1662 PropertyKey::StaticIdentifier(id) => id.name == name,
1663 PropertyKey::StringLiteral(s) => s.value == name,
1664 _ => false,
1665 }
1666}
1667
1668fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
1670 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
1671}
1672
1673fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
1675 find_property(obj, key)
1676 .map(|p| expression_to_string_array(&p.value))
1677 .unwrap_or_default()
1678}
1679
1680fn get_nested_string_array_from_object(
1682 obj: &ObjectExpression,
1683 path: &[&str],
1684) -> Option<Vec<String>> {
1685 if path.is_empty() {
1686 return None;
1687 }
1688 if path.len() == 1 {
1689 return Some(get_object_string_array_property(obj, path[0]));
1690 }
1691 let prop = find_property(obj, path[0])?;
1692 if let Expression::ObjectExpression(nested) = &prop.value {
1693 get_nested_string_array_from_object(nested, &path[1..])
1694 } else {
1695 None
1696 }
1697}
1698
1699fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
1701 if path.is_empty() {
1702 return None;
1703 }
1704 if path.len() == 1 {
1705 return get_object_string_property(obj, path[0]);
1706 }
1707 let prop = find_property(obj, path[0])?;
1708 if let Expression::ObjectExpression(nested) = &prop.value {
1709 get_nested_string_from_object(nested, &path[1..])
1710 } else {
1711 None
1712 }
1713}
1714
1715fn get_nested_command_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
1717 if path.is_empty() {
1718 return None;
1719 }
1720 if path.len() == 1 {
1721 return find_property(obj, path[0]).and_then(|prop| expression_to_command(&prop.value));
1722 }
1723 let prop = find_property(obj, path[0])?;
1724 if let Expression::ObjectExpression(nested) = &prop.value {
1725 get_nested_command_from_object(nested, &path[1..])
1726 } else {
1727 None
1728 }
1729}
1730
1731pub(crate) fn expression_to_string(expr: &Expression) -> Option<String> {
1733 match expr {
1734 Expression::StringLiteral(s) => Some(s.value.to_string()),
1735 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
1736 t.quasis.first().map(|q| q.value.raw.to_string())
1737 }
1738 _ => None,
1739 }
1740}
1741
1742fn expression_to_command(expr: &Expression) -> Option<String> {
1744 match expr {
1745 Expression::StringLiteral(s) => Some(s.value.to_string()),
1746 Expression::TemplateLiteral(template) => template_literal_to_command(template),
1747 Expression::ParenthesizedExpression(paren) => expression_to_command(&paren.expression),
1748 Expression::TSAsExpression(ts_as) => expression_to_command(&ts_as.expression),
1749 Expression::TSSatisfiesExpression(ts_sat) => expression_to_command(&ts_sat.expression),
1750 _ => None,
1751 }
1752}
1753
1754fn template_literal_to_command(template: &TemplateLiteral<'_>) -> Option<String> {
1755 let first = template.quasis.first()?.value.raw.as_str();
1756 if first.trim_start().is_empty() {
1757 return None;
1758 }
1759
1760 let mut command = String::new();
1761 for (idx, quasi) in template.quasis.iter().enumerate() {
1762 command.push_str(quasi.value.raw.as_str());
1763 if idx < template.expressions.len() {
1764 let next = template
1765 .quasis
1766 .get(idx + 1)
1767 .map_or("", |next| next.value.raw.as_str());
1768 if dynamic_template_boundary_splits_static_token(quasi.value.raw.as_str(), next) {
1769 return None;
1770 }
1771 command.push(' ');
1772 }
1773 }
1774
1775 Some(command)
1776}
1777
1778fn dynamic_template_boundary_splits_static_token(before: &str, after: &str) -> bool {
1779 before
1780 .chars()
1781 .next_back()
1782 .is_some_and(is_command_token_char)
1783 && after.chars().next().is_some_and(is_command_token_char)
1784}
1785
1786fn is_command_token_char(ch: char) -> bool {
1787 !ch.is_whitespace() && !matches!(ch, '&' | '|' | ';' | '"' | '\'')
1788}
1789
1790pub(crate) fn expression_to_path_string(expr: &Expression) -> Option<String> {
1792 match expr {
1793 Expression::ParenthesizedExpression(paren) => expression_to_path_string(&paren.expression),
1794 Expression::TSAsExpression(ts_as) => expression_to_path_string(&ts_as.expression),
1795 Expression::TSSatisfiesExpression(ts_sat) => expression_to_path_string(&ts_sat.expression),
1796 Expression::StaticMemberExpression(member) if member.property.name == "pathname" => {
1797 expression_to_path_string(&member.object)
1798 }
1799 Expression::CallExpression(call) => call_expression_to_path_string(call),
1800 Expression::NewExpression(new_expr) => new_expression_to_path_string(new_expr),
1801 _ => expression_to_string(expr),
1802 }
1803}
1804
1805fn call_expression_to_path_string(call: &CallExpression) -> Option<String> {
1806 if matches!(&call.callee, Expression::Identifier(id) if id.name == "fileURLToPath") {
1807 return call
1808 .arguments
1809 .first()
1810 .and_then(Argument::as_expression)
1811 .and_then(expression_to_path_string);
1812 }
1813
1814 let callee_name = match &call.callee {
1815 Expression::Identifier(id) => Some(id.name.as_str()),
1816 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
1817 _ => None,
1818 }?;
1819
1820 if !matches!(callee_name, "resolve" | "join") {
1821 return None;
1822 }
1823
1824 let mut segments = Vec::new();
1825 for (index, arg) in call.arguments.iter().enumerate() {
1826 let expr = arg.as_expression()?;
1827
1828 if is_dirname_anchor(expr) {
1829 if index == 0 {
1830 continue;
1831 }
1832 return None;
1833 }
1834
1835 segments.push(expression_to_string(expr)?);
1836 }
1837
1838 (!segments.is_empty()).then(|| join_path_segments(&segments))
1839}
1840
1841fn is_dirname_anchor(expr: &Expression) -> bool {
1846 match expr {
1847 Expression::Identifier(id) => id.name == "__dirname",
1848 Expression::StaticMemberExpression(member) => {
1849 member.property.name == "dirname" && is_import_meta_expression(&member.object)
1850 }
1851 _ => false,
1852 }
1853}
1854
1855fn is_import_meta_expression(expr: &Expression) -> bool {
1857 matches!(
1858 expr,
1859 Expression::MetaProperty(meta) if meta.meta.name == "import" && meta.property.name == "meta"
1860 )
1861}
1862
1863fn new_expression_to_path_string(new_expr: &NewExpression) -> Option<String> {
1864 if !matches!(&new_expr.callee, Expression::Identifier(id) if id.name == "URL") {
1865 return None;
1866 }
1867
1868 let source = new_expr
1869 .arguments
1870 .first()
1871 .and_then(Argument::as_expression)
1872 .and_then(expression_to_string)?;
1873
1874 let base = new_expr
1875 .arguments
1876 .get(1)
1877 .and_then(Argument::as_expression)?;
1878 is_import_meta_url_expression(base).then_some(source)
1879}
1880
1881fn is_import_meta_url_expression(expr: &Expression) -> bool {
1882 if let Expression::StaticMemberExpression(member) = expr {
1883 member.property.name == "url" && matches!(member.object, Expression::MetaProperty(_))
1884 } else {
1885 false
1886 }
1887}
1888
1889fn join_path_segments(segments: &[String]) -> String {
1890 let mut joined = PathBuf::new();
1891 for segment in segments {
1892 joined.push(segment);
1893 }
1894 joined.to_string_lossy().replace('\\', "/")
1895}
1896
1897fn expression_to_alias_pairs_kinded(expr: &Expression) -> Vec<(String, String, bool)> {
1902 match expr {
1903 Expression::ObjectExpression(obj) => obj
1904 .properties
1905 .iter()
1906 .filter_map(|prop| {
1907 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
1908 return None;
1909 };
1910 let find = property_key_to_string(&prop.key)?;
1911 let (replacement, is_bare) = alias_replacement_kinded(&prop.value)?;
1912 Some((find, replacement, is_bare))
1913 })
1914 .collect(),
1915 Expression::ArrayExpression(arr) => arr
1916 .elements
1917 .iter()
1918 .filter_map(|element| {
1919 let Expression::ObjectExpression(obj) = element.as_expression()? else {
1920 return None;
1921 };
1922 let find = find_property(obj, "find")
1923 .and_then(|prop| expression_to_string(&prop.value))?;
1924 let (replacement, is_bare) = find_property(obj, "replacement")
1925 .and_then(|prop| alias_replacement_kinded(&prop.value))?;
1926 Some((find, replacement, is_bare))
1927 })
1928 .collect(),
1929 _ => Vec::new(),
1930 }
1931}
1932
1933fn alias_replacement_kinded(expr: &Expression) -> Option<(String, bool)> {
1940 match expr {
1941 Expression::ParenthesizedExpression(paren) => alias_replacement_kinded(&paren.expression),
1942 Expression::TSAsExpression(ts_as) => alias_replacement_kinded(&ts_as.expression),
1943 Expression::TSSatisfiesExpression(ts_sat) => alias_replacement_kinded(&ts_sat.expression),
1944 Expression::StringLiteral(s) => {
1945 let value = s.value.to_string();
1946 let is_bare =
1947 !value.starts_with("./") && !value.starts_with("../") && !value.starts_with('/');
1948 Some((value, is_bare))
1949 }
1950 Expression::ArrayExpression(arr) => arr
1954 .elements
1955 .iter()
1956 .find_map(ArrayExpressionElement::as_expression)
1957 .and_then(alias_replacement_kinded),
1958 _ => expression_to_path_string(expr).map(|value| (value, false)),
1959 }
1960}
1961
1962const MAX_ALIAS_RESOLVE_DEPTH: usize = 8;
1968
1969const ALIAS_SIBLING_EXTS: [&str; 6] = ["js", "mjs", "cjs", "ts", "mts", "cts"];
1976
1977fn resolve_alias_pairs_kinded(
1994 program: &Program,
1995 config_path: &Path,
1996 expr: &Expression,
1997 visited: &mut FxHashSet<PathBuf>,
1998 depth: usize,
1999) -> Vec<(String, String, bool)> {
2000 match expr {
2001 Expression::ParenthesizedExpression(paren) => {
2002 resolve_alias_pairs_kinded(program, config_path, &paren.expression, visited, depth)
2003 }
2004 Expression::TSAsExpression(ts_as) => {
2005 resolve_alias_pairs_kinded(program, config_path, &ts_as.expression, visited, depth)
2006 }
2007 Expression::TSSatisfiesExpression(ts_sat) => {
2008 resolve_alias_pairs_kinded(program, config_path, &ts_sat.expression, visited, depth)
2009 }
2010 Expression::ObjectExpression(obj) => {
2011 resolve_object_alias_pairs_kinded(program, config_path, obj, visited, depth)
2012 }
2013 Expression::ArrayExpression(arr) => {
2014 resolve_array_alias_pairs_kinded(program, config_path, arr, visited, depth)
2015 }
2016 Expression::Identifier(id) => {
2017 resolve_identifier_alias_pairs(program, config_path, id.name.as_str(), visited, depth)
2018 }
2019 _ => Vec::new(),
2020 }
2021}
2022
2023fn resolve_object_alias_pairs_kinded(
2026 program: &Program,
2027 config_path: &Path,
2028 obj: &ObjectExpression,
2029 visited: &mut FxHashSet<PathBuf>,
2030 depth: usize,
2031) -> Vec<(String, String, bool)> {
2032 let mut pairs = Vec::new();
2033 for prop in &obj.properties {
2034 match prop {
2035 ObjectPropertyKind::ObjectProperty(prop) => {
2036 if let Some(find) = property_key_to_string(&prop.key)
2037 && let Some((replacement, is_bare)) = alias_replacement_kinded(&prop.value)
2038 {
2039 pairs.push((find, replacement, is_bare));
2040 }
2041 }
2042 ObjectPropertyKind::SpreadProperty(spread) => {
2044 pairs.extend(resolve_alias_pairs_kinded(
2045 program,
2046 config_path,
2047 &spread.argument,
2048 visited,
2049 depth,
2050 ));
2051 }
2052 }
2053 }
2054 pairs
2055}
2056
2057fn resolve_array_alias_pairs_kinded(
2060 program: &Program,
2061 config_path: &Path,
2062 arr: &ArrayExpression,
2063 visited: &mut FxHashSet<PathBuf>,
2064 depth: usize,
2065) -> Vec<(String, String, bool)> {
2066 let mut pairs = Vec::new();
2067 for element in &arr.elements {
2068 match element {
2069 ArrayExpressionElement::SpreadElement(spread) => {
2071 pairs.extend(resolve_alias_pairs_kinded(
2072 program,
2073 config_path,
2074 &spread.argument,
2075 visited,
2076 depth,
2077 ));
2078 }
2079 _ => {
2080 if let Some(Expression::ObjectExpression(obj)) = element.as_expression()
2081 && let Some(find) = find_property(obj, "find")
2082 .and_then(|prop| expression_to_string(&prop.value))
2083 && let Some((replacement, is_bare)) = find_property(obj, "replacement")
2084 .and_then(|prop| alias_replacement_kinded(&prop.value))
2085 {
2086 pairs.push((find, replacement, is_bare));
2087 }
2088 }
2089 }
2090 }
2091 pairs
2092}
2093
2094fn resolve_identifier_alias_pairs(
2097 program: &Program,
2098 config_path: &Path,
2099 name: &str,
2100 visited: &mut FxHashSet<PathBuf>,
2101 depth: usize,
2102) -> Vec<(String, String, bool)> {
2103 if depth >= MAX_ALIAS_RESOLVE_DEPTH {
2104 return Vec::new();
2105 }
2106 if let Some(init) = find_variable_init_expression(program, name) {
2108 return resolve_alias_pairs_kinded(program, config_path, init, visited, depth + 1);
2109 }
2110 let Some((specifier, imported_name)) = find_relative_import_binding(program, name) else {
2112 return Vec::new();
2113 };
2114 resolve_imported_alias_pairs(
2115 config_path,
2116 &specifier,
2117 imported_name.as_deref(),
2118 visited,
2119 depth + 1,
2120 )
2121}
2122
2123fn resolve_imported_alias_pairs(
2126 config_path: &Path,
2127 specifier: &str,
2128 imported_name: Option<&str>,
2129 visited: &mut FxHashSet<PathBuf>,
2130 depth: usize,
2131) -> Vec<(String, String, bool)> {
2132 let Some((sibling_path, sibling_source)) = resolve_sibling_module(config_path, specifier)
2133 else {
2134 return Vec::new();
2135 };
2136 if !visited.insert(sibling_path.clone()) {
2137 return Vec::new();
2138 }
2139 extract_from_source(&sibling_source, &sibling_path, |program| {
2140 let init = find_exported_init(program, imported_name)?;
2141 let pairs = resolve_alias_pairs_kinded(program, &sibling_path, init, visited, depth);
2142 (!pairs.is_empty()).then_some(pairs)
2143 })
2144 .unwrap_or_default()
2145}
2146
2147fn find_variable_init_expression<'a>(
2152 program: &'a Program<'a>,
2153 name: &str,
2154) -> Option<&'a Expression<'a>> {
2155 for decl in top_level_variable_declarations(program) {
2156 for declarator in &decl.declarations {
2157 if let BindingPattern::BindingIdentifier(id) = &declarator.id
2158 && id.name == name
2159 && let Some(init) = &declarator.init
2160 {
2161 return Some(init);
2162 }
2163 }
2164 }
2165 None
2166}
2167
2168pub(crate) fn find_module_export_expression<'a>(
2171 program: &'a Program<'a>,
2172) -> Option<&'a Expression<'a>> {
2173 program.body.iter().find_map(|stmt| match stmt {
2174 Statement::ExportDefaultDeclaration(decl) => decl.declaration.as_expression(),
2175 Statement::ExpressionStatement(expr_stmt) => match &expr_stmt.expression {
2176 Expression::AssignmentExpression(assign) if is_module_exports_target(&assign.left) => {
2177 Some(&assign.right)
2178 }
2179 _ => None,
2180 },
2181 _ => None,
2182 })
2183}
2184
2185pub(crate) fn find_exported_init<'a>(
2190 program: &'a Program<'a>,
2191 name: Option<&str>,
2192) -> Option<&'a Expression<'a>> {
2193 match name {
2194 Some(name) => find_variable_init_expression(program, name),
2195 None => program.body.iter().find_map(|stmt| {
2196 if let Statement::ExportDefaultDeclaration(decl) = stmt {
2197 decl.declaration.as_expression()
2198 } else {
2199 None
2200 }
2201 }),
2202 }
2203}
2204
2205pub(crate) fn find_relative_import_binding(
2210 program: &Program,
2211 name: &str,
2212) -> Option<(String, Option<String>)> {
2213 for stmt in &program.body {
2214 let Statement::ImportDeclaration(decl) = stmt else {
2215 continue;
2216 };
2217 let specifier = decl.source.value.as_str();
2218 if !is_relative_specifier(specifier) {
2219 continue;
2220 }
2221 let Some(specifiers) = &decl.specifiers else {
2222 continue;
2223 };
2224 for spec in specifiers {
2225 match spec {
2226 ImportDeclarationSpecifier::ImportSpecifier(spec) if spec.local.name == name => {
2227 return Some((
2228 specifier.to_string(),
2229 Some(spec.imported.name().to_string()),
2230 ));
2231 }
2232 ImportDeclarationSpecifier::ImportDefaultSpecifier(spec)
2233 if spec.local.name == name =>
2234 {
2235 return Some((specifier.to_string(), None));
2236 }
2237 _ => {}
2238 }
2239 }
2240 }
2241 None
2242}
2243
2244pub(crate) fn is_relative_specifier(specifier: &str) -> bool {
2247 specifier.starts_with("./") || specifier.starts_with("../") || specifier.starts_with('/')
2248}
2249
2250pub(crate) fn resolve_sibling_module(
2256 config_path: &Path,
2257 specifier: &str,
2258) -> Option<(PathBuf, String)> {
2259 let parent = config_path.parent().unwrap_or(config_path);
2260 let direct = parent.join(specifier);
2261 if let Ok(source) = std::fs::read_to_string(&direct) {
2262 return Some((direct, source));
2263 }
2264 for ext in ALIAS_SIBLING_EXTS {
2265 let candidate = parent.join(format!("{specifier}.{ext}"));
2266 if let Ok(source) = std::fs::read_to_string(&candidate) {
2267 return Some((candidate, source));
2268 }
2269 }
2270 for ext in ALIAS_SIBLING_EXTS {
2271 let candidate = direct.join(format!("index.{ext}"));
2272 if let Ok(source) = std::fs::read_to_string(&candidate) {
2273 return Some((candidate, source));
2274 }
2275 }
2276 None
2277}
2278
2279fn find_default_export_array<'a>(program: &'a Program<'a>) -> Option<&'a ArrayExpression<'a>> {
2284 for stmt in &program.body {
2285 if let Statement::ExportDefaultDeclaration(decl) = stmt
2286 && let Some(expr) = decl.declaration.as_expression()
2287 {
2288 return array_from_expression(expr);
2289 }
2290 }
2291 None
2292}
2293
2294fn array_from_expression<'a>(expr: &'a Expression<'a>) -> Option<&'a ArrayExpression<'a>> {
2295 match expr {
2296 Expression::ArrayExpression(arr) => Some(arr),
2297 Expression::ParenthesizedExpression(paren) => array_from_expression(&paren.expression),
2298 Expression::TSAsExpression(ts_as) => array_from_expression(&ts_as.expression),
2299 Expression::TSSatisfiesExpression(ts_sat) => array_from_expression(&ts_sat.expression),
2300 Expression::CallExpression(call) => call
2301 .arguments
2302 .first()
2303 .and_then(Argument::as_expression)
2304 .and_then(array_from_expression),
2305 _ => None,
2306 }
2307}
2308
2309pub(crate) fn lexical_normalize(path: &Path) -> PathBuf {
2310 let mut normalized = PathBuf::new();
2311
2312 for component in path.components() {
2313 match component {
2314 std::path::Component::CurDir => {}
2315 std::path::Component::ParentDir => {
2316 normalized.pop();
2317 }
2318 _ => normalized.push(component.as_os_str()),
2319 }
2320 }
2321
2322 normalized
2323}
2324
2325pub(crate) fn is_package_specifier(specifier: &str) -> bool {
2329 !specifier.is_empty()
2330 && specifier != "."
2331 && specifier != ".."
2332 && !specifier.starts_with("./")
2333 && !specifier.starts_with("../")
2334 && !specifier.starts_with('/')
2335 && !specifier.contains(':')
2336 && !specifier.contains('\\')
2337 && !specifier.chars().any(char::is_whitespace)
2338}
2339
2340fn expression_to_string_array(expr: &Expression) -> Vec<String> {
2342 match expr {
2343 Expression::ArrayExpression(arr) => arr
2344 .elements
2345 .iter()
2346 .filter_map(|el| match el {
2347 ArrayExpressionElement::SpreadElement(_) => None,
2348 _ => el.as_expression().and_then(expression_to_string),
2349 })
2350 .collect(),
2351 _ => vec![],
2352 }
2353}
2354
2355fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
2360 let mut values = Vec::new();
2361 match expr {
2362 Expression::StringLiteral(s) => {
2363 values.push(s.value.to_string());
2364 }
2365 Expression::ArrayExpression(arr) => {
2366 for el in &arr.elements {
2367 if let Some(inner) = el.as_expression() {
2368 match inner {
2369 Expression::StringLiteral(s) => {
2370 values.push(s.value.to_string());
2371 }
2372 Expression::ArrayExpression(sub_arr) => {
2373 if let Some(first) = sub_arr.elements.first()
2374 && let Some(first_expr) = first.as_expression()
2375 && let Some(s) = expression_to_string(first_expr)
2376 {
2377 values.push(s);
2378 }
2379 }
2380 _ => {}
2381 }
2382 }
2383 }
2384 }
2385 Expression::ObjectExpression(obj) => {
2386 for prop in &obj.properties {
2387 if let ObjectPropertyKind::ObjectProperty(p) = prop {
2388 match &p.value {
2389 Expression::StringLiteral(s) => {
2390 values.push(s.value.to_string());
2391 }
2392 Expression::ArrayExpression(sub_arr) => {
2393 if let Some(first) = sub_arr.elements.first()
2394 && let Some(first_expr) = first.as_expression()
2395 && let Some(s) = expression_to_string(first_expr)
2396 {
2397 values.push(s);
2398 }
2399 }
2400 _ => {}
2401 }
2402 }
2403 }
2404 }
2405 _ => {}
2406 }
2407 values
2408}
2409
2410fn collect_shallow_string_or_object_property_values(
2412 expr: &Expression,
2413 object_property: &str,
2414) -> Vec<String> {
2415 match expr {
2416 Expression::ArrayExpression(arr) => arr
2417 .elements
2418 .iter()
2419 .filter_map(|element| {
2420 element
2421 .as_expression()
2422 .and_then(|expr| shallow_string_or_object_property(expr, object_property))
2423 })
2424 .collect(),
2425 _ => shallow_string_or_object_property(expr, object_property)
2426 .into_iter()
2427 .collect(),
2428 }
2429}
2430
2431fn shallow_string_or_object_property(expr: &Expression, object_property: &str) -> Option<String> {
2432 match expr {
2433 Expression::ParenthesizedExpression(paren) => {
2434 shallow_string_or_object_property(&paren.expression, object_property)
2435 }
2436 Expression::TSSatisfiesExpression(ts_sat) => {
2437 shallow_string_or_object_property(&ts_sat.expression, object_property)
2438 }
2439 Expression::TSAsExpression(ts_as) => {
2440 shallow_string_or_object_property(&ts_as.expression, object_property)
2441 }
2442 Expression::ArrayExpression(sub_arr) => sub_arr
2443 .elements
2444 .first()
2445 .and_then(ArrayExpressionElement::as_expression)
2446 .and_then(expression_to_string),
2447 Expression::ObjectExpression(obj) => {
2448 find_property(obj, object_property).and_then(|prop| expression_to_string(&prop.value))
2449 }
2450 _ => expression_to_string(expr),
2451 }
2452}
2453
2454fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
2456 match expr {
2457 Expression::StringLiteral(s) => {
2458 values.push(s.value.to_string());
2459 }
2460 Expression::ArrayExpression(arr) => {
2461 for el in &arr.elements {
2462 if let Some(expr) = el.as_expression() {
2463 collect_all_string_values(expr, values);
2464 }
2465 }
2466 }
2467 Expression::ObjectExpression(obj) => {
2468 for prop in &obj.properties {
2469 if let ObjectPropertyKind::ObjectProperty(p) = prop {
2470 collect_all_string_values(&p.value, values);
2471 }
2472 }
2473 }
2474 _ => {}
2475 }
2476}
2477
2478fn property_key_to_string(key: &PropertyKey) -> Option<String> {
2480 match key {
2481 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
2482 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
2483 _ => None,
2484 }
2485}
2486
2487fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
2489 if path.is_empty() {
2490 return None;
2491 }
2492 let prop = find_property(obj, path[0])?;
2493 if path.len() == 1 {
2494 if let Expression::ObjectExpression(nested) = &prop.value {
2495 let keys = nested
2496 .properties
2497 .iter()
2498 .filter_map(|p| {
2499 if let ObjectPropertyKind::ObjectProperty(p) = p {
2500 property_key_to_string(&p.key)
2501 } else {
2502 None
2503 }
2504 })
2505 .collect();
2506 return Some(keys);
2507 }
2508 return None;
2509 }
2510 if let Expression::ObjectExpression(nested) = &prop.value {
2511 get_nested_object_keys(nested, &path[1..])
2512 } else {
2513 None
2514 }
2515}
2516
2517fn get_nested_expression<'a>(
2519 obj: &'a ObjectExpression<'a>,
2520 path: &[&str],
2521) -> Option<&'a Expression<'a>> {
2522 if path.is_empty() {
2523 return None;
2524 }
2525 let prop = find_property(obj, path[0])?;
2526 if path.len() == 1 {
2527 return Some(&prop.value);
2528 }
2529 if let Expression::ObjectExpression(nested) = &prop.value {
2530 get_nested_expression(nested, &path[1..])
2531 } else {
2532 None
2533 }
2534}
2535
2536fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
2538 if path.is_empty() {
2539 return None;
2540 }
2541 if path.len() == 1 {
2542 let prop = find_property(obj, path[0])?;
2543 return Some(expression_to_string_or_array(&prop.value));
2544 }
2545 let prop = find_property(obj, path[0])?;
2546 if let Expression::ObjectExpression(nested) = &prop.value {
2547 get_nested_string_or_array(nested, &path[1..])
2548 } else {
2549 None
2550 }
2551}
2552
2553pub(crate) fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
2561 match expr {
2562 Expression::StringLiteral(s) => vec![s.value.to_string()],
2563 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
2564 .quasis
2565 .first()
2566 .map(|q| vec![q.value.raw.to_string()])
2567 .unwrap_or_default(),
2568 Expression::ArrayExpression(arr) => arr
2569 .elements
2570 .iter()
2571 .filter_map(|el| el.as_expression())
2572 .flat_map(|e| match e {
2573 Expression::ObjectExpression(obj) => find_property(obj, "input")
2574 .map(|p| expression_to_string_or_array(&p.value))
2575 .unwrap_or_default(),
2576 _ => expression_to_path_string(e).into_iter().collect(),
2577 })
2578 .collect(),
2579 Expression::ObjectExpression(obj) => obj
2580 .properties
2581 .iter()
2582 .flat_map(|p| {
2583 if let ObjectPropertyKind::ObjectProperty(p) = p {
2584 match &p.value {
2585 Expression::ArrayExpression(_) => expression_to_string_or_array(&p.value),
2586 Expression::ObjectExpression(value_obj) => {
2587 find_property(value_obj, "import")
2588 .map(|import_prop| {
2589 expression_to_string_or_array(&import_prop.value)
2590 })
2591 .unwrap_or_default()
2592 }
2593 _ => expression_to_path_string(&p.value).into_iter().collect(),
2594 }
2595 } else {
2596 Vec::new()
2597 }
2598 })
2599 .collect(),
2600 _ => expression_to_path_string(expr).into_iter().collect(),
2601 }
2602}
2603
2604fn collect_require_sources(expr: &Expression) -> Vec<String> {
2606 let mut sources = Vec::new();
2607 match expr {
2608 Expression::CallExpression(call) if is_require_call(call) => {
2609 if let Some(s) = get_require_source(call) {
2610 sources.push(s);
2611 }
2612 }
2613 Expression::ArrayExpression(arr) => {
2614 for el in &arr.elements {
2615 if let Some(inner) = el.as_expression() {
2616 match inner {
2617 Expression::CallExpression(call) if is_require_call(call) => {
2618 if let Some(s) = get_require_source(call) {
2619 sources.push(s);
2620 }
2621 }
2622 Expression::ArrayExpression(sub_arr) => {
2623 if let Some(first) = sub_arr.elements.first()
2624 && let Some(Expression::CallExpression(call)) =
2625 first.as_expression()
2626 && is_require_call(call)
2627 && let Some(s) = get_require_source(call)
2628 {
2629 sources.push(s);
2630 }
2631 }
2632 _ => {}
2633 }
2634 }
2635 }
2636 }
2637 _ => {}
2638 }
2639 sources
2640}
2641
2642pub(crate) fn is_require_call(call: &CallExpression) -> bool {
2644 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
2645}
2646
2647pub(crate) fn get_require_source(call: &CallExpression) -> Option<String> {
2649 call.arguments.first().and_then(|arg| {
2650 if let Argument::StringLiteral(s) = arg {
2651 Some(s.value.to_string())
2652 } else {
2653 None
2654 }
2655 })
2656}
2657
2658#[cfg(test)]
2659mod tests {
2660 use super::*;
2661 use std::path::PathBuf;
2662
2663 fn js_path() -> PathBuf {
2664 PathBuf::from("config.js")
2665 }
2666
2667 fn ts_path() -> PathBuf {
2668 PathBuf::from("config.ts")
2669 }
2670
2671 #[test]
2672 fn extract_config_array_tuple_heads_reads_first_string_of_each_tuple() {
2673 let source = r#"
2674 export default defineNuxtConfig({
2675 modules: ["@nuxt/image", ["@nuxt/content", { watch: false }], [someModule, {}], []]
2676 });
2677 "#;
2678 assert_eq!(
2679 extract_config_array_tuple_heads(source, &ts_path(), &["modules"]),
2680 vec!["@nuxt/content".to_string()]
2681 );
2682 }
2683
2684 #[test]
2685 fn extract_lazy_imports_bare_arrows() {
2686 let source = r"
2687 import { defineConfig } from '@adonisjs/core/app'
2688 export default defineConfig({
2689 preloads: [
2690 () => import('#start/routes'),
2691 () => import('#start/kernel'),
2692 ],
2693 })
2694 ";
2695 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["preloads"]);
2696 assert_eq!(specs, vec!["#start/routes", "#start/kernel"]);
2697 }
2698
2699 #[test]
2700 fn extract_lazy_imports_object_form_with_file_key() {
2701 let source = r"
2702 export default defineConfig({
2703 providers: [
2704 () => import('@adonisjs/core/providers/app_provider'),
2705 {
2706 file: () => import('@adonisjs/core/providers/repl_provider'),
2707 environment: ['repl', 'test'],
2708 },
2709 ],
2710 })
2711 ";
2712 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
2713 assert_eq!(
2714 specs,
2715 vec![
2716 "@adonisjs/core/providers/app_provider",
2717 "@adonisjs/core/providers/repl_provider",
2718 ]
2719 );
2720 }
2721
2722 #[test]
2723 fn extract_lazy_imports_block_body_with_return() {
2724 let source = r"
2725 export default defineConfig({
2726 commands: [
2727 () => { return import('@adonisjs/core/commands') },
2728 ],
2729 })
2730 ";
2731 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
2732 assert_eq!(specs, vec!["@adonisjs/core/commands"]);
2733 }
2734
2735 #[test]
2736 fn extract_lazy_imports_skips_unknown_element_shapes() {
2737 let source = r"
2738 export default defineConfig({
2739 commands: [
2740 'string-entry',
2741 42,
2742 { other: 'value' },
2743 () => import('@adonisjs/lucid/commands'),
2744 ],
2745 })
2746 ";
2747 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["commands"]);
2748 assert_eq!(specs, vec!["@adonisjs/lucid/commands"]);
2749 }
2750
2751 #[test]
2752 fn extract_lazy_imports_missing_property_returns_empty() {
2753 let source = r"
2754 export default defineConfig({
2755 preloads: [() => import('#start/routes')],
2756 })
2757 ";
2758 let specs = extract_lazy_imports_in_array(source, &ts_path(), &["providers"]);
2759 assert!(specs.is_empty());
2760 }
2761
2762 #[test]
2763 fn extract_imports_basic() {
2764 let source = r"
2765 import foo from 'foo-pkg';
2766 import { bar } from '@scope/bar';
2767 export default {};
2768 ";
2769 let imports = extract_imports(source, &js_path());
2770 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
2771 }
2772
2773 #[test]
2774 fn extract_default_export_object_property() {
2775 let source = r#"export default { testDir: "./tests" };"#;
2776 let val = extract_config_string(source, &js_path(), &["testDir"]);
2777 assert_eq!(val, Some("./tests".to_string()));
2778 }
2779
2780 #[test]
2781 fn extract_define_config_property() {
2782 let source = r#"
2783 import { defineConfig } from 'vitest/config';
2784 export default defineConfig({
2785 test: {
2786 include: ["**/*.test.ts", "**/*.spec.ts"],
2787 setupFiles: ["./test/setup.ts"]
2788 }
2789 });
2790 "#;
2791 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
2792 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
2793
2794 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
2795 assert_eq!(setup, vec!["./test/setup.ts"]);
2796 }
2797
2798 #[test]
2799 fn extract_module_exports_property() {
2800 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
2801 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
2802 assert_eq!(val, Some("jsdom".to_string()));
2803 }
2804
2805 #[test]
2806 fn extract_nested_string_array() {
2807 let source = r#"
2808 export default {
2809 resolve: {
2810 alias: {
2811 "@": "./src"
2812 }
2813 },
2814 test: {
2815 include: ["src/**/*.test.ts"]
2816 }
2817 };
2818 "#;
2819 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
2820 assert_eq!(include, vec!["src/**/*.test.ts"]);
2821 }
2822
2823 #[test]
2824 fn extract_addons_array() {
2825 let source = r#"
2826 export default {
2827 addons: [
2828 "@storybook/addon-a11y",
2829 "@storybook/addon-docs",
2830 "@storybook/addon-links"
2831 ]
2832 };
2833 "#;
2834 let addons = extract_config_property_strings(source, &ts_path(), "addons");
2835 assert_eq!(
2836 addons,
2837 vec![
2838 "@storybook/addon-a11y",
2839 "@storybook/addon-docs",
2840 "@storybook/addon-links"
2841 ]
2842 );
2843 }
2844
2845 #[test]
2846 fn handle_empty_config() {
2847 let source = "";
2848 let result = extract_config_string(source, &js_path(), &["key"]);
2849 assert_eq!(result, None);
2850 }
2851
2852 #[test]
2853 fn object_keys_postcss_plugins() {
2854 let source = r"
2855 module.exports = {
2856 plugins: {
2857 autoprefixer: {},
2858 tailwindcss: {},
2859 'postcss-import': {}
2860 }
2861 };
2862 ";
2863 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2864 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
2865 }
2866
2867 #[test]
2868 fn object_keys_nested_path() {
2869 let source = r"
2870 export default {
2871 build: {
2872 plugins: {
2873 minify: {},
2874 compress: {}
2875 }
2876 }
2877 };
2878 ";
2879 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
2880 assert_eq!(keys, vec!["minify", "compress"]);
2881 }
2882
2883 #[test]
2884 fn object_keys_empty_object() {
2885 let source = r"export default { plugins: {} };";
2886 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2887 assert!(keys.is_empty());
2888 }
2889
2890 #[test]
2891 fn object_keys_non_object_returns_empty() {
2892 let source = r#"export default { plugins: ["a", "b"] };"#;
2893 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
2894 assert!(keys.is_empty());
2895 }
2896
2897 #[test]
2898 fn string_or_array_single_string() {
2899 let source = r#"export default { entry: "./src/index.js" };"#;
2900 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2901 assert_eq!(result, vec!["./src/index.js"]);
2902 }
2903
2904 #[test]
2905 fn string_or_array_array() {
2906 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
2907 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2908 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
2909 }
2910
2911 #[test]
2912 fn string_or_array_object_values() {
2913 let source =
2914 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
2915 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2916 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
2917 }
2918
2919 #[test]
2920 fn string_or_array_object_array_values() {
2921 let source = r#"export default { entry: { app: ["./src/polyfill.js", "./src/app.js"] } };"#;
2922 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2923 assert_eq!(result, vec!["./src/polyfill.js", "./src/app.js"]);
2924 }
2925
2926 #[test]
2927 fn string_or_array_webpack_entry_descriptors() {
2928 let source = r#"
2929 export default {
2930 entry: {
2931 app: {
2932 import: "./src/app.js",
2933 filename: "pages/app.js",
2934 dependOn: "shared",
2935 },
2936 admin: {
2937 import: ["./src/admin-polyfill.js", "./src/admin.js"],
2938 runtime: "runtime",
2939 },
2940 shared: ["react", "react-dom"],
2941 },
2942 };
2943 "#;
2944 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2945 assert_eq!(
2946 result,
2947 vec![
2948 "./src/app.js",
2949 "./src/admin-polyfill.js",
2950 "./src/admin.js",
2951 "react",
2952 "react-dom"
2953 ]
2954 );
2955 }
2956
2957 #[test]
2958 fn string_or_array_nested_path() {
2959 let source = r#"
2960 export default {
2961 build: {
2962 rollupOptions: {
2963 input: ["./index.html", "./about.html"]
2964 }
2965 }
2966 };
2967 "#;
2968 let result = extract_config_string_or_array(
2969 source,
2970 &js_path(),
2971 &["build", "rollupOptions", "input"],
2972 );
2973 assert_eq!(result, vec!["./index.html", "./about.html"]);
2974 }
2975
2976 #[test]
2977 fn string_or_array_template_literal() {
2978 let source = r"export default { entry: `./src/index.js` };";
2979 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
2980 assert_eq!(result, vec!["./src/index.js"]);
2981 }
2982
2983 #[test]
2984 fn string_or_array_object_path_helper_values() {
2985 let source = r#"
2986 import { resolve, join } from "node:path";
2987 import path from "node:path";
2988 export default {
2989 build: {
2990 rollupOptions: {
2991 input: {
2992 app: resolve(__dirname, "src/app.ts"),
2993 modal: path.resolve(__dirname, "src/modal.ts"),
2994 tabs: join(__dirname, "src/tabs.ts"),
2995 styles: resolve(__dirname, "src/index.css"),
2996 },
2997 },
2998 },
2999 };
3000 "#;
3001 let result = extract_config_string_or_array(
3002 source,
3003 &js_path(),
3004 &["build", "rollupOptions", "input"],
3005 );
3006 assert_eq!(
3007 result,
3008 vec!["src/app.ts", "src/modal.ts", "src/tabs.ts", "src/index.css"]
3009 );
3010 }
3011
3012 #[test]
3013 fn string_or_array_array_path_helper_values() {
3014 let source = r#"
3015 import { resolve } from "node:path";
3016 export default {
3017 build: {
3018 rollupOptions: {
3019 input: [resolve(__dirname, "src/a.ts"), "./src/b.ts"],
3020 },
3021 },
3022 };
3023 "#;
3024 let result = extract_config_string_or_array(
3025 source,
3026 &js_path(),
3027 &["build", "rollupOptions", "input"],
3028 );
3029 assert_eq!(result, vec!["src/a.ts", "./src/b.ts"]);
3030 }
3031
3032 #[test]
3033 fn string_or_array_top_level_path_helper_call() {
3034 let source = r#"
3035 import { resolve } from "node:path";
3036 export default { build: { lib: { entry: resolve(__dirname, "src/index.ts") } } };
3037 "#;
3038 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
3039 assert_eq!(result, vec!["src/index.ts"]);
3040 }
3041
3042 #[test]
3043 fn string_or_array_import_meta_dirname_anchor() {
3044 let source = r#"
3045 import { resolve } from "node:path";
3046 export default {
3047 build: { lib: { entry: resolve(import.meta.dirname, "src/index.ts") } },
3048 };
3049 "#;
3050 let result = extract_config_string_or_array(source, &ts_path(), &["build", "lib", "entry"]);
3051 assert_eq!(result, vec!["src/index.ts"]);
3052 }
3053
3054 #[test]
3055 fn string_or_array_non_literal_path_helper_args_dropped() {
3056 let source = r#"
3057 import { resolve } from "node:path";
3058 export default { build: { lib: { entry: resolve(baseDir, "src/index.ts") } } };
3059 "#;
3060 let result = extract_config_string_or_array(source, &js_path(), &["build", "lib", "entry"]);
3061 assert!(
3062 result.is_empty(),
3063 "non-literal path-helper args must be dropped: {result:?}"
3064 );
3065 }
3066
3067 #[test]
3068 fn require_strings_array() {
3069 let source = r"
3070 module.exports = {
3071 plugins: [
3072 require('autoprefixer'),
3073 require('postcss-import')
3074 ]
3075 };
3076 ";
3077 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3078 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
3079 }
3080
3081 #[test]
3082 fn require_strings_with_tuples() {
3083 let source = r"
3084 module.exports = {
3085 plugins: [
3086 require('autoprefixer'),
3087 [require('postcss-preset-env'), { stage: 3 }]
3088 ]
3089 };
3090 ";
3091 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3092 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
3093 }
3094
3095 #[test]
3096 fn require_strings_empty_array() {
3097 let source = r"module.exports = { plugins: [] };";
3098 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3099 assert!(deps.is_empty());
3100 }
3101
3102 #[test]
3103 fn require_strings_no_require_calls() {
3104 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
3105 let deps = extract_config_require_strings(source, &js_path(), "plugins");
3106 assert!(deps.is_empty());
3107 }
3108
3109 #[test]
3110 fn extract_aliases_from_object_with_file_url_to_path() {
3111 let source = r#"
3112 import { defineConfig } from 'vite';
3113 import { fileURLToPath, URL } from 'node:url';
3114
3115 export default defineConfig({
3116 resolve: {
3117 alias: {
3118 "@": fileURLToPath(new URL("./src", import.meta.url))
3119 }
3120 }
3121 });
3122 "#;
3123
3124 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
3125 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
3126 }
3127
3128 #[test]
3129 fn extract_aliases_from_array_form() {
3130 let source = r#"
3131 export default {
3132 resolve: {
3133 alias: [
3134 { find: "@", replacement: "./src" },
3135 { find: "$utils", replacement: "src/lib/utils" }
3136 ]
3137 }
3138 };
3139 "#;
3140
3141 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
3142 assert_eq!(
3143 aliases,
3144 vec![
3145 ("@".to_string(), "./src".to_string()),
3146 ("$utils".to_string(), "src/lib/utils".to_string())
3147 ]
3148 );
3149 }
3150
3151 #[test]
3152 fn extract_aliases_from_object_with_array_values() {
3153 let source = r#"
3154 ({
3155 compilerOptions: {
3156 paths: {
3157 "@/*": ["./src/*"],
3158 "@shared/*": ["./shared/*", "./fallback/*"]
3159 }
3160 }
3161 })
3162 "#;
3163
3164 let aliases = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
3165 assert_eq!(
3166 aliases,
3167 vec![
3168 ("@/*".to_string(), "./src/*".to_string()),
3169 ("@shared/*".to_string(), "./shared/*".to_string())
3170 ]
3171 );
3172 }
3173
3174 #[test]
3175 fn extract_array_object_strings_mixed_forms() {
3176 let source = r#"
3177 export default {
3178 components: [
3179 "~/components",
3180 { path: "@/feature-components" }
3181 ]
3182 };
3183 "#;
3184
3185 let values =
3186 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
3187 assert_eq!(
3188 values,
3189 vec![
3190 "~/components".to_string(),
3191 "@/feature-components".to_string()
3192 ]
3193 );
3194 }
3195
3196 #[test]
3197 fn extract_config_plugin_option_string_from_json() {
3198 let source = r#"{
3199 "expo": {
3200 "plugins": [
3201 ["expo-router", { "root": "src/app" }]
3202 ]
3203 }
3204 }"#;
3205
3206 let value = extract_config_plugin_option_string(
3207 source,
3208 &json_path(),
3209 &["expo", "plugins"],
3210 "expo-router",
3211 "root",
3212 );
3213
3214 assert_eq!(value, Some("src/app".to_string()));
3215 }
3216
3217 #[test]
3218 fn extract_config_plugin_option_string_from_top_level_plugins() {
3219 let source = r#"{
3220 "plugins": [
3221 ["expo-router", { "root": "./src/routes" }]
3222 ]
3223 }"#;
3224
3225 let value = extract_config_plugin_option_string_from_paths(
3226 source,
3227 &json_path(),
3228 &[&["plugins"], &["expo", "plugins"]],
3229 "expo-router",
3230 "root",
3231 );
3232
3233 assert_eq!(value, Some("./src/routes".to_string()));
3234 }
3235
3236 #[test]
3237 fn extract_config_plugin_option_string_from_ts_config() {
3238 let source = r"
3239 export default {
3240 expo: {
3241 plugins: [
3242 ['expo-router', { root: './src/app' }]
3243 ]
3244 }
3245 };
3246 ";
3247
3248 let value = extract_config_plugin_option_string(
3249 source,
3250 &ts_path(),
3251 &["expo", "plugins"],
3252 "expo-router",
3253 "root",
3254 );
3255
3256 assert_eq!(value, Some("./src/app".to_string()));
3257 }
3258
3259 #[test]
3260 fn extract_config_plugin_option_string_returns_none_when_plugin_missing() {
3261 let source = r#"{
3262 "expo": {
3263 "plugins": [
3264 ["expo-font", {}]
3265 ]
3266 }
3267 }"#;
3268
3269 let value = extract_config_plugin_option_string(
3270 source,
3271 &json_path(),
3272 &["expo", "plugins"],
3273 "expo-router",
3274 "root",
3275 );
3276
3277 assert_eq!(value, None);
3278 }
3279
3280 #[test]
3281 fn vite_react_babel_dependencies_extract_plain_tuple_and_prefixed_entries() {
3282 let source = r#"
3283 import react from "@vitejs/plugin-react";
3284
3285 export default defineConfig({
3286 plugins: [
3287 react({
3288 babel: {
3289 plugins: [
3290 "babel-plugin-plain",
3291 ["module:@preact/signals-react-transform", { mode: "auto" }],
3292 ],
3293 presets: [["@babel/preset-react", { runtime: "automatic" }]],
3294 },
3295 }),
3296 ],
3297 });
3298 "#;
3299
3300 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3301
3302 assert_eq!(
3303 deps,
3304 vec![
3305 "babel-plugin-plain".to_string(),
3306 "@preact/signals-react-transform".to_string(),
3307 "@babel/preset-react".to_string(),
3308 ]
3309 );
3310 }
3311
3312 #[test]
3313 fn vite_react_babel_dependencies_support_default_alias_import() {
3314 let source = r#"
3315 import { default as viteReact } from "@vitejs/plugin-react";
3316
3317 export default {
3318 plugins: [
3319 viteReact({
3320 babel: {
3321 plugins: [["module:@scope/pkg/plugin", {}]],
3322 },
3323 }),
3324 ],
3325 };
3326 "#;
3327
3328 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3329
3330 assert_eq!(deps, vec!["@scope/pkg".to_string()]);
3331 }
3332
3333 #[test]
3334 fn vite_react_babel_dependencies_ignore_unrelated_plugin_calls() {
3335 let source = r#"
3336 import vue from "@vitejs/plugin-vue";
3337
3338 export default {
3339 plugins: [
3340 vue({
3341 babel: {
3342 plugins: ["@preact/signals-react-transform"],
3343 },
3344 }),
3345 ],
3346 };
3347 "#;
3348
3349 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3350
3351 assert!(deps.is_empty());
3352 }
3353
3354 #[test]
3355 fn vite_react_babel_dependencies_skip_relative_and_protocol_entries() {
3356 let source = r#"
3357 import react from "@vitejs/plugin-react";
3358
3359 export default {
3360 plugins: [
3361 react({
3362 babel: {
3363 plugins: ["./local-plugin", "module:./local-prefixed", "http://example.com/plugin"],
3364 },
3365 }),
3366 ],
3367 };
3368 "#;
3369
3370 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
3371
3372 assert!(deps.is_empty());
3373 }
3374
3375 #[test]
3376 fn normalize_config_path_relative_to_root() {
3377 let config_path = PathBuf::from("/project/vite.config.ts");
3378 let root = PathBuf::from("/project");
3379
3380 assert_eq!(
3381 normalize_config_path("./src/lib", &config_path, &root),
3382 Some("src/lib".to_string())
3383 );
3384 assert_eq!(
3385 normalize_config_path("/src/lib", &config_path, &root),
3386 Some("src/lib".to_string())
3387 );
3388 }
3389
3390 #[test]
3391 fn normalize_config_path_mixed_separators_and_parent_dirs() {
3392 let config_path = PathBuf::from("/project/config/vite.config.ts");
3393 let root = PathBuf::from("/project");
3394
3395 assert_eq!(
3396 normalize_config_path(".\\src\\..\\app\\lib", &config_path, &root),
3397 Some("config/app/lib".to_string())
3398 );
3399 }
3400
3401 #[test]
3404 fn normalize_config_path_reads_an_absolute_path_under_root_as_absolute() {
3405 let root = std::env::temp_dir().join("fallow-2806-project");
3406 let config_path = root.join("vite.config.ts");
3407 let absolute = path_to_config_string(&root.join("src").join("lib"));
3408
3409 assert_eq!(
3410 normalize_config_path(&absolute, &config_path, &root),
3411 Some("src/lib".to_string())
3412 );
3413 assert_eq!(
3414 normalize_filesystem_config_path(&absolute, &config_path, &root),
3415 Some("src/lib".to_string())
3416 );
3417 }
3418
3419 #[cfg(unix)]
3424 #[test]
3425 fn normalize_config_path_keeps_the_vite_root_in_root_reading() {
3426 let temp = tempfile::tempdir().expect("temp dir");
3427 let root = temp.path().join("src");
3428 std::fs::create_dir_all(&root).expect("root dir");
3429 let config_path = root.join("vite.config.ts");
3430 let root_string = path_to_config_string(&root);
3431 let nested = root_string.trim_start_matches('/').to_string();
3432 let under = format!("{root_string}/lib");
3433
3434 assert_eq!(
3435 normalize_config_path(&root_string, &config_path, &root),
3436 Some(nested.clone())
3437 );
3438 assert_eq!(
3439 normalize_config_path(&under, &config_path, &root),
3440 Some("lib".to_string())
3441 );
3442
3443 std::fs::create_dir_all(root.join(&nested)).expect("root-in-root dir");
3444 assert_eq!(
3445 normalize_config_path(&under, &config_path, &root),
3446 Some(format!("{nested}/lib"))
3447 );
3448 assert_eq!(
3449 normalize_filesystem_config_path(&under, &config_path, &root),
3450 Some("lib".to_string())
3451 );
3452 }
3453
3454 #[test]
3457 fn normalize_filesystem_config_path_does_not_read_a_leading_slash_as_root_relative() {
3458 let config_path = PathBuf::from("/project/config/webpack.config.js");
3459 let root = PathBuf::from("/project");
3460
3461 assert_eq!(
3462 normalize_filesystem_config_path("/src/lib", &config_path, &root),
3463 None
3464 );
3465 assert_eq!(
3466 normalize_filesystem_config_path("./src/lib", &config_path, &root),
3467 Some("config/src/lib".to_string())
3468 );
3469 assert_eq!(
3470 normalize_config_path("/src/lib", &config_path, &root),
3471 Some("src/lib".to_string())
3472 );
3473 }
3474
3475 #[test]
3476 fn normalize_config_path_leading_slash_stays_project_relative() {
3477 let config_path = PathBuf::from("/project/vite.config.ts");
3478 let root = PathBuf::from("/project");
3479
3480 assert_eq!(
3481 normalize_config_path("/src\\lib", &config_path, &root),
3482 Some("src/lib".to_string())
3483 );
3484 }
3485
3486 #[test]
3487 fn json_wrapped_in_parens_string() {
3488 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
3489 let val = extract_config_string(source, &js_path(), &["extends"]);
3490 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
3491 }
3492
3493 #[test]
3494 fn json_wrapped_in_parens_nested_array() {
3495 let source =
3496 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
3497 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
3498 assert_eq!(types, vec!["node", "jest"]);
3499
3500 let include = extract_config_string_array(source, &js_path(), &["include"]);
3501 assert_eq!(include, vec!["src/**/*"]);
3502 }
3503
3504 #[test]
3505 fn json_wrapped_in_parens_object_keys() {
3506 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
3507 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
3508 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
3509 }
3510
3511 fn json_path() -> PathBuf {
3512 PathBuf::from("config.json")
3513 }
3514
3515 #[test]
3516 fn json_file_parsed_correctly() {
3517 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
3518 let val = extract_config_string(source, &json_path(), &["key"]);
3519 assert_eq!(val, Some("value".to_string()));
3520
3521 let list = extract_config_string_array(source, &json_path(), &["list"]);
3522 assert_eq!(list, vec!["a", "b"]);
3523 }
3524
3525 #[test]
3526 fn jsonc_file_parsed_correctly() {
3527 let source = r#"{"key": "value"}"#;
3528 let path = PathBuf::from("tsconfig.jsonc");
3529 let val = extract_config_string(source, &path, &["key"]);
3530 assert_eq!(val, Some("value".to_string()));
3531 }
3532
3533 #[test]
3534 fn extract_define_config_arrow_function() {
3535 let source = r#"
3536 import { defineConfig } from 'vite';
3537 export default defineConfig(() => ({
3538 test: {
3539 include: ["**/*.test.ts"]
3540 }
3541 }));
3542 "#;
3543 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
3544 assert_eq!(include, vec!["**/*.test.ts"]);
3545 }
3546
3547 #[test]
3551 fn extract_define_config_block_body_arrow_function() {
3552 let source = r#"
3553 import { defineConfig } from 'vite';
3554 export default defineConfig(({ mode }) => {
3555 const isProduction = mode === 'production';
3556 return {
3557 test: {
3558 environment: "jsdom",
3559 setupFiles: "./tests/setup.ts"
3560 },
3561 base: isProduction ? '/app/' : '/'
3562 };
3563 });
3564 "#;
3565 assert_eq!(
3566 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
3567 Some("jsdom")
3568 );
3569 assert_eq!(
3570 extract_config_string(source, &ts_path(), &["test", "setupFiles"]).as_deref(),
3571 Some("./tests/setup.ts")
3572 );
3573 }
3574
3575 #[test]
3576 fn extract_define_config_function_expression() {
3577 let source = r#"
3578 import { defineConfig } from 'vite';
3579 export default defineConfig(function () {
3580 return { test: { environment: "happy-dom" } };
3581 });
3582 "#;
3583 assert_eq!(
3584 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
3585 Some("happy-dom")
3586 );
3587 }
3588
3589 #[test]
3593 fn wrapper_options_object_does_not_shadow_named_config_arg() {
3594 let source = r#"
3595 const nextConfig = { pageExtensions: ["page.tsx"] };
3596 module.exports = withSentryConfig(nextConfig, { org: "o", project: "p", silent: true });
3597 "#;
3598 assert_eq!(
3599 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
3600 vec!["page.tsx"]
3601 );
3602 }
3603
3604 #[test]
3605 fn wrapper_options_object_does_not_shadow_named_config_arg_esm() {
3606 let source = r#"
3607 const nextConfig = { pageExtensions: ["page.tsx"] };
3608 export default withSentryConfig(nextConfig, { org: "o", project: "p" });
3609 "#;
3610 assert_eq!(
3611 extract_config_string_array(source, &ts_path(), &["pageExtensions"]),
3612 vec!["page.tsx"]
3613 );
3614 }
3615
3616 #[test]
3618 fn merge_config_extracts_nested_define_config_object() {
3619 let source = r#"
3620 import { defineConfig, mergeConfig } from 'vitest/config';
3621 import viteConfig from './vite.config';
3622 export default mergeConfig(viteConfig, defineConfig({
3623 test: { environment: "jsdom" }
3624 }));
3625 "#;
3626 assert_eq!(
3627 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
3628 Some("jsdom")
3629 );
3630 }
3631
3632 #[test]
3633 fn define_config_wrapping_merge_config_extracts_object() {
3634 let source = r#"
3635 import { defineConfig, mergeConfig } from 'vitest/config';
3636 import base from './base';
3637 export default defineConfig(mergeConfig(base, { test: { environment: "jsdom" } }));
3638 "#;
3639 assert_eq!(
3640 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
3641 Some("jsdom")
3642 );
3643 }
3644
3645 #[test]
3648 fn inline_object_at_argument_zero_still_wins() {
3649 let source = r#"
3650 const unrelated = { pageExtensions: ["wrong.tsx"] };
3651 module.exports = withSentryConfig({ pageExtensions: ["right.tsx"] }, { org: "o" });
3652 "#;
3653 assert_eq!(
3654 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
3655 vec!["right.tsx"]
3656 );
3657 }
3658
3659 #[test]
3663 fn conditional_config_callback_extracts_branch_return() {
3664 let source = r#"
3665 import { defineConfig } from 'vite';
3666 export default defineConfig(({ command }) => {
3667 if (command === "serve") {
3668 return { test: { environment: "jsdom" } };
3669 } else {
3670 return { test: { environment: "node" } };
3671 }
3672 });
3673 "#;
3674 assert_eq!(
3675 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
3676 Some("jsdom"),
3677 "with no top-level return, the first branch in source order wins"
3678 );
3679 }
3680
3681 #[test]
3682 fn try_block_return_is_extracted() {
3683 let source = r#"
3684 export default (() => {
3685 try {
3686 return { test: { environment: "jsdom" } };
3687 } catch (e) {
3688 return { test: { environment: "node" } };
3689 }
3690 });
3691 "#;
3692 assert_eq!(
3693 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
3694 Some("jsdom")
3695 );
3696 }
3697
3698 #[test]
3699 fn switch_case_return_is_extracted() {
3700 let source = r#"
3701 import { defineConfig } from 'vite';
3702 export default defineConfig(({ mode }) => {
3703 switch (mode) {
3704 case "test":
3705 return { test: { environment: "jsdom" } };
3706 default:
3707 return { test: { environment: "node" } };
3708 }
3709 });
3710 "#;
3711 assert_eq!(
3712 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
3713 Some("jsdom")
3714 );
3715 }
3716
3717 #[test]
3721 fn guard_clause_does_not_shadow_the_top_level_return() {
3722 let source = r#"
3723 import { defineConfig } from 'vite';
3724 export default defineConfig(({ mode }) => {
3725 if (!mode) {
3726 return {};
3727 }
3728 return { test: { environment: "jsdom" } };
3729 });
3730 "#;
3731 assert_eq!(
3732 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
3733 Some("jsdom"),
3734 "a return at the callback's own level is the main config"
3735 );
3736 }
3737
3738 #[test]
3741 fn else_if_chain_does_not_shadow_the_top_level_return() {
3742 let source = r#"
3743 import { defineConfig } from 'vite';
3744 export default defineConfig(({ mode }) => {
3745 if (mode === "a") {
3746 return { base: "/a/" };
3747 } else if (mode === "b") {
3748 return { base: "/b/" };
3749 }
3750 return { test: { environment: "jsdom" } };
3751 });
3752 "#;
3753 assert_eq!(
3754 extract_config_string(source, &ts_path(), &["test", "environment"]).as_deref(),
3755 Some("jsdom")
3756 );
3757 }
3758
3759 #[test]
3760 fn extract_config_from_default_export_function_declaration() {
3761 let source = r#"
3762 export default function createConfig() {
3763 return {
3764 clientModules: ["./src/client/global.js"]
3765 };
3766 }
3767 "#;
3768
3769 let client_modules = extract_config_string_array(source, &ts_path(), &["clientModules"]);
3770 assert_eq!(client_modules, vec!["./src/client/global.js"]);
3771 }
3772
3773 #[test]
3774 fn extract_config_from_default_export_async_function_declaration() {
3775 let source = r#"
3776 export default async function createConfigAsync() {
3777 return {
3778 docs: {
3779 path: "knowledge"
3780 }
3781 };
3782 }
3783 "#;
3784
3785 let docs_path = extract_config_string(source, &ts_path(), &["docs", "path"]);
3786 assert_eq!(docs_path, Some("knowledge".to_string()));
3787 }
3788
3789 #[test]
3790 fn extract_config_from_exported_arrow_function_identifier() {
3791 let source = r#"
3792 const config = async () => {
3793 return {
3794 themes: ["classic"]
3795 };
3796 };
3797
3798 export default config;
3799 "#;
3800
3801 let themes = extract_config_shallow_strings(source, &ts_path(), "themes");
3802 assert_eq!(themes, vec!["classic"]);
3803 }
3804
3805 #[test]
3806 fn module_exports_nested_string() {
3807 let source = r#"
3808 module.exports = {
3809 resolve: {
3810 alias: {
3811 "@": "./src"
3812 }
3813 }
3814 };
3815 "#;
3816 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
3817 assert_eq!(val, Some("./src".to_string()));
3818 }
3819
3820 #[test]
3821 fn property_strings_nested_objects() {
3822 let source = r#"
3823 export default {
3824 plugins: {
3825 group1: { a: "val-a" },
3826 group2: { b: "val-b" }
3827 }
3828 };
3829 "#;
3830 let values = extract_config_property_strings(source, &js_path(), "plugins");
3831 assert!(values.contains(&"val-a".to_string()));
3832 assert!(values.contains(&"val-b".to_string()));
3833 }
3834
3835 #[test]
3836 fn property_strings_missing_key_returns_empty() {
3837 let source = r#"export default { other: "value" };"#;
3838 let values = extract_config_property_strings(source, &js_path(), "missing");
3839 assert!(values.is_empty());
3840 }
3841
3842 #[test]
3843 fn shallow_strings_tuple_array() {
3844 let source = r#"
3845 module.exports = {
3846 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
3847 };
3848 "#;
3849 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
3850 assert_eq!(values, vec!["default", "jest-junit"]);
3851 assert!(!values.contains(&"reports".to_string()));
3852 }
3853
3854 #[test]
3855 fn shallow_strings_single_string() {
3856 let source = r#"export default { preset: "ts-jest" };"#;
3857 let values = extract_config_shallow_strings(source, &js_path(), "preset");
3858 assert_eq!(values, vec!["ts-jest"]);
3859 }
3860
3861 #[test]
3862 fn shallow_strings_missing_key() {
3863 let source = r#"export default { other: "val" };"#;
3864 let values = extract_config_shallow_strings(source, &js_path(), "missing");
3865 assert!(values.is_empty());
3866 }
3867
3868 #[test]
3869 fn shallow_strings_or_object_property_alias_objects() {
3870 let source = r#"
3871 export default {
3872 jsPlugins: [
3873 "eslint-plugin-playwright",
3874 ["eslint-plugin-regexp", { rules: {} }],
3875 { name: "short", specifier: "eslint-plugin-with-long-name" }
3876 ]
3877 };
3878 "#;
3879 let values = extract_config_shallow_strings_or_object_property(
3880 source,
3881 &ts_path(),
3882 "jsPlugins",
3883 "specifier",
3884 );
3885 assert_eq!(
3886 values,
3887 vec![
3888 "eslint-plugin-playwright",
3889 "eslint-plugin-regexp",
3890 "eslint-plugin-with-long-name"
3891 ]
3892 );
3893 }
3894
3895 #[test]
3896 fn nested_shallow_strings_vitest_reporters() {
3897 let source = r#"
3898 export default {
3899 test: {
3900 reporters: ["default", "vitest-sonar-reporter"]
3901 }
3902 };
3903 "#;
3904 let values =
3905 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3906 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
3907 }
3908
3909 #[test]
3910 fn nested_shallow_strings_tuple_format() {
3911 let source = r#"
3912 export default {
3913 test: {
3914 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
3915 }
3916 };
3917 "#;
3918 let values =
3919 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3920 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
3921 }
3922
3923 #[test]
3924 fn nested_shallow_strings_missing_outer() {
3925 let source = r"export default { other: {} };";
3926 let values =
3927 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3928 assert!(values.is_empty());
3929 }
3930
3931 #[test]
3932 fn nested_shallow_strings_missing_inner() {
3933 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
3934 let values =
3935 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
3936 assert!(values.is_empty());
3937 }
3938
3939 #[test]
3940 fn string_or_array_missing_path() {
3941 let source = r"export default {};";
3942 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3943 assert!(result.is_empty());
3944 }
3945
3946 #[test]
3947 fn string_or_array_non_string_values() {
3948 let source = r"export default { entry: [42, true] };";
3949 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
3950 assert!(result.is_empty());
3951 }
3952
3953 #[test]
3954 fn array_nested_extraction() {
3955 let source = r#"
3956 export default defineConfig({
3957 test: {
3958 projects: [
3959 {
3960 test: {
3961 setupFiles: ["./test/setup-a.ts"]
3962 }
3963 },
3964 {
3965 test: {
3966 setupFiles: "./test/setup-b.ts"
3967 }
3968 }
3969 ]
3970 }
3971 });
3972 "#;
3973 let results = extract_config_array_nested_string_or_array(
3974 source,
3975 &ts_path(),
3976 &["test", "projects"],
3977 &["test", "setupFiles"],
3978 );
3979 assert!(results.contains(&"./test/setup-a.ts".to_string()));
3980 assert!(results.contains(&"./test/setup-b.ts".to_string()));
3981 }
3982
3983 #[test]
3984 fn array_nested_empty_when_no_array() {
3985 let source = r#"export default { test: { projects: "not-an-array" } };"#;
3986 let results = extract_config_array_nested_string_or_array(
3987 source,
3988 &js_path(),
3989 &["test", "projects"],
3990 &["test", "setupFiles"],
3991 );
3992 assert!(results.is_empty());
3993 }
3994
3995 #[test]
3996 fn object_nested_extraction() {
3997 let source = r#"{
3998 "projects": {
3999 "app-one": {
4000 "architect": {
4001 "build": {
4002 "options": {
4003 "styles": ["src/styles.css"]
4004 }
4005 }
4006 }
4007 }
4008 }
4009 }"#;
4010 let results = extract_config_object_nested_string_or_array(
4011 source,
4012 &json_path(),
4013 &["projects"],
4014 &["architect", "build", "options", "styles"],
4015 );
4016 assert_eq!(results, vec!["src/styles.css"]);
4017 }
4018
4019 #[test]
4020 fn array_with_object_input_form_extracted() {
4021 let source = r#"{
4022 "projects": {
4023 "app": {
4024 "architect": {
4025 "build": {
4026 "options": {
4027 "styles": [
4028 "src/styles.scss",
4029 { "input": "src/theme.scss", "bundleName": "theme", "inject": false },
4030 { "bundleName": "lazy-only" }
4031 ]
4032 }
4033 }
4034 }
4035 }
4036 }
4037 }"#;
4038 let results = extract_config_object_nested_string_or_array(
4039 source,
4040 &json_path(),
4041 &["projects"],
4042 &["architect", "build", "options", "styles"],
4043 );
4044 assert!(
4045 results.contains(&"src/styles.scss".to_string()),
4046 "string form must still work: {results:?}"
4047 );
4048 assert!(
4049 results.contains(&"src/theme.scss".to_string()),
4050 "object form with `input` must be extracted: {results:?}"
4051 );
4052 assert!(
4053 !results.contains(&"lazy-only".to_string()),
4054 "bundleName must not be misinterpreted as a path: {results:?}"
4055 );
4056 assert!(
4057 !results.contains(&"theme".to_string()),
4058 "bundleName from full object must not leak: {results:?}"
4059 );
4060 }
4061
4062 #[test]
4063 fn object_nested_strings_extraction() {
4064 let source = r#"{
4065 "targets": {
4066 "build": {
4067 "executor": "@angular/build:application"
4068 },
4069 "test": {
4070 "executor": "@nx/vite:test"
4071 }
4072 }
4073 }"#;
4074 let results =
4075 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
4076 assert!(results.contains(&"@angular/build:application".to_string()));
4077 assert!(results.contains(&"@nx/vite:test".to_string()));
4078 }
4079
4080 #[test]
4081 fn require_strings_direct_call() {
4082 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
4083 let deps = extract_config_require_strings(source, &js_path(), "adapter");
4084 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
4085 }
4086
4087 #[test]
4088 fn require_strings_no_matching_key() {
4089 let source = r"module.exports = { other: require('something') };";
4090 let deps = extract_config_require_strings(source, &js_path(), "plugins");
4091 assert!(deps.is_empty());
4092 }
4093
4094 #[test]
4095 fn extract_imports_no_imports() {
4096 let source = r"export default {};";
4097 let imports = extract_imports(source, &js_path());
4098 assert!(imports.is_empty());
4099 }
4100
4101 #[test]
4102 fn extract_imports_side_effect_import() {
4103 let source = r"
4104 import 'polyfill';
4105 import './local-setup';
4106 export default {};
4107 ";
4108 let imports = extract_imports(source, &js_path());
4109 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
4110 }
4111
4112 #[test]
4113 fn extract_imports_mixed_specifiers() {
4114 let source = r"
4115 import defaultExport from 'module-a';
4116 import { named } from 'module-b';
4117 import * as ns from 'module-c';
4118 export default {};
4119 ";
4120 let imports = extract_imports(source, &js_path());
4121 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
4122 }
4123
4124 #[test]
4125 fn template_literal_in_string_or_array() {
4126 let source = r"export default { entry: `./src/index.ts` };";
4127 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
4128 assert_eq!(result, vec!["./src/index.ts"]);
4129 }
4130
4131 #[test]
4132 fn template_literal_in_config_string() {
4133 let source = r"export default { testDir: `./tests` };";
4134 let val = extract_config_string(source, &js_path(), &["testDir"]);
4135 assert_eq!(val, Some("./tests".to_string()));
4136 }
4137
4138 #[test]
4139 fn template_literal_command_recovers_static_command_tokens() {
4140 let source = r"
4141 const PORT = 3000;
4142 export default {
4143 webServer: {
4144 command: `pnpm exec srvx --port ${PORT} --hostname 127.0.0.1`
4145 }
4146 };
4147 ";
4148 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
4149 assert_eq!(
4150 val,
4151 Some("pnpm exec srvx --port --hostname 127.0.0.1".to_string())
4152 );
4153 }
4154
4155 #[test]
4156 fn template_literal_command_skips_dynamic_prefix() {
4157 let source = r"
4158 export default {
4159 webServer: { command: `${serverCommand} && pnpm exec srvx` }
4160 };
4161 ";
4162 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
4163 assert!(val.is_none());
4164 }
4165
4166 #[test]
4167 fn template_literal_command_skips_split_static_token() {
4168 let source = r"
4169 export default {
4170 webServer: { command: `pnpm exec sr${part}vx --port 3000` }
4171 };
4172 ";
4173 let val = extract_config_command(source, &ts_path(), &["webServer", "command"]);
4174 assert!(val.is_none());
4175 }
4176
4177 #[test]
4178 fn array_object_command_pairs_recover_template_command() {
4179 let source = r"
4180 const PORT = 3000;
4181 export default {
4182 webServer: [
4183 {
4184 command: `pnpm exec srvx --port ${PORT}`,
4185 cwd: 'apps/web'
4186 }
4187 ]
4188 };
4189 ";
4190 let pairs = extract_config_array_object_command_pairs(
4191 source,
4192 &ts_path(),
4193 &["webServer"],
4194 "command",
4195 "cwd",
4196 );
4197 assert_eq!(
4198 pairs,
4199 vec![(
4200 "pnpm exec srvx --port ".to_string(),
4201 Some("apps/web".to_string())
4202 )]
4203 );
4204 }
4205
4206 #[test]
4207 fn nested_string_array_empty_path() {
4208 let source = r#"export default { items: ["a", "b"] };"#;
4209 let result = extract_config_string_array(source, &js_path(), &[]);
4210 assert!(result.is_empty());
4211 }
4212
4213 #[test]
4214 fn nested_string_empty_path() {
4215 let source = r#"export default { key: "val" };"#;
4216 let result = extract_config_string(source, &js_path(), &[]);
4217 assert!(result.is_none());
4218 }
4219
4220 #[test]
4221 fn object_keys_empty_path() {
4222 let source = r"export default { plugins: {} };";
4223 let result = extract_config_object_keys(source, &js_path(), &[]);
4224 assert!(result.is_empty());
4225 }
4226
4227 #[test]
4228 fn no_config_object_returns_empty() {
4229 let source = r"const x = 42;";
4230 let result = extract_config_string(source, &js_path(), &["key"]);
4231 assert!(result.is_none());
4232
4233 let arr = extract_config_string_array(source, &js_path(), &["items"]);
4234 assert!(arr.is_empty());
4235
4236 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
4237 assert!(keys.is_empty());
4238 }
4239
4240 #[test]
4241 fn property_with_string_key() {
4242 let source = r#"export default { "string-key": "value" };"#;
4243 let val = extract_config_string(source, &js_path(), &["string-key"]);
4244 assert_eq!(val, Some("value".to_string()));
4245 }
4246
4247 #[test]
4248 fn nested_navigation_through_non_object() {
4249 let source = r#"export default { level1: "not-an-object" };"#;
4250 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
4251 assert!(val.is_none());
4252 }
4253
4254 #[test]
4255 fn variable_reference_untyped() {
4256 let source = r#"
4257 const config = {
4258 testDir: "./tests"
4259 };
4260 export default config;
4261 "#;
4262 let val = extract_config_string(source, &js_path(), &["testDir"]);
4263 assert_eq!(val, Some("./tests".to_string()));
4264 }
4265
4266 #[test]
4267 fn variable_reference_to_an_exported_const() {
4268 for source in [
4269 r#"
4270 export const config = { testDir: "./tests" };
4271 export default config;
4272 "#,
4273 r#"
4274 export const config = { testDir: "./tests" };
4275 module.exports = config;
4276 "#,
4277 ] {
4278 let val = extract_config_string(source, &js_path(), &["testDir"]);
4279 assert_eq!(val, Some("./tests".to_string()), "source: {source}");
4280 }
4281 }
4282
4283 #[test]
4284 fn variable_reference_with_type_annotation() {
4285 let source = r#"
4286 import type { StorybookConfig } from '@storybook/react-vite';
4287 const config: StorybookConfig = {
4288 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
4289 framework: "@storybook/react-vite"
4290 };
4291 export default config;
4292 "#;
4293 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
4294 assert_eq!(
4295 addons,
4296 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
4297 );
4298
4299 let framework = extract_config_string(source, &ts_path(), &["framework"]);
4300 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
4301 }
4302
4303 #[test]
4304 fn variable_reference_with_define_config() {
4305 let source = r#"
4306 import { defineConfig } from 'vitest/config';
4307 const config = defineConfig({
4308 test: {
4309 include: ["**/*.test.ts"]
4310 }
4311 });
4312 export default config;
4313 "#;
4314 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
4315 assert_eq!(include, vec!["**/*.test.ts"]);
4316 }
4317
4318 #[test]
4319 fn ts_satisfies_direct_export() {
4320 let source = r#"
4321 export default {
4322 testDir: "./tests"
4323 } satisfies PlaywrightTestConfig;
4324 "#;
4325 let val = extract_config_string(source, &ts_path(), &["testDir"]);
4326 assert_eq!(val, Some("./tests".to_string()));
4327 }
4328
4329 #[test]
4330 fn ts_as_direct_export() {
4331 let source = r#"
4332 export default {
4333 testDir: "./tests"
4334 } as const;
4335 "#;
4336 let val = extract_config_string(source, &ts_path(), &["testDir"]);
4337 assert_eq!(val, Some("./tests".to_string()));
4338 }
4339
4340 fn aliases(source: &str) -> Vec<(String, String)> {
4343 extract_config_aliases(source, &js_path(), &["resolve", "alias"])
4344 }
4345
4346 #[test]
4347 fn aliases_inline_object_still_extracted() {
4348 let source = r#"
4350 export default defineConfig({
4351 resolve: { alias: { "@": "./src", utils: "../../utils" } }
4352 });
4353 "#;
4354 let mut got = aliases(source);
4355 got.sort();
4356 assert_eq!(
4357 got,
4358 vec![
4359 ("@".to_string(), "./src".to_string()),
4360 ("utils".to_string(), "../../utils".to_string()),
4361 ]
4362 );
4363 }
4364
4365 #[test]
4366 fn aliases_inline_array_still_extracted() {
4367 let source = r#"
4368 export default defineConfig({
4369 resolve: { alias: [{ find: "@", replacement: "./src" }] }
4370 });
4371 "#;
4372 assert_eq!(
4373 aliases(source),
4374 vec![("@".to_string(), "./src".to_string())]
4375 );
4376 }
4377
4378 #[test]
4379 fn aliases_local_const_array_identifier() {
4380 let source = r#"
4381 const sharedAliases = [{ find: "@", replacement: "./src" }];
4382 export default defineConfig({ resolve: { alias: sharedAliases } });
4383 "#;
4384 assert_eq!(
4385 aliases(source),
4386 vec![("@".to_string(), "./src".to_string())]
4387 );
4388 }
4389
4390 #[test]
4391 fn aliases_local_const_object_identifier() {
4392 let source = r#"
4393 const sharedAliases = { "@": "./src" };
4394 export default defineConfig({ resolve: { alias: sharedAliases } });
4395 "#;
4396 assert_eq!(
4397 aliases(source),
4398 vec![("@".to_string(), "./src".to_string())]
4399 );
4400 }
4401
4402 #[test]
4403 fn aliases_array_spread_of_identifiers_and_inline() {
4404 let source = r##"
4405 const a = [{ find: "@", replacement: "./src" }];
4406 const b = [{ find: "~", replacement: "./lib" }];
4407 export default defineConfig({
4408 resolve: { alias: [...a, ...b, { find: "#", replacement: "./test" }] }
4409 });
4410 "##;
4411 let mut got = aliases(source);
4412 got.sort();
4413 assert_eq!(
4414 got,
4415 vec![
4416 ("#".to_string(), "./test".to_string()),
4417 ("@".to_string(), "./src".to_string()),
4418 ("~".to_string(), "./lib".to_string()),
4419 ]
4420 );
4421 }
4422
4423 #[test]
4424 fn aliases_object_spread_of_identifier_and_inline() {
4425 let source = r#"
4426 const base = { "@": "./src" };
4427 export default defineConfig({
4428 resolve: { alias: { ...base, "~": "./lib" } }
4429 });
4430 "#;
4431 let mut got = aliases(source);
4432 got.sort();
4433 assert_eq!(
4434 got,
4435 vec![
4436 ("@".to_string(), "./src".to_string()),
4437 ("~".to_string(), "./lib".to_string()),
4438 ]
4439 );
4440 }
4441
4442 #[test]
4443 fn aliases_local_const_chained_identifier() {
4444 let source = r#"
4446 const real = [{ find: "@", replacement: "./src" }];
4447 const alias2 = real;
4448 export default defineConfig({ resolve: { alias: alias2 } });
4449 "#;
4450 assert_eq!(
4451 aliases(source),
4452 vec![("@".to_string(), "./src".to_string())]
4453 );
4454 }
4455
4456 #[test]
4457 fn aliases_imported_named_identifier_from_sibling() {
4458 let dir = tempfile::tempdir().unwrap();
4459 std::fs::write(
4460 dir.path().join("vite.shared.js"),
4461 r#"export const sharedAliases = [
4462 { find: "@", replacement: new URL("./src", import.meta.url).pathname },
4463 ];"#,
4464 )
4465 .unwrap();
4466 let config = dir.path().join("vite.config.js");
4467 let source = r#"
4468 import { defineConfig } from "vite";
4469 import { sharedAliases } from "./vite.shared.js";
4470 export default defineConfig({ resolve: { alias: sharedAliases } });
4471 "#;
4472 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4473 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4474 }
4475
4476 #[test]
4477 fn aliases_imported_extensionless_specifier_probed() {
4478 let dir = tempfile::tempdir().unwrap();
4479 std::fs::write(
4480 dir.path().join("aliases.mjs"),
4481 r#"export const sharedAliases = { "@": "./src" };"#,
4482 )
4483 .unwrap();
4484 let config = dir.path().join("vite.config.ts");
4485 let source = r#"
4486 import { sharedAliases } from "./aliases";
4487 export default defineConfig({ resolve: { alias: sharedAliases } });
4488 "#;
4489 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4490 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4491 }
4492
4493 #[test]
4494 fn aliases_imported_default_export_from_sibling() {
4495 let dir = tempfile::tempdir().unwrap();
4496 std::fs::write(
4497 dir.path().join("aliases.js"),
4498 r#"export default [{ find: "@", replacement: "./src" }];"#,
4499 )
4500 .unwrap();
4501 let config = dir.path().join("vite.config.js");
4502 let source = r#"
4503 import sharedAliases from "./aliases.js";
4504 export default defineConfig({ resolve: { alias: sharedAliases } });
4505 "#;
4506 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4507 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4508 }
4509
4510 #[test]
4511 fn aliases_imported_spread_from_two_siblings() {
4512 let dir = tempfile::tempdir().unwrap();
4513 std::fs::write(
4514 dir.path().join("a.js"),
4515 r#"export const a = [{ find: "@", replacement: "./src" }];"#,
4516 )
4517 .unwrap();
4518 std::fs::write(
4519 dir.path().join("b.js"),
4520 r#"export const b = [{ find: "~", replacement: "./lib" }];"#,
4521 )
4522 .unwrap();
4523 let config = dir.path().join("vite.config.js");
4524 let source = r#"
4525 import { a } from "./a.js";
4526 import { b } from "./b.js";
4527 export default defineConfig({ resolve: { alias: [...a, ...b] } });
4528 "#;
4529 let mut got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4530 got.sort();
4531 assert_eq!(
4532 got,
4533 vec![
4534 ("@".to_string(), "./src".to_string()),
4535 ("~".to_string(), "./lib".to_string()),
4536 ]
4537 );
4538 }
4539
4540 #[test]
4541 fn aliases_import_cycle_terminates() {
4542 let dir = tempfile::tempdir().unwrap();
4545 std::fs::write(
4546 dir.path().join("a.js"),
4547 r#"import { b } from "./b.js";
4548 export const a = [{ find: "@", replacement: "./src" }, ...b];"#,
4549 )
4550 .unwrap();
4551 std::fs::write(
4552 dir.path().join("b.js"),
4553 r#"import { a } from "./a.js";
4554 export const b = [...a];"#,
4555 )
4556 .unwrap();
4557 let config = dir.path().join("vite.config.js");
4558 let source = r#"
4559 import { a } from "./a.js";
4560 export default defineConfig({ resolve: { alias: a } });
4561 "#;
4562 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
4563 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
4564 }
4565
4566 #[test]
4567 fn aliases_non_relative_import_not_followed() {
4568 let source = r#"
4571 import { sharedAliases } from "some-pkg";
4572 export default defineConfig({ resolve: { alias: sharedAliases } });
4573 "#;
4574 let dir = tempfile::tempdir().unwrap();
4575 let config = dir.path().join("vite.config.js");
4576 assert!(extract_config_aliases(source, &config, &["resolve", "alias"]).is_empty());
4577 }
4578
4579 #[test]
4580 fn aliases_object_array_value_takes_first_entry() {
4581 let source = r#"
4586 export default {
4587 compilerOptions: { paths: { "@/*": ["./src/*"], "~/*": ["./lib/*", "./vendor/*"] } }
4588 };
4589 "#;
4590 let mut got = extract_config_aliases(source, &js_path(), &["compilerOptions", "paths"]);
4591 got.sort();
4592 assert_eq!(
4593 got,
4594 vec![
4595 ("@/*".to_string(), "./src/*".to_string()),
4596 ("~/*".to_string(), "./lib/*".to_string()),
4597 ]
4598 );
4599 }
4600
4601 #[test]
4602 fn aliases_kinded_preserves_is_bare_through_resolution() {
4603 let source = r#"
4606 const a = [{ find: "lodash-es", replacement: "lodash" }];
4607 export default defineConfig({
4608 resolve: { alias: [...a, { find: "@", replacement: "./src" }] }
4609 });
4610 "#;
4611 let mut got = extract_config_aliases_kinded(source, &js_path(), &["resolve", "alias"]);
4612 got.sort();
4613 assert_eq!(
4614 got,
4615 vec![
4616 ("@".to_string(), "./src".to_string(), false),
4617 ("lodash-es".to_string(), "lodash".to_string(), true),
4618 ]
4619 );
4620 }
4621
4622 #[test]
4623 fn aliases_kinded_preserves_is_bare_through_imported_spread() {
4624 let dir = tempfile::tempdir().unwrap();
4625 std::fs::write(
4626 dir.path().join("aliases.js"),
4627 r#"export const packageAliases = [{ find: "lodash-es", replacement: "lodash" }];"#,
4628 )
4629 .unwrap();
4630 let config = dir.path().join("vite.config.js");
4631 let source = r#"
4632 import { packageAliases } from "./aliases.js";
4633 export default defineConfig({
4634 resolve: { alias: [...packageAliases, { find: "@", replacement: "./src" }] }
4635 });
4636 "#;
4637 let mut got = extract_config_aliases_kinded(source, &config, &["resolve", "alias"]);
4638 got.sort();
4639 assert_eq!(
4640 got,
4641 vec![
4642 ("@".to_string(), "./src".to_string(), false),
4643 ("lodash-es".to_string(), "lodash".to_string(), true),
4644 ]
4645 );
4646 }
4647
4648 #[test]
4651 fn extract_command_string_literal() {
4652 let source = r#"export default { start: "node server.js" };"#;
4653 let val = extract_config_command(source, &js_path(), &["start"]);
4654 assert_eq!(val, Some("node server.js".to_string()));
4655 }
4656
4657 #[test]
4658 fn extract_command_nested_path() {
4659 let source = r#"
4660 export default {
4661 scripts: {
4662 dev: "vite dev"
4663 }
4664 };
4665 "#;
4666 let val = extract_config_command(source, &js_path(), &["scripts", "dev"]);
4667 assert_eq!(val, Some("vite dev".to_string()));
4668 }
4669
4670 #[test]
4671 fn extract_command_missing_key_returns_none() {
4672 let source = r#"export default { other: "val" };"#;
4673 let val = extract_config_command(source, &js_path(), &["start"]);
4674 assert!(val.is_none());
4675 }
4676
4677 #[test]
4678 fn extract_command_ts_as_expression() {
4679 let source = r#"export default { start: "node server.js" as string };"#;
4680 let val = extract_config_command(source, &ts_path(), &["start"]);
4681 assert_eq!(val, Some("node server.js".to_string()));
4682 }
4683
4684 #[test]
4685 fn extract_command_ts_satisfies_expression() {
4686 let source = r#"export default { start: "node server.js" satisfies string };"#;
4687 let val = extract_config_command(source, &ts_path(), &["start"]);
4688 assert_eq!(val, Some("node server.js".to_string()));
4689 }
4690
4691 #[test]
4692 fn extract_command_parenthesized_expression() {
4693 let source = r#"export default { start: ("node server.js") };"#;
4694 let val = extract_config_command(source, &js_path(), &["start"]);
4695 assert_eq!(val, Some("node server.js".to_string()));
4696 }
4697
4698 #[test]
4699 fn extract_command_empty_path_returns_none() {
4700 let source = r#"export default { start: "node server.js" };"#;
4701 let val = extract_config_command(source, &js_path(), &[]);
4702 assert!(val.is_none());
4703 }
4704
4705 #[test]
4708 fn truthy_bool_or_object_with_true_value() {
4709 let source = r"export default { typescript: true };";
4710 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4711 assert!(result);
4712 }
4713
4714 #[test]
4715 fn truthy_bool_or_object_with_false_value() {
4716 let source = r"export default { typescript: false };";
4717 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4718 assert!(!result);
4719 }
4720
4721 #[test]
4722 fn truthy_bool_or_object_with_object_value() {
4723 let source = r#"export default { typescript: { reactDocgen: "react-docgen" } };"#;
4724 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4725 assert!(result);
4726 }
4727
4728 #[test]
4729 fn truthy_bool_or_object_missing_key_returns_false() {
4730 let source = r"export default { other: true };";
4731 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4732 assert!(!result);
4733 }
4734
4735 #[test]
4736 fn truthy_bool_or_object_with_string_value_returns_false() {
4737 let source = r#"export default { typescript: "yes" };"#;
4739 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4740 assert!(!result);
4741 }
4742
4743 #[test]
4744 fn truthy_bool_or_object_ts_satisfies_wrapper() {
4745 let source = r"export default { typescript: (true satisfies boolean) };";
4746 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4747 assert!(result);
4748 }
4749
4750 #[test]
4751 fn truthy_bool_or_object_ts_as_wrapper() {
4752 let source = r"export default { typescript: (true as boolean) };";
4753 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4754 assert!(result);
4755 }
4756
4757 #[test]
4758 fn truthy_bool_or_object_parenthesized_wrapper() {
4759 let source = r"export default { typescript: (true) };";
4760 let result = extract_config_truthy_bool_or_object(source, &ts_path(), &["typescript"]);
4761 assert!(result);
4762 }
4763
4764 #[test]
4770 fn static_dir_entries_object_form_exercises_property_string() {
4771 let source = r#"
4774 export default {
4775 staticDirs: [
4776 { from: "./media", to: "/assets" }
4777 ]
4778 };
4779 "#;
4780 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
4781 assert_eq!(
4782 entries,
4783 vec![("./media".to_string(), Some("/assets".to_string()))]
4784 );
4785 }
4786
4787 #[test]
4790 fn expression_to_path_values_array_form_via_config_path() {
4791 let source = r#"export default { entries: ["./src/a.ts", "./src/b.ts"] };"#;
4794 let result = extract_config_string_or_array(source, &js_path(), &["entries"]);
4795 assert_eq!(result, vec!["./src/a.ts", "./src/b.ts"]);
4796 }
4797
4798 #[test]
4801 fn array_nested_aliases_object_form() {
4802 let source = r#"
4803 export default {
4804 test: {
4805 projects: [
4806 {
4807 resolve: {
4808 alias: { "@": "./src" }
4809 }
4810 }
4811 ]
4812 }
4813 };
4814 "#;
4815 let aliases = extract_config_array_nested_aliases_kinded(
4816 source,
4817 &ts_path(),
4818 &["test", "projects"],
4819 &["resolve", "alias"],
4820 );
4821 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
4822 }
4823
4824 #[test]
4825 fn array_nested_aliases_empty_when_path_is_not_array() {
4826 let source = r#"export default { test: { projects: "not-an-array" } };"#;
4827 let aliases = extract_config_array_nested_aliases_kinded(
4828 source,
4829 &ts_path(),
4830 &["test", "projects"],
4831 &["resolve", "alias"],
4832 );
4833 assert!(aliases.is_empty());
4834 }
4835
4836 #[test]
4837 fn array_nested_aliases_kinded_tracks_is_bare() {
4838 let source = r#"
4839 export default {
4840 projects: [
4841 {
4842 resolve: {
4843 alias: [
4844 { find: "lodash-es", replacement: "lodash" },
4845 { find: "@", replacement: "./src" }
4846 ]
4847 }
4848 }
4849 ]
4850 };
4851 "#;
4852 let mut aliases = extract_config_array_nested_aliases_kinded(
4853 source,
4854 &ts_path(),
4855 &["projects"],
4856 &["resolve", "alias"],
4857 );
4858 aliases.sort();
4859 assert_eq!(
4860 aliases,
4861 vec![
4862 ("@".to_string(), "./src".to_string(), false),
4863 ("lodash-es".to_string(), "lodash".to_string(), true),
4864 ]
4865 );
4866 }
4867
4868 #[test]
4871 fn default_export_array_aliases_kinded_extracts_from_workspace_config() {
4872 let source = r#"
4873 export default [
4874 {
4875 resolve: {
4876 alias: { "@": "./src" }
4877 }
4878 },
4879 {
4880 resolve: {
4881 alias: [{ find: "~", replacement: "./lib" }]
4882 }
4883 }
4884 ];
4885 "#;
4886 let mut aliases =
4887 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4888 aliases.sort();
4889 assert_eq!(
4890 aliases,
4891 vec![
4892 ("@".to_string(), "./src".to_string(), false),
4893 ("~".to_string(), "./lib".to_string(), false),
4894 ]
4895 );
4896 }
4897
4898 #[test]
4899 fn default_export_array_aliases_kinded_define_workspace_wrapper() {
4900 let source = r#"
4901 export default defineWorkspace([
4902 {
4903 resolve: { alias: { "@": "./src" } }
4904 }
4905 ]);
4906 "#;
4907 let aliases =
4908 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4909 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
4910 }
4911
4912 #[test]
4913 fn default_export_array_aliases_kinded_empty_when_no_alias_path() {
4914 let source = r#"
4915 export default [
4916 { test: { include: ["**/*.test.ts"] } }
4917 ];
4918 "#;
4919 let aliases =
4920 extract_default_export_array_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
4921 assert!(aliases.is_empty());
4922 }
4923
4924 #[test]
4927 fn config_default_export_unreachable_when_no_export() {
4928 let source = r"const x = 42;";
4929 assert!(config_default_export_unreachable(source, &js_path()));
4930 }
4931
4932 #[test]
4933 fn config_default_export_unreachable_false_for_object_export() {
4934 let source = r#"export default { key: "value" };"#;
4935 assert!(!config_default_export_unreachable(source, &js_path()));
4936 }
4937
4938 #[test]
4939 fn config_default_export_unreachable_false_for_array_export() {
4940 let source = r#"export default ["a", "b"];"#;
4941 assert!(!config_default_export_unreachable(source, &js_path()));
4942 }
4943
4944 #[test]
4945 fn config_default_export_unreachable_true_for_function_without_return_object() {
4946 let source = r"export default function config() { return 42; }";
4948 assert!(config_default_export_unreachable(source, &js_path()));
4949 }
4950
4951 #[test]
4954 fn static_dir_entries_string_and_object_form() {
4955 let source = r#"
4956 export default {
4957 staticDirs: [
4958 "./public",
4959 { from: "../assets", to: "/static" }
4960 ]
4961 };
4962 "#;
4963 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
4964 assert_eq!(
4965 entries,
4966 vec![
4967 ("./public".to_string(), None),
4968 ("../assets".to_string(), Some("/static".to_string())),
4969 ]
4970 );
4971 }
4972
4973 #[test]
4974 fn static_dir_entries_object_without_to() {
4975 let source = r#"
4976 export default {
4977 staticDirs: [
4978 { from: "./media" }
4979 ]
4980 };
4981 "#;
4982 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
4983 assert_eq!(entries, vec![("./media".to_string(), None)]);
4984 }
4985
4986 #[test]
4987 fn static_dir_entries_object_missing_from_skipped() {
4988 let source = r#"
4990 export default {
4991 staticDirs: [
4992 { to: "/target" },
4993 "./public"
4994 ]
4995 };
4996 "#;
4997 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
4998 assert_eq!(entries, vec![("./public".to_string(), None)]);
4999 }
5000
5001 #[test]
5002 fn static_dir_entries_empty_when_not_array() {
5003 let source = r#"export default { staticDirs: "./public" };"#;
5004 let entries = extract_config_static_dir_entries(source, &ts_path(), &["staticDirs"]);
5005 assert!(entries.is_empty());
5006 }
5007
5008 #[test]
5011 fn aliases_array_form_missing_find_or_replacement_skipped() {
5012 let source = r#"
5014 export default {
5015 resolve: {
5016 alias: [
5017 { replacement: "./src" },
5018 { find: "@" },
5019 { find: "~", replacement: "./lib" }
5020 ]
5021 }
5022 };
5023 "#;
5024 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5025 assert_eq!(aliases, vec![("~".to_string(), "./lib".to_string())]);
5026 }
5027
5028 #[test]
5029 fn aliases_object_form_computed_key_skipped() {
5030 let source = r#"
5032 const k = "@";
5033 export default {
5034 resolve: {
5035 alias: {
5036 [k]: "./src",
5037 "~": "./lib"
5038 }
5039 }
5040 };
5041 "#;
5042 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5043 assert_eq!(aliases, vec![("~".to_string(), "./lib".to_string())]);
5045 }
5046
5047 #[test]
5048 fn aliases_kinded_array_form_path_replacement_is_not_bare() {
5049 let source = r#"
5050 export default {
5051 resolve: {
5052 alias: [{ find: "@", replacement: "./src" }]
5053 }
5054 };
5055 "#;
5056 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5057 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string(), false)]);
5058 }
5059
5060 #[test]
5061 fn aliases_kinded_object_form_bare_and_path_discrimination() {
5062 let source = r#"
5063 export default {
5064 resolve: {
5065 alias: {
5066 "lodash-es": "lodash",
5067 "@": "./src"
5068 }
5069 }
5070 };
5071 "#;
5072 let mut aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5073 aliases.sort();
5074 assert_eq!(
5075 aliases,
5076 vec![
5077 ("@".to_string(), "./src".to_string(), false),
5078 ("lodash-es".to_string(), "lodash".to_string(), true),
5079 ]
5080 );
5081 }
5082
5083 #[test]
5084 fn aliases_kinded_parent_relative_replacement_is_not_bare() {
5085 let source = r#"
5086 export default {
5087 resolve: { alias: { "@": "../shared/src" } }
5088 };
5089 "#;
5090 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5091 assert_eq!(
5092 aliases,
5093 vec![("@".to_string(), "../shared/src".to_string(), false)]
5094 );
5095 }
5096
5097 #[test]
5098 fn aliases_kinded_absolute_replacement_is_not_bare() {
5099 let source = r#"
5100 export default {
5101 resolve: { alias: { "@": "/absolute/path" } }
5102 };
5103 "#;
5104 let aliases = extract_config_aliases_kinded(source, &ts_path(), &["resolve", "alias"]);
5105 assert_eq!(
5106 aliases,
5107 vec![("@".to_string(), "/absolute/path".to_string(), false)]
5108 );
5109 }
5110
5111 #[test]
5114 fn default_export_array_ts_as_wrapper() {
5115 let source = r"export default [] as string[];";
5117 assert!(!config_default_export_unreachable(source, &js_path()));
5118 }
5119
5120 #[test]
5121 fn default_export_array_ts_satisfies_wrapper() {
5122 let source = r"export default [] satisfies string[];";
5123 assert!(!config_default_export_unreachable(source, &ts_path()));
5124 }
5125
5126 #[test]
5127 fn default_export_array_define_config_call_wrapper() {
5128 let source = r#"export default defineConfig(["**/*.test.ts"]);"#;
5129 assert!(!config_default_export_unreachable(source, &ts_path()));
5130 }
5131
5132 #[test]
5135 fn shallow_strings_object_with_string_values() {
5136 let source = r#"
5138 export default {
5139 plugins: {
5140 autoprefixer: "autoprefixer",
5141 tailwindcss: "tailwindcss"
5142 }
5143 };
5144 "#;
5145 let vals = extract_config_shallow_strings(source, &js_path(), "plugins");
5146 assert!(vals.contains(&"autoprefixer".to_string()));
5147 assert!(vals.contains(&"tailwindcss".to_string()));
5148 }
5149
5150 #[test]
5151 fn shallow_strings_object_with_sub_array_first_element() {
5152 let source = r#"
5154 export default {
5155 reporters: {
5156 main: ["jest-junit", { outputFile: "report.xml" }],
5157 alt: ["html-reporter"]
5158 }
5159 };
5160 "#;
5161 let vals = extract_config_shallow_strings(source, &js_path(), "reporters");
5162 assert!(vals.contains(&"jest-junit".to_string()));
5163 assert!(vals.contains(&"html-reporter".to_string()));
5164 }
5165
5166 #[test]
5169 fn shallow_strings_or_object_property_non_array_single_string() {
5170 let source = r#"export default { jsPlugins: "eslint-plugin-foo" };"#;
5172 let vals = extract_config_shallow_strings_or_object_property(
5173 source,
5174 &ts_path(),
5175 "jsPlugins",
5176 "specifier",
5177 );
5178 assert_eq!(vals, vec!["eslint-plugin-foo"]);
5179 }
5180
5181 #[test]
5182 fn shallow_strings_or_object_property_ts_satisfies_array_element() {
5183 let source = r#"
5185 export default {
5186 jsPlugins: [
5187 ("eslint-plugin-a" satisfies string)
5188 ]
5189 };
5190 "#;
5191 let vals = extract_config_shallow_strings_or_object_property(
5192 source,
5193 &ts_path(),
5194 "jsPlugins",
5195 "specifier",
5196 );
5197 assert_eq!(vals, vec!["eslint-plugin-a"]);
5198 }
5199
5200 #[test]
5201 fn shallow_strings_or_object_property_ts_as_array_element() {
5202 let source = r#"
5203 export default {
5204 jsPlugins: [
5205 ("eslint-plugin-b" as string)
5206 ]
5207 };
5208 "#;
5209 let vals = extract_config_shallow_strings_or_object_property(
5210 source,
5211 &ts_path(),
5212 "jsPlugins",
5213 "specifier",
5214 );
5215 assert_eq!(vals, vec!["eslint-plugin-b"]);
5216 }
5217
5218 #[test]
5219 fn shallow_strings_or_object_property_sub_array_first_element_string() {
5220 let source = r#"
5222 export default {
5223 jsPlugins: [
5224 ["eslint-plugin-tuple-pkg", { options: true }]
5225 ]
5226 };
5227 "#;
5228 let vals = extract_config_shallow_strings_or_object_property(
5229 source,
5230 &ts_path(),
5231 "jsPlugins",
5232 "specifier",
5233 );
5234 assert_eq!(vals, vec!["eslint-plugin-tuple-pkg"]);
5235 }
5236
5237 #[test]
5240 fn array_object_command_pairs_basic() {
5241 let source = r#"
5242 export default {
5243 webServer: [
5244 { command: "node server.js", cwd: "packages/api" },
5245 { command: "vite dev" }
5246 ]
5247 };
5248 "#;
5249 let pairs = extract_config_array_object_command_pairs(
5250 source,
5251 &ts_path(),
5252 &["webServer"],
5253 "command",
5254 "cwd",
5255 );
5256 assert_eq!(
5257 pairs,
5258 vec![
5259 (
5260 "node server.js".to_string(),
5261 Some("packages/api".to_string())
5262 ),
5263 ("vite dev".to_string(), None),
5264 ]
5265 );
5266 }
5267
5268 #[test]
5269 fn array_object_command_pairs_skips_missing_command() {
5270 let source = r#"
5271 export default {
5272 webServer: [
5273 { cwd: "packages/api" },
5274 { command: "vite dev", cwd: "apps/web" }
5275 ]
5276 };
5277 "#;
5278 let pairs = extract_config_array_object_command_pairs(
5279 source,
5280 &ts_path(),
5281 &["webServer"],
5282 "command",
5283 "cwd",
5284 );
5285 assert_eq!(
5286 pairs,
5287 vec![("vite dev".to_string(), Some("apps/web".to_string()))]
5288 );
5289 }
5290
5291 #[test]
5292 fn array_object_command_pairs_empty_when_not_array() {
5293 let source = r#"export default { webServer: { command: "vite dev" } };"#;
5294 let pairs = extract_config_array_object_command_pairs(
5295 source,
5296 &ts_path(),
5297 &["webServer"],
5298 "command",
5299 "cwd",
5300 );
5301 assert!(pairs.is_empty());
5302 }
5303
5304 #[test]
5307 fn normalize_config_path_empty_string_returns_none() {
5308 let config_path = PathBuf::from("/project/vite.config.ts");
5309 let root = PathBuf::from("/project");
5310 assert_eq!(normalize_config_path("", &config_path, &root), None);
5311 }
5312
5313 #[test]
5314 fn normalize_config_path_escapes_to_above_root_returns_none() {
5315 let config_path = PathBuf::from("/project/vite.config.ts");
5316 let root = PathBuf::from("/project");
5317 assert_eq!(
5319 normalize_config_path("../../etc", &config_path, &root),
5320 None
5321 );
5322 }
5323
5324 #[test]
5325 fn normalize_config_path_dot_slash_resolves_relative_to_config_dir() {
5326 let config_path = PathBuf::from("/project/packages/app/vite.config.ts");
5327 let root = PathBuf::from("/project");
5328 assert_eq!(
5329 normalize_config_path("./src", &config_path, &root),
5330 Some("packages/app/src".to_string())
5331 );
5332 }
5333
5334 #[test]
5337 fn json_config_array_of_arrays_via_shallow_strings() {
5338 let source = r#"{"reporters": ["default", ["jest-junit", {}]]}"#;
5340 let vals = extract_config_shallow_strings(source, &json_path(), "reporters");
5341 assert_eq!(vals, vec!["default", "jest-junit"]);
5342 }
5343
5344 #[test]
5347 fn extract_config_path_string_literal() {
5348 let source = r#"export default { outDir: "./dist" };"#;
5349 let path = extract_config_path(source, &js_path(), &["outDir"]);
5350 assert_eq!(
5351 path.map(|p| p.to_string_lossy().replace('\\', "/")),
5352 Some("./dist".to_string())
5353 );
5354 }
5355
5356 #[test]
5357 fn extract_config_path_with_resolve_call() {
5358 let source = r#"
5359 import { resolve } from "node:path";
5360 export default { outDir: resolve(__dirname, "dist") };
5361 "#;
5362 let path = extract_config_path(source, &js_path(), &["outDir"]);
5363 assert_eq!(
5364 path.map(|p| p.to_string_lossy().replace('\\', "/")),
5365 Some("dist".to_string())
5366 );
5367 }
5368
5369 #[test]
5370 fn extract_config_path_missing_key_returns_none() {
5371 let source = r#"export default { other: "val" };"#;
5372 let path = extract_config_path(source, &js_path(), &["outDir"]);
5373 assert!(path.is_none());
5374 }
5375
5376 #[test]
5379 fn extract_imports_and_requires_both_forms() {
5380 let source = r"
5381 import foo from 'foo-pkg';
5382 require('bar-pkg');
5383 export default {};
5384 ";
5385 let sources = extract_imports_and_requires(source, &js_path());
5386 assert!(sources.contains(&"foo-pkg".to_string()));
5387 assert!(sources.contains(&"bar-pkg".to_string()));
5388 }
5389
5390 #[test]
5391 fn extract_imports_and_requires_skips_non_require_calls() {
5392 let source = r"
5393 import foo from 'foo-pkg';
5394 someOtherCall('bar-pkg');
5395 export default {};
5396 ";
5397 let sources = extract_imports_and_requires(source, &js_path());
5398 assert_eq!(sources, vec!["foo-pkg"]);
5399 }
5400
5401 #[test]
5404 fn nested_shallow_strings_non_object_nested_returns_empty() {
5405 let source = r#"export default { test: "not-an-object" };"#;
5407 let vals =
5408 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
5409 assert!(vals.is_empty());
5410 }
5411
5412 #[test]
5415 fn vite_react_babel_dependencies_namespace_import() {
5416 let source = r#"
5417 import * as react from "@vitejs/plugin-react";
5418
5419 export default defineConfig({
5420 plugins: [
5421 react.default({
5422 babel: {
5423 plugins: ["babel-plugin-ns"],
5424 },
5425 }),
5426 ],
5427 });
5428 "#;
5429 let deps = extract_vite_react_babel_dependencies(source, &ts_path());
5430 assert_eq!(deps, vec!["babel-plugin-ns".to_string()]);
5431 }
5432
5433 #[test]
5436 fn property_strings_deeply_nested_object_values() {
5437 let source = r#"
5439 export default {
5440 settings: {
5441 a: "val-a",
5442 b: {
5443 c: "val-c",
5444 d: ["val-d1", "val-d2"]
5445 }
5446 }
5447 };
5448 "#;
5449 let values = extract_config_property_strings(source, &js_path(), "settings");
5450 assert!(values.contains(&"val-a".to_string()));
5451 assert!(values.contains(&"val-c".to_string()));
5452 assert!(values.contains(&"val-d1".to_string()));
5453 assert!(values.contains(&"val-d2".to_string()));
5454 }
5455
5456 #[test]
5459 fn aliases_exported_const_form_resolves() {
5460 let source = r#"
5462 export const sharedAliases = { "@": "./src" };
5463 export default defineConfig({ resolve: { alias: sharedAliases } });
5464 "#;
5465 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5466 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
5467 }
5468
5469 #[test]
5472 fn aliases_imported_from_sibling_directory_index_file() {
5473 let dir = tempfile::tempdir().unwrap();
5476 let aliases_dir = dir.path().join("aliases");
5477 std::fs::create_dir_all(&aliases_dir).unwrap();
5478 std::fs::write(
5479 aliases_dir.join("index.js"),
5480 r#"export const aliases = [{ find: "@", replacement: "./src" }];"#,
5481 )
5482 .unwrap();
5483 let config = dir.path().join("vite.config.js");
5484 let source = r#"
5485 import { aliases } from "./aliases";
5486 export default defineConfig({ resolve: { alias: aliases } });
5487 "#;
5488 let got = extract_config_aliases(source, &config, &["resolve", "alias"]);
5489 assert_eq!(got, vec![("@".to_string(), "./src".to_string())]);
5490 }
5491
5492 #[test]
5495 fn aliases_depth_limit_terminates_deep_chain() {
5496 let source = r#"
5499 const a9 = [{ find: "@", replacement: "./src" }];
5500 const a8 = a9;
5501 const a7 = a8;
5502 const a6 = a7;
5503 const a5 = a6;
5504 const a4 = a5;
5505 const a3 = a4;
5506 const a2 = a3;
5507 const a1 = a2;
5508 export default defineConfig({ resolve: { alias: a1 } });
5509 "#;
5510 let got = extract_config_aliases(source, &js_path(), &["resolve", "alias"]);
5512 let _ = got; }
5514
5515 #[test]
5518 fn extract_aliases_file_url_to_path_new_url() {
5519 let source = r#"
5521 import { fileURLToPath, URL } from 'node:url';
5522 export default {
5523 resolve: {
5524 alias: {
5525 "@": fileURLToPath(new URL("./src", import.meta.url))
5526 }
5527 }
5528 };
5529 "#;
5530 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5531 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
5532 }
5533
5534 #[test]
5535 fn extract_path_via_new_url_pathname_member() {
5536 let source = r#"
5538 export default {
5539 resolve: {
5540 alias: {
5541 "@": new URL("./src", import.meta.url).pathname
5542 }
5543 }
5544 };
5545 "#;
5546 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
5547 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
5548 }
5549
5550 #[test]
5553 fn truthy_bool_or_object_null_literal_returns_false() {
5554 let source = r"export default { typescript: null };";
5556 let result = extract_config_truthy_bool_or_object(source, &js_path(), &["typescript"]);
5557 assert!(!result);
5558 }
5559
5560 #[test]
5563 fn string_array_non_array_value_returns_empty() {
5564 let source = r#"export default { items: "not-an-array" };"#;
5565 let result = extract_config_string_array(source, &js_path(), &["items"]);
5566 assert!(result.is_empty());
5567 }
5568
5569 #[test]
5572 fn object_nested_empty_when_inner_value_is_not_object() {
5573 let source = r#"export default { targets: { build: "not-an-object" } };"#;
5575 let results =
5576 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
5577 assert!(results.is_empty());
5578 }
5579
5580 #[test]
5583 fn array_nested_string_or_array_missing_inner_path_returns_empty() {
5584 let source = r#"
5585 export default {
5586 test: {
5587 projects: [
5588 { test: { include: ["**/*.test.ts"] } }
5589 ]
5590 }
5591 };
5592 "#;
5593 let results = extract_config_array_nested_string_or_array(
5594 source,
5595 &ts_path(),
5596 &["test", "projects"],
5597 &["test", "setupFiles"],
5598 );
5599 assert!(results.is_empty());
5600 }
5601
5602 #[test]
5603 fn wrapped_named_const_default_export_resolves() {
5604 let source = r#"
5607 import createMDX from "@next/mdx";
5608 const nextConfig = { pageExtensions: ["ts", "tsx", "md", "mdx"] };
5609 const withMDX = createMDX({});
5610 export default withMDX(nextConfig);
5611 "#;
5612 let exts = extract_config_string_array(source, &ts_path(), &["pageExtensions"]);
5613 assert_eq!(exts, vec!["ts", "tsx", "md", "mdx"]);
5614 }
5615
5616 #[test]
5617 fn wrapped_named_const_module_exports_resolves() {
5618 let source = r#"
5620 const nextJest = require("next/jest");
5621 const createJestConfig = nextJest();
5622 const customConfig = { testMatch: ["**/*.test.ts"] };
5623 module.exports = createJestConfig(customConfig);
5624 "#;
5625 let matches = extract_config_string_array(source, &js_path(), &["testMatch"]);
5626 assert_eq!(matches, vec!["**/*.test.ts"]);
5627 }
5628
5629 #[test]
5630 fn wrapped_named_const_nested_and_curried_resolve() {
5631 let nested = r#"
5632 const nextConfig = { pageExtensions: ["mdx"] };
5633 const withMDX = (c) => c;
5634 const withFoo = (c) => c;
5635 export default withMDX(withFoo(nextConfig));
5636 "#;
5637 assert_eq!(
5638 extract_config_string_array(nested, &js_path(), &["pageExtensions"]),
5639 vec!["mdx"]
5640 );
5641
5642 let curried = r#"
5643 const nextConfig = { pageExtensions: ["md"] };
5644 const compose = (..._p) => (c) => c;
5645 export default compose(a, b)(nextConfig);
5646 "#;
5647 assert_eq!(
5648 extract_config_string_array(curried, &js_path(), &["pageExtensions"]),
5649 vec!["md"]
5650 );
5651 }
5652
5653 #[test]
5654 fn wrapped_inline_object_still_resolves() {
5655 let source = r#"
5657 const withMDX = createMDX({});
5658 export default withMDX({ pageExtensions: ["mdx"] });
5659 "#;
5660 assert_eq!(
5661 extract_config_string_array(source, &js_path(), &["pageExtensions"]),
5662 vec!["mdx"]
5663 );
5664 }
5665}