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