1use std::path::{Path, PathBuf};
15
16use oxc_allocator::Allocator;
17#[allow(clippy::wildcard_imports, reason = "many AST types used")]
18use oxc_ast::ast::*;
19use oxc_parser::Parser;
20use oxc_span::SourceType;
21
22#[must_use]
24pub fn extract_imports(source: &str, path: &Path) -> Vec<String> {
25 extract_from_source(source, path, |program| {
26 let mut sources = Vec::new();
27 for stmt in &program.body {
28 if let Statement::ImportDeclaration(decl) = stmt {
29 sources.push(decl.source.value.to_string());
30 }
31 }
32 Some(sources)
33 })
34 .unwrap_or_default()
35}
36
37#[must_use]
39pub fn extract_config_string_array(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
40 extract_from_source(source, path, |program| {
41 let obj = find_config_object(program)?;
42 get_nested_string_array_from_object(obj, prop_path)
43 })
44 .unwrap_or_default()
45}
46
47#[must_use]
49pub fn extract_config_string(source: &str, path: &Path, prop_path: &[&str]) -> Option<String> {
50 extract_from_source(source, path, |program| {
51 let obj = find_config_object(program)?;
52 get_nested_string_from_object(obj, prop_path)
53 })
54}
55
56#[must_use]
63pub fn extract_config_property_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
64 extract_from_source(source, path, |program| {
65 let obj = find_config_object(program)?;
66 let mut values = Vec::new();
67 if let Some(prop) = find_property(obj, key) {
68 collect_all_string_values(&prop.value, &mut values);
69 }
70 Some(values)
71 })
72 .unwrap_or_default()
73}
74
75#[must_use]
82pub fn extract_config_shallow_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
83 extract_from_source(source, path, |program| {
84 let obj = find_config_object(program)?;
85 let prop = find_property(obj, key)?;
86 Some(collect_shallow_string_values(&prop.value))
87 })
88 .unwrap_or_default()
89}
90
91#[must_use]
97pub fn extract_config_nested_shallow_strings(
98 source: &str,
99 path: &Path,
100 outer_path: &[&str],
101 key: &str,
102) -> Vec<String> {
103 extract_from_source(source, path, |program| {
104 let obj = find_config_object(program)?;
105 let nested = get_nested_expression(obj, outer_path)?;
106 if let Expression::ObjectExpression(nested_obj) = nested {
107 let prop = find_property(nested_obj, key)?;
108 Some(collect_shallow_string_values(&prop.value))
109 } else {
110 None
111 }
112 })
113 .unwrap_or_default()
114}
115
116pub fn find_config_object_pub<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
118 find_config_object(program)
119}
120
121#[must_use]
126pub fn extract_config_object_keys(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
127 extract_from_source(source, path, |program| {
128 let obj = find_config_object(program)?;
129 get_nested_object_keys(obj, prop_path)
130 })
131 .unwrap_or_default()
132}
133
134#[must_use]
141pub fn extract_config_string_or_array(
142 source: &str,
143 path: &Path,
144 prop_path: &[&str],
145) -> Vec<String> {
146 extract_from_source(source, path, |program| {
147 let obj = find_config_object(program)?;
148 get_nested_string_or_array(obj, prop_path)
149 })
150 .unwrap_or_default()
151}
152
153#[must_use]
160pub fn extract_config_array_nested_string_or_array(
161 source: &str,
162 path: &Path,
163 array_path: &[&str],
164 inner_path: &[&str],
165) -> Vec<String> {
166 extract_from_source(source, path, |program| {
167 let obj = find_config_object(program)?;
168 let array_expr = get_nested_expression(obj, array_path)?;
169 let Expression::ArrayExpression(arr) = array_expr else {
170 return None;
171 };
172 let mut results = Vec::new();
173 for element in &arr.elements {
174 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
175 && let Some(values) = get_nested_string_or_array(element_obj, inner_path)
176 {
177 results.extend(values);
178 }
179 }
180 if results.is_empty() {
181 None
182 } else {
183 Some(results)
184 }
185 })
186 .unwrap_or_default()
187}
188
189#[must_use]
196pub fn extract_config_object_nested_string_or_array(
197 source: &str,
198 path: &Path,
199 object_path: &[&str],
200 inner_path: &[&str],
201) -> Vec<String> {
202 extract_config_object_nested(source, path, object_path, |value_obj| {
203 get_nested_string_or_array(value_obj, inner_path)
204 })
205}
206
207#[must_use]
212pub fn extract_config_object_nested_strings(
213 source: &str,
214 path: &Path,
215 object_path: &[&str],
216 inner_path: &[&str],
217) -> Vec<String> {
218 extract_config_object_nested(source, path, object_path, |value_obj| {
219 get_nested_string_from_object(value_obj, inner_path).map(|s| vec![s])
220 })
221}
222
223fn extract_config_object_nested(
228 source: &str,
229 path: &Path,
230 object_path: &[&str],
231 extract_fn: impl Fn(&ObjectExpression<'_>) -> Option<Vec<String>>,
232) -> Vec<String> {
233 extract_from_source(source, path, |program| {
234 let obj = find_config_object(program)?;
235 let obj_expr = get_nested_expression(obj, object_path)?;
236 let Expression::ObjectExpression(target_obj) = obj_expr else {
237 return None;
238 };
239 let mut results = Vec::new();
240 for prop in &target_obj.properties {
241 if let ObjectPropertyKind::ObjectProperty(p) = prop
242 && let Expression::ObjectExpression(value_obj) = &p.value
243 && let Some(values) = extract_fn(value_obj)
244 {
245 results.extend(values);
246 }
247 }
248 if results.is_empty() {
249 None
250 } else {
251 Some(results)
252 }
253 })
254 .unwrap_or_default()
255}
256
257#[must_use]
263pub fn extract_config_require_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
264 extract_from_source(source, path, |program| {
265 let obj = find_config_object(program)?;
266 let prop = find_property(obj, key)?;
267 Some(collect_require_sources(&prop.value))
268 })
269 .unwrap_or_default()
270}
271
272#[must_use]
279pub fn extract_config_aliases(
280 source: &str,
281 path: &Path,
282 prop_path: &[&str],
283) -> Vec<(String, String)> {
284 extract_from_source(source, path, |program| {
285 let obj = find_config_object(program)?;
286 let expr = get_nested_expression(obj, prop_path)?;
287 let aliases = expression_to_alias_pairs(expr);
288 (!aliases.is_empty()).then_some(aliases)
289 })
290 .unwrap_or_default()
291}
292
293#[must_use]
299pub fn extract_config_array_object_strings(
300 source: &str,
301 path: &Path,
302 array_path: &[&str],
303 key: &str,
304) -> Vec<String> {
305 extract_from_source(source, path, |program| {
306 let obj = find_config_object(program)?;
307 let array_expr = get_nested_expression(obj, array_path)?;
308 let Expression::ArrayExpression(arr) = array_expr else {
309 return None;
310 };
311
312 let mut results = Vec::new();
313 for element in &arr.elements {
314 let Some(expr) = element.as_expression() else {
315 continue;
316 };
317 match expr {
318 Expression::ObjectExpression(item) => {
319 if let Some(prop) = find_property(item, key)
320 && let Some(value) = expression_to_path_string(&prop.value)
321 {
322 results.push(value);
323 }
324 }
325 _ => {
326 if let Some(value) = expression_to_path_string(expr) {
327 results.push(value);
328 }
329 }
330 }
331 }
332
333 (!results.is_empty()).then_some(results)
334 })
335 .unwrap_or_default()
336}
337
338#[must_use]
345pub fn extract_config_plugin_option_string(
346 source: &str,
347 path: &Path,
348 plugins_path: &[&str],
349 plugin_name: &str,
350 option_key: &str,
351) -> Option<String> {
352 extract_from_source(source, path, |program| {
353 let obj = find_config_object(program)?;
354 let plugins_expr = get_nested_expression(obj, plugins_path)?;
355 let Expression::ArrayExpression(plugins) = plugins_expr else {
356 return None;
357 };
358
359 for entry in &plugins.elements {
360 let Some(Expression::ArrayExpression(tuple)) = entry.as_expression() else {
361 continue;
362 };
363 let Some(plugin_expr) = tuple
364 .elements
365 .first()
366 .and_then(ArrayExpressionElement::as_expression)
367 else {
368 continue;
369 };
370 if expression_to_string(plugin_expr).as_deref() != Some(plugin_name) {
371 continue;
372 }
373
374 let Some(options_expr) = tuple
375 .elements
376 .get(1)
377 .and_then(ArrayExpressionElement::as_expression)
378 else {
379 continue;
380 };
381 let Expression::ObjectExpression(options_obj) = options_expr else {
382 continue;
383 };
384 let option = find_property(options_obj, option_key)?;
385 return expression_to_path_string(&option.value);
386 }
387
388 None
389 })
390}
391
392#[must_use]
394pub fn extract_config_plugin_option_string_from_paths(
395 source: &str,
396 path: &Path,
397 plugin_paths: &[&[&str]],
398 plugin_name: &str,
399 option_key: &str,
400) -> Option<String> {
401 plugin_paths.iter().find_map(|plugins_path| {
402 extract_config_plugin_option_string(source, path, plugins_path, plugin_name, option_key)
403 })
404}
405
406#[must_use]
411pub fn normalize_config_path(raw: &str, config_path: &Path, root: &Path) -> Option<String> {
412 if raw.is_empty() {
413 return None;
414 }
415
416 let candidate = if let Some(stripped) = raw.strip_prefix('/') {
417 lexical_normalize(&root.join(stripped))
418 } else {
419 let path = Path::new(raw);
420 if path.is_absolute() {
421 lexical_normalize(path)
422 } else {
423 let base = config_path.parent().unwrap_or(root);
424 lexical_normalize(&base.join(path))
425 }
426 };
427
428 let relative = candidate.strip_prefix(root).ok()?;
429 let normalized = relative.to_string_lossy().replace('\\', "/");
430 (!normalized.is_empty()).then_some(normalized)
431}
432
433fn extract_from_source<T>(
442 source: &str,
443 path: &Path,
444 extractor: impl FnOnce(&Program) -> Option<T>,
445) -> Option<T> {
446 let source_type = SourceType::from_path(path).unwrap_or_default();
447 let alloc = Allocator::default();
448
449 let is_json = path
452 .extension()
453 .is_some_and(|ext| ext == "json" || ext == "jsonc");
454 if is_json {
455 let wrapped = format!("({source})");
456 let parsed = Parser::new(&alloc, &wrapped, SourceType::mjs()).parse();
457 return extractor(&parsed.program);
458 }
459
460 let parsed = Parser::new(&alloc, source, source_type).parse();
461 extractor(&parsed.program)
462}
463
464fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
476 for stmt in &program.body {
477 match stmt {
478 Statement::ExportDefaultDeclaration(decl) => {
480 let expr: Option<&Expression> = match &decl.declaration {
482 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
483 return Some(obj);
484 }
485 _ => decl.declaration.as_expression(),
486 };
487 if let Some(expr) = expr {
488 if let Some(obj) = extract_object_from_expression(expr) {
490 return Some(obj);
491 }
492 if let Some(name) = unwrap_to_identifier_name(expr) {
495 return find_variable_init_object(program, name);
496 }
497 }
498 }
499 Statement::ExpressionStatement(expr_stmt) => {
501 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
502 && is_module_exports_target(&assign.left)
503 {
504 return extract_object_from_expression(&assign.right);
505 }
506 }
507 _ => {}
508 }
509 }
510
511 if program.body.len() == 1
514 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
515 {
516 match &expr_stmt.expression {
517 Expression::ObjectExpression(obj) => return Some(obj),
518 Expression::ParenthesizedExpression(paren) => {
519 if let Expression::ObjectExpression(obj) = &paren.expression {
520 return Some(obj);
521 }
522 }
523 _ => {}
524 }
525 }
526
527 None
528}
529
530fn extract_object_from_expression<'a>(
532 expr: &'a Expression<'a>,
533) -> Option<&'a ObjectExpression<'a>> {
534 match expr {
535 Expression::ObjectExpression(obj) => Some(obj),
537 Expression::CallExpression(call) => {
539 for arg in &call.arguments {
541 match arg {
542 Argument::ObjectExpression(obj) => return Some(obj),
543 Argument::ArrowFunctionExpression(arrow) => {
545 if arrow.expression
546 && !arrow.body.statements.is_empty()
547 && let Statement::ExpressionStatement(expr_stmt) =
548 &arrow.body.statements[0]
549 {
550 return extract_object_from_expression(&expr_stmt.expression);
551 }
552 }
553 _ => {}
554 }
555 }
556 None
557 }
558 Expression::ParenthesizedExpression(paren) => {
560 extract_object_from_expression(&paren.expression)
561 }
562 Expression::TSSatisfiesExpression(ts_sat) => {
564 extract_object_from_expression(&ts_sat.expression)
565 }
566 Expression::TSAsExpression(ts_as) => extract_object_from_expression(&ts_as.expression),
567 _ => None,
568 }
569}
570
571fn is_module_exports_target(target: &AssignmentTarget) -> bool {
573 if let AssignmentTarget::StaticMemberExpression(member) = target
574 && let Expression::Identifier(obj) = &member.object
575 {
576 return obj.name == "module" && member.property.name == "exports";
577 }
578 false
579}
580
581fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
585 match expr {
586 Expression::Identifier(id) => Some(&id.name),
587 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
588 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
589 _ => None,
590 }
591}
592
593fn find_variable_init_object<'a>(
598 program: &'a Program,
599 name: &str,
600) -> Option<&'a ObjectExpression<'a>> {
601 for stmt in &program.body {
602 if let Statement::VariableDeclaration(decl) = stmt {
603 for declarator in &decl.declarations {
604 if let BindingPattern::BindingIdentifier(id) = &declarator.id
605 && id.name == name
606 && let Some(init) = &declarator.init
607 {
608 return extract_object_from_expression(init);
609 }
610 }
611 }
612 }
613 None
614}
615
616fn find_property<'a>(obj: &'a ObjectExpression<'a>, key: &str) -> Option<&'a ObjectProperty<'a>> {
618 for prop in &obj.properties {
619 if let ObjectPropertyKind::ObjectProperty(p) = prop
620 && property_key_matches(&p.key, key)
621 {
622 return Some(p);
623 }
624 }
625 None
626}
627
628fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
630 match key {
631 PropertyKey::StaticIdentifier(id) => id.name == name,
632 PropertyKey::StringLiteral(s) => s.value == name,
633 _ => false,
634 }
635}
636
637fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
639 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
640}
641
642fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
644 find_property(obj, key)
645 .map(|p| expression_to_string_array(&p.value))
646 .unwrap_or_default()
647}
648
649fn get_nested_string_array_from_object(
651 obj: &ObjectExpression,
652 path: &[&str],
653) -> Option<Vec<String>> {
654 if path.is_empty() {
655 return None;
656 }
657 if path.len() == 1 {
658 return Some(get_object_string_array_property(obj, path[0]));
659 }
660 let prop = find_property(obj, path[0])?;
662 if let Expression::ObjectExpression(nested) = &prop.value {
663 get_nested_string_array_from_object(nested, &path[1..])
664 } else {
665 None
666 }
667}
668
669fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
671 if path.is_empty() {
672 return None;
673 }
674 if path.len() == 1 {
675 return get_object_string_property(obj, path[0]);
676 }
677 let prop = find_property(obj, path[0])?;
678 if let Expression::ObjectExpression(nested) = &prop.value {
679 get_nested_string_from_object(nested, &path[1..])
680 } else {
681 None
682 }
683}
684
685fn expression_to_string(expr: &Expression) -> Option<String> {
687 match expr {
688 Expression::StringLiteral(s) => Some(s.value.to_string()),
689 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
690 t.quasis.first().map(|q| q.value.raw.to_string())
692 }
693 _ => None,
694 }
695}
696
697fn expression_to_path_string(expr: &Expression) -> Option<String> {
699 match expr {
700 Expression::ParenthesizedExpression(paren) => expression_to_path_string(&paren.expression),
701 Expression::TSAsExpression(ts_as) => expression_to_path_string(&ts_as.expression),
702 Expression::TSSatisfiesExpression(ts_sat) => expression_to_path_string(&ts_sat.expression),
703 Expression::CallExpression(call) => call_expression_to_path_string(call),
704 Expression::NewExpression(new_expr) => new_expression_to_path_string(new_expr),
705 _ => expression_to_string(expr),
706 }
707}
708
709fn call_expression_to_path_string(call: &CallExpression) -> Option<String> {
710 if matches!(&call.callee, Expression::Identifier(id) if id.name == "fileURLToPath") {
711 return call
712 .arguments
713 .first()
714 .and_then(Argument::as_expression)
715 .and_then(expression_to_path_string);
716 }
717
718 let callee_name = match &call.callee {
719 Expression::Identifier(id) => Some(id.name.as_str()),
720 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
721 _ => None,
722 }?;
723
724 if !matches!(callee_name, "resolve" | "join") {
725 return None;
726 }
727
728 let mut segments = Vec::new();
729 for (index, arg) in call.arguments.iter().enumerate() {
730 let expr = arg.as_expression()?;
731
732 if matches!(expr, Expression::Identifier(id) if id.name == "__dirname") {
733 if index == 0 {
734 continue;
735 }
736 return None;
737 }
738
739 segments.push(expression_to_string(expr)?);
740 }
741
742 (!segments.is_empty()).then(|| join_path_segments(&segments))
743}
744
745fn new_expression_to_path_string(new_expr: &NewExpression) -> Option<String> {
746 if !matches!(&new_expr.callee, Expression::Identifier(id) if id.name == "URL") {
747 return None;
748 }
749
750 let source = new_expr
751 .arguments
752 .first()
753 .and_then(Argument::as_expression)
754 .and_then(expression_to_string)?;
755
756 let base = new_expr
757 .arguments
758 .get(1)
759 .and_then(Argument::as_expression)?;
760 is_import_meta_url_expression(base).then_some(source)
761}
762
763fn is_import_meta_url_expression(expr: &Expression) -> bool {
764 if let Expression::StaticMemberExpression(member) = expr {
765 member.property.name == "url" && matches!(member.object, Expression::MetaProperty(_))
766 } else {
767 false
768 }
769}
770
771fn join_path_segments(segments: &[String]) -> String {
772 let mut joined = PathBuf::new();
773 for segment in segments {
774 joined.push(segment);
775 }
776 joined.to_string_lossy().replace('\\', "/")
777}
778
779fn expression_to_alias_pairs(expr: &Expression) -> Vec<(String, String)> {
780 match expr {
781 Expression::ObjectExpression(obj) => obj
782 .properties
783 .iter()
784 .filter_map(|prop| {
785 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
786 return None;
787 };
788 let find = property_key_to_string(&prop.key)?;
789 let replacement = expression_to_path_string(&prop.value)?;
790 Some((find, replacement))
791 })
792 .collect(),
793 Expression::ArrayExpression(arr) => arr
794 .elements
795 .iter()
796 .filter_map(|element| {
797 let Expression::ObjectExpression(obj) = element.as_expression()? else {
798 return None;
799 };
800 let find = find_property(obj, "find")
801 .and_then(|prop| expression_to_string(&prop.value))?;
802 let replacement = find_property(obj, "replacement")
803 .and_then(|prop| expression_to_path_string(&prop.value))?;
804 Some((find, replacement))
805 })
806 .collect(),
807 _ => Vec::new(),
808 }
809}
810
811fn lexical_normalize(path: &Path) -> PathBuf {
812 let mut normalized = PathBuf::new();
813
814 for component in path.components() {
815 match component {
816 std::path::Component::CurDir => {}
817 std::path::Component::ParentDir => {
818 normalized.pop();
819 }
820 _ => normalized.push(component.as_os_str()),
821 }
822 }
823
824 normalized
825}
826
827fn expression_to_string_array(expr: &Expression) -> Vec<String> {
829 match expr {
830 Expression::ArrayExpression(arr) => arr
831 .elements
832 .iter()
833 .filter_map(|el| match el {
834 ArrayExpressionElement::SpreadElement(_) => None,
835 _ => el.as_expression().and_then(expression_to_string),
836 })
837 .collect(),
838 _ => vec![],
839 }
840}
841
842fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
847 let mut values = Vec::new();
848 match expr {
849 Expression::StringLiteral(s) => {
850 values.push(s.value.to_string());
851 }
852 Expression::ArrayExpression(arr) => {
853 for el in &arr.elements {
854 if let Some(inner) = el.as_expression() {
855 match inner {
856 Expression::StringLiteral(s) => {
857 values.push(s.value.to_string());
858 }
859 Expression::ArrayExpression(sub_arr) => {
861 if let Some(first) = sub_arr.elements.first()
862 && let Some(first_expr) = first.as_expression()
863 && let Some(s) = expression_to_string(first_expr)
864 {
865 values.push(s);
866 }
867 }
868 _ => {}
869 }
870 }
871 }
872 }
873 Expression::ObjectExpression(obj) => {
875 for prop in &obj.properties {
876 if let ObjectPropertyKind::ObjectProperty(p) = prop {
877 match &p.value {
878 Expression::StringLiteral(s) => {
879 values.push(s.value.to_string());
880 }
881 Expression::ArrayExpression(sub_arr) => {
883 if let Some(first) = sub_arr.elements.first()
884 && let Some(first_expr) = first.as_expression()
885 && let Some(s) = expression_to_string(first_expr)
886 {
887 values.push(s);
888 }
889 }
890 _ => {}
891 }
892 }
893 }
894 }
895 _ => {}
896 }
897 values
898}
899
900fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
902 match expr {
903 Expression::StringLiteral(s) => {
904 values.push(s.value.to_string());
905 }
906 Expression::ArrayExpression(arr) => {
907 for el in &arr.elements {
908 if let Some(expr) = el.as_expression() {
909 collect_all_string_values(expr, values);
910 }
911 }
912 }
913 Expression::ObjectExpression(obj) => {
914 for prop in &obj.properties {
915 if let ObjectPropertyKind::ObjectProperty(p) = prop {
916 collect_all_string_values(&p.value, values);
917 }
918 }
919 }
920 _ => {}
921 }
922}
923
924fn property_key_to_string(key: &PropertyKey) -> Option<String> {
926 match key {
927 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
928 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
929 _ => None,
930 }
931}
932
933fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
935 if path.is_empty() {
936 return None;
937 }
938 let prop = find_property(obj, path[0])?;
939 if path.len() == 1 {
940 if let Expression::ObjectExpression(nested) = &prop.value {
941 let keys = nested
942 .properties
943 .iter()
944 .filter_map(|p| {
945 if let ObjectPropertyKind::ObjectProperty(p) = p {
946 property_key_to_string(&p.key)
947 } else {
948 None
949 }
950 })
951 .collect();
952 return Some(keys);
953 }
954 return None;
955 }
956 if let Expression::ObjectExpression(nested) = &prop.value {
957 get_nested_object_keys(nested, &path[1..])
958 } else {
959 None
960 }
961}
962
963fn get_nested_expression<'a>(
965 obj: &'a ObjectExpression<'a>,
966 path: &[&str],
967) -> Option<&'a Expression<'a>> {
968 if path.is_empty() {
969 return None;
970 }
971 let prop = find_property(obj, path[0])?;
972 if path.len() == 1 {
973 return Some(&prop.value);
974 }
975 if let Expression::ObjectExpression(nested) = &prop.value {
976 get_nested_expression(nested, &path[1..])
977 } else {
978 None
979 }
980}
981
982fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
984 if path.is_empty() {
985 return None;
986 }
987 if path.len() == 1 {
988 let prop = find_property(obj, path[0])?;
989 return Some(expression_to_string_or_array(&prop.value));
990 }
991 let prop = find_property(obj, path[0])?;
992 if let Expression::ObjectExpression(nested) = &prop.value {
993 get_nested_string_or_array(nested, &path[1..])
994 } else {
995 None
996 }
997}
998
999fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
1001 match expr {
1002 Expression::StringLiteral(s) => vec![s.value.to_string()],
1003 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
1004 .quasis
1005 .first()
1006 .map(|q| vec![q.value.raw.to_string()])
1007 .unwrap_or_default(),
1008 Expression::ArrayExpression(arr) => arr
1009 .elements
1010 .iter()
1011 .filter_map(|el| el.as_expression().and_then(expression_to_string))
1012 .collect(),
1013 Expression::ObjectExpression(obj) => obj
1014 .properties
1015 .iter()
1016 .filter_map(|p| {
1017 if let ObjectPropertyKind::ObjectProperty(p) = p {
1018 expression_to_string(&p.value)
1019 } else {
1020 None
1021 }
1022 })
1023 .collect(),
1024 _ => vec![],
1025 }
1026}
1027
1028fn collect_require_sources(expr: &Expression) -> Vec<String> {
1030 let mut sources = Vec::new();
1031 match expr {
1032 Expression::CallExpression(call) if is_require_call(call) => {
1033 if let Some(s) = get_require_source(call) {
1034 sources.push(s);
1035 }
1036 }
1037 Expression::ArrayExpression(arr) => {
1038 for el in &arr.elements {
1039 if let Some(inner) = el.as_expression() {
1040 match inner {
1041 Expression::CallExpression(call) if is_require_call(call) => {
1042 if let Some(s) = get_require_source(call) {
1043 sources.push(s);
1044 }
1045 }
1046 Expression::ArrayExpression(sub_arr) => {
1048 if let Some(first) = sub_arr.elements.first()
1049 && let Some(Expression::CallExpression(call)) =
1050 first.as_expression()
1051 && is_require_call(call)
1052 && let Some(s) = get_require_source(call)
1053 {
1054 sources.push(s);
1055 }
1056 }
1057 _ => {}
1058 }
1059 }
1060 }
1061 }
1062 _ => {}
1063 }
1064 sources
1065}
1066
1067fn is_require_call(call: &CallExpression) -> bool {
1069 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
1070}
1071
1072fn get_require_source(call: &CallExpression) -> Option<String> {
1074 call.arguments.first().and_then(|arg| {
1075 if let Argument::StringLiteral(s) = arg {
1076 Some(s.value.to_string())
1077 } else {
1078 None
1079 }
1080 })
1081}
1082
1083#[cfg(test)]
1084mod tests {
1085 use super::*;
1086 use std::path::PathBuf;
1087
1088 fn js_path() -> PathBuf {
1089 PathBuf::from("config.js")
1090 }
1091
1092 fn ts_path() -> PathBuf {
1093 PathBuf::from("config.ts")
1094 }
1095
1096 #[test]
1097 fn extract_imports_basic() {
1098 let source = r"
1099 import foo from 'foo-pkg';
1100 import { bar } from '@scope/bar';
1101 export default {};
1102 ";
1103 let imports = extract_imports(source, &js_path());
1104 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
1105 }
1106
1107 #[test]
1108 fn extract_default_export_object_property() {
1109 let source = r#"export default { testDir: "./tests" };"#;
1110 let val = extract_config_string(source, &js_path(), &["testDir"]);
1111 assert_eq!(val, Some("./tests".to_string()));
1112 }
1113
1114 #[test]
1115 fn extract_define_config_property() {
1116 let source = r#"
1117 import { defineConfig } from 'vitest/config';
1118 export default defineConfig({
1119 test: {
1120 include: ["**/*.test.ts", "**/*.spec.ts"],
1121 setupFiles: ["./test/setup.ts"]
1122 }
1123 });
1124 "#;
1125 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1126 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
1127
1128 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
1129 assert_eq!(setup, vec!["./test/setup.ts"]);
1130 }
1131
1132 #[test]
1133 fn extract_module_exports_property() {
1134 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
1135 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
1136 assert_eq!(val, Some("jsdom".to_string()));
1137 }
1138
1139 #[test]
1140 fn extract_nested_string_array() {
1141 let source = r#"
1142 export default {
1143 resolve: {
1144 alias: {
1145 "@": "./src"
1146 }
1147 },
1148 test: {
1149 include: ["src/**/*.test.ts"]
1150 }
1151 };
1152 "#;
1153 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
1154 assert_eq!(include, vec!["src/**/*.test.ts"]);
1155 }
1156
1157 #[test]
1158 fn extract_addons_array() {
1159 let source = r#"
1160 export default {
1161 addons: [
1162 "@storybook/addon-a11y",
1163 "@storybook/addon-docs",
1164 "@storybook/addon-links"
1165 ]
1166 };
1167 "#;
1168 let addons = extract_config_property_strings(source, &ts_path(), "addons");
1169 assert_eq!(
1170 addons,
1171 vec![
1172 "@storybook/addon-a11y",
1173 "@storybook/addon-docs",
1174 "@storybook/addon-links"
1175 ]
1176 );
1177 }
1178
1179 #[test]
1180 fn handle_empty_config() {
1181 let source = "";
1182 let result = extract_config_string(source, &js_path(), &["key"]);
1183 assert_eq!(result, None);
1184 }
1185
1186 #[test]
1189 fn object_keys_postcss_plugins() {
1190 let source = r"
1191 module.exports = {
1192 plugins: {
1193 autoprefixer: {},
1194 tailwindcss: {},
1195 'postcss-import': {}
1196 }
1197 };
1198 ";
1199 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1200 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
1201 }
1202
1203 #[test]
1204 fn object_keys_nested_path() {
1205 let source = r"
1206 export default {
1207 build: {
1208 plugins: {
1209 minify: {},
1210 compress: {}
1211 }
1212 }
1213 };
1214 ";
1215 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
1216 assert_eq!(keys, vec!["minify", "compress"]);
1217 }
1218
1219 #[test]
1220 fn object_keys_empty_object() {
1221 let source = r"export default { plugins: {} };";
1222 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1223 assert!(keys.is_empty());
1224 }
1225
1226 #[test]
1227 fn object_keys_non_object_returns_empty() {
1228 let source = r#"export default { plugins: ["a", "b"] };"#;
1229 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1230 assert!(keys.is_empty());
1231 }
1232
1233 #[test]
1236 fn string_or_array_single_string() {
1237 let source = r#"export default { entry: "./src/index.js" };"#;
1238 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1239 assert_eq!(result, vec!["./src/index.js"]);
1240 }
1241
1242 #[test]
1243 fn string_or_array_array() {
1244 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
1245 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1246 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
1247 }
1248
1249 #[test]
1250 fn string_or_array_object_values() {
1251 let source =
1252 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
1253 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1254 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
1255 }
1256
1257 #[test]
1258 fn string_or_array_nested_path() {
1259 let source = r#"
1260 export default {
1261 build: {
1262 rollupOptions: {
1263 input: ["./index.html", "./about.html"]
1264 }
1265 }
1266 };
1267 "#;
1268 let result = extract_config_string_or_array(
1269 source,
1270 &js_path(),
1271 &["build", "rollupOptions", "input"],
1272 );
1273 assert_eq!(result, vec!["./index.html", "./about.html"]);
1274 }
1275
1276 #[test]
1277 fn string_or_array_template_literal() {
1278 let source = r"export default { entry: `./src/index.js` };";
1279 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1280 assert_eq!(result, vec!["./src/index.js"]);
1281 }
1282
1283 #[test]
1286 fn require_strings_array() {
1287 let source = r"
1288 module.exports = {
1289 plugins: [
1290 require('autoprefixer'),
1291 require('postcss-import')
1292 ]
1293 };
1294 ";
1295 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1296 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
1297 }
1298
1299 #[test]
1300 fn require_strings_with_tuples() {
1301 let source = r"
1302 module.exports = {
1303 plugins: [
1304 require('autoprefixer'),
1305 [require('postcss-preset-env'), { stage: 3 }]
1306 ]
1307 };
1308 ";
1309 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1310 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
1311 }
1312
1313 #[test]
1314 fn require_strings_empty_array() {
1315 let source = r"module.exports = { plugins: [] };";
1316 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1317 assert!(deps.is_empty());
1318 }
1319
1320 #[test]
1321 fn require_strings_no_require_calls() {
1322 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
1323 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1324 assert!(deps.is_empty());
1325 }
1326
1327 #[test]
1328 fn extract_aliases_from_object_with_file_url_to_path() {
1329 let source = r#"
1330 import { defineConfig } from 'vite';
1331 import { fileURLToPath, URL } from 'node:url';
1332
1333 export default defineConfig({
1334 resolve: {
1335 alias: {
1336 "@": fileURLToPath(new URL("./src", import.meta.url))
1337 }
1338 }
1339 });
1340 "#;
1341
1342 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
1343 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
1344 }
1345
1346 #[test]
1347 fn extract_aliases_from_array_form() {
1348 let source = r#"
1349 export default {
1350 resolve: {
1351 alias: [
1352 { find: "@", replacement: "./src" },
1353 { find: "$utils", replacement: "src/lib/utils" }
1354 ]
1355 }
1356 };
1357 "#;
1358
1359 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
1360 assert_eq!(
1361 aliases,
1362 vec![
1363 ("@".to_string(), "./src".to_string()),
1364 ("$utils".to_string(), "src/lib/utils".to_string())
1365 ]
1366 );
1367 }
1368
1369 #[test]
1370 fn extract_array_object_strings_mixed_forms() {
1371 let source = r#"
1372 export default {
1373 components: [
1374 "~/components",
1375 { path: "@/feature-components" }
1376 ]
1377 };
1378 "#;
1379
1380 let values =
1381 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
1382 assert_eq!(
1383 values,
1384 vec![
1385 "~/components".to_string(),
1386 "@/feature-components".to_string()
1387 ]
1388 );
1389 }
1390
1391 #[test]
1392 fn extract_config_plugin_option_string_from_json() {
1393 let source = r#"{
1394 "expo": {
1395 "plugins": [
1396 ["expo-router", { "root": "src/app" }]
1397 ]
1398 }
1399 }"#;
1400
1401 let value = extract_config_plugin_option_string(
1402 source,
1403 &json_path(),
1404 &["expo", "plugins"],
1405 "expo-router",
1406 "root",
1407 );
1408
1409 assert_eq!(value, Some("src/app".to_string()));
1410 }
1411
1412 #[test]
1413 fn extract_config_plugin_option_string_from_top_level_plugins() {
1414 let source = r#"{
1415 "plugins": [
1416 ["expo-router", { "root": "./src/routes" }]
1417 ]
1418 }"#;
1419
1420 let value = extract_config_plugin_option_string_from_paths(
1421 source,
1422 &json_path(),
1423 &[&["plugins"], &["expo", "plugins"]],
1424 "expo-router",
1425 "root",
1426 );
1427
1428 assert_eq!(value, Some("./src/routes".to_string()));
1429 }
1430
1431 #[test]
1432 fn extract_config_plugin_option_string_from_ts_config() {
1433 let source = r"
1434 export default {
1435 expo: {
1436 plugins: [
1437 ['expo-router', { root: './src/app' }]
1438 ]
1439 }
1440 };
1441 ";
1442
1443 let value = extract_config_plugin_option_string(
1444 source,
1445 &ts_path(),
1446 &["expo", "plugins"],
1447 "expo-router",
1448 "root",
1449 );
1450
1451 assert_eq!(value, Some("./src/app".to_string()));
1452 }
1453
1454 #[test]
1455 fn extract_config_plugin_option_string_returns_none_when_plugin_missing() {
1456 let source = r#"{
1457 "expo": {
1458 "plugins": [
1459 ["expo-font", {}]
1460 ]
1461 }
1462 }"#;
1463
1464 let value = extract_config_plugin_option_string(
1465 source,
1466 &json_path(),
1467 &["expo", "plugins"],
1468 "expo-router",
1469 "root",
1470 );
1471
1472 assert_eq!(value, None);
1473 }
1474
1475 #[test]
1476 fn normalize_config_path_relative_to_root() {
1477 let config_path = PathBuf::from("/project/vite.config.ts");
1478 let root = PathBuf::from("/project");
1479
1480 assert_eq!(
1481 normalize_config_path("./src/lib", &config_path, &root),
1482 Some("src/lib".to_string())
1483 );
1484 assert_eq!(
1485 normalize_config_path("/src/lib", &config_path, &root),
1486 Some("src/lib".to_string())
1487 );
1488 }
1489
1490 #[test]
1493 fn json_wrapped_in_parens_string() {
1494 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
1495 let val = extract_config_string(source, &js_path(), &["extends"]);
1496 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
1497 }
1498
1499 #[test]
1500 fn json_wrapped_in_parens_nested_array() {
1501 let source =
1502 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
1503 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
1504 assert_eq!(types, vec!["node", "jest"]);
1505
1506 let include = extract_config_string_array(source, &js_path(), &["include"]);
1507 assert_eq!(include, vec!["src/**/*"]);
1508 }
1509
1510 #[test]
1511 fn json_wrapped_in_parens_object_keys() {
1512 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
1513 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1514 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
1515 }
1516
1517 fn json_path() -> PathBuf {
1520 PathBuf::from("config.json")
1521 }
1522
1523 #[test]
1524 fn json_file_parsed_correctly() {
1525 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
1526 let val = extract_config_string(source, &json_path(), &["key"]);
1527 assert_eq!(val, Some("value".to_string()));
1528
1529 let list = extract_config_string_array(source, &json_path(), &["list"]);
1530 assert_eq!(list, vec!["a", "b"]);
1531 }
1532
1533 #[test]
1534 fn jsonc_file_parsed_correctly() {
1535 let source = r#"{"key": "value"}"#;
1536 let path = PathBuf::from("tsconfig.jsonc");
1537 let val = extract_config_string(source, &path, &["key"]);
1538 assert_eq!(val, Some("value".to_string()));
1539 }
1540
1541 #[test]
1544 fn extract_define_config_arrow_function() {
1545 let source = r#"
1546 import { defineConfig } from 'vite';
1547 export default defineConfig(() => ({
1548 test: {
1549 include: ["**/*.test.ts"]
1550 }
1551 }));
1552 "#;
1553 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1554 assert_eq!(include, vec!["**/*.test.ts"]);
1555 }
1556
1557 #[test]
1560 fn module_exports_nested_string() {
1561 let source = r#"
1562 module.exports = {
1563 resolve: {
1564 alias: {
1565 "@": "./src"
1566 }
1567 }
1568 };
1569 "#;
1570 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
1571 assert_eq!(val, Some("./src".to_string()));
1572 }
1573
1574 #[test]
1577 fn property_strings_nested_objects() {
1578 let source = r#"
1579 export default {
1580 plugins: {
1581 group1: { a: "val-a" },
1582 group2: { b: "val-b" }
1583 }
1584 };
1585 "#;
1586 let values = extract_config_property_strings(source, &js_path(), "plugins");
1587 assert!(values.contains(&"val-a".to_string()));
1588 assert!(values.contains(&"val-b".to_string()));
1589 }
1590
1591 #[test]
1592 fn property_strings_missing_key_returns_empty() {
1593 let source = r#"export default { other: "value" };"#;
1594 let values = extract_config_property_strings(source, &js_path(), "missing");
1595 assert!(values.is_empty());
1596 }
1597
1598 #[test]
1601 fn shallow_strings_tuple_array() {
1602 let source = r#"
1603 module.exports = {
1604 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
1605 };
1606 "#;
1607 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
1608 assert_eq!(values, vec!["default", "jest-junit"]);
1609 assert!(!values.contains(&"reports".to_string()));
1611 }
1612
1613 #[test]
1614 fn shallow_strings_single_string() {
1615 let source = r#"export default { preset: "ts-jest" };"#;
1616 let values = extract_config_shallow_strings(source, &js_path(), "preset");
1617 assert_eq!(values, vec!["ts-jest"]);
1618 }
1619
1620 #[test]
1621 fn shallow_strings_missing_key() {
1622 let source = r#"export default { other: "val" };"#;
1623 let values = extract_config_shallow_strings(source, &js_path(), "missing");
1624 assert!(values.is_empty());
1625 }
1626
1627 #[test]
1630 fn nested_shallow_strings_vitest_reporters() {
1631 let source = r#"
1632 export default {
1633 test: {
1634 reporters: ["default", "vitest-sonar-reporter"]
1635 }
1636 };
1637 "#;
1638 let values =
1639 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1640 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
1641 }
1642
1643 #[test]
1644 fn nested_shallow_strings_tuple_format() {
1645 let source = r#"
1646 export default {
1647 test: {
1648 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
1649 }
1650 };
1651 "#;
1652 let values =
1653 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1654 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
1655 }
1656
1657 #[test]
1658 fn nested_shallow_strings_missing_outer() {
1659 let source = r"export default { other: {} };";
1660 let values =
1661 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1662 assert!(values.is_empty());
1663 }
1664
1665 #[test]
1666 fn nested_shallow_strings_missing_inner() {
1667 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
1668 let values =
1669 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1670 assert!(values.is_empty());
1671 }
1672
1673 #[test]
1676 fn string_or_array_missing_path() {
1677 let source = r"export default {};";
1678 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1679 assert!(result.is_empty());
1680 }
1681
1682 #[test]
1683 fn string_or_array_non_string_values() {
1684 let source = r"export default { entry: [42, true] };";
1686 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1687 assert!(result.is_empty());
1688 }
1689
1690 #[test]
1693 fn array_nested_extraction() {
1694 let source = r#"
1695 export default defineConfig({
1696 test: {
1697 projects: [
1698 {
1699 test: {
1700 setupFiles: ["./test/setup-a.ts"]
1701 }
1702 },
1703 {
1704 test: {
1705 setupFiles: "./test/setup-b.ts"
1706 }
1707 }
1708 ]
1709 }
1710 });
1711 "#;
1712 let results = extract_config_array_nested_string_or_array(
1713 source,
1714 &ts_path(),
1715 &["test", "projects"],
1716 &["test", "setupFiles"],
1717 );
1718 assert!(results.contains(&"./test/setup-a.ts".to_string()));
1719 assert!(results.contains(&"./test/setup-b.ts".to_string()));
1720 }
1721
1722 #[test]
1723 fn array_nested_empty_when_no_array() {
1724 let source = r#"export default { test: { projects: "not-an-array" } };"#;
1725 let results = extract_config_array_nested_string_or_array(
1726 source,
1727 &js_path(),
1728 &["test", "projects"],
1729 &["test", "setupFiles"],
1730 );
1731 assert!(results.is_empty());
1732 }
1733
1734 #[test]
1737 fn object_nested_extraction() {
1738 let source = r#"{
1739 "projects": {
1740 "app-one": {
1741 "architect": {
1742 "build": {
1743 "options": {
1744 "styles": ["src/styles.css"]
1745 }
1746 }
1747 }
1748 }
1749 }
1750 }"#;
1751 let results = extract_config_object_nested_string_or_array(
1752 source,
1753 &json_path(),
1754 &["projects"],
1755 &["architect", "build", "options", "styles"],
1756 );
1757 assert_eq!(results, vec!["src/styles.css"]);
1758 }
1759
1760 #[test]
1763 fn object_nested_strings_extraction() {
1764 let source = r#"{
1765 "targets": {
1766 "build": {
1767 "executor": "@angular/build:application"
1768 },
1769 "test": {
1770 "executor": "@nx/vite:test"
1771 }
1772 }
1773 }"#;
1774 let results =
1775 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
1776 assert!(results.contains(&"@angular/build:application".to_string()));
1777 assert!(results.contains(&"@nx/vite:test".to_string()));
1778 }
1779
1780 #[test]
1783 fn require_strings_direct_call() {
1784 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
1785 let deps = extract_config_require_strings(source, &js_path(), "adapter");
1786 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
1787 }
1788
1789 #[test]
1790 fn require_strings_no_matching_key() {
1791 let source = r"module.exports = { other: require('something') };";
1792 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1793 assert!(deps.is_empty());
1794 }
1795
1796 #[test]
1799 fn extract_imports_no_imports() {
1800 let source = r"export default {};";
1801 let imports = extract_imports(source, &js_path());
1802 assert!(imports.is_empty());
1803 }
1804
1805 #[test]
1806 fn extract_imports_side_effect_import() {
1807 let source = r"
1808 import 'polyfill';
1809 import './local-setup';
1810 export default {};
1811 ";
1812 let imports = extract_imports(source, &js_path());
1813 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
1814 }
1815
1816 #[test]
1817 fn extract_imports_mixed_specifiers() {
1818 let source = r"
1819 import defaultExport from 'module-a';
1820 import { named } from 'module-b';
1821 import * as ns from 'module-c';
1822 export default {};
1823 ";
1824 let imports = extract_imports(source, &js_path());
1825 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
1826 }
1827
1828 #[test]
1831 fn template_literal_in_string_or_array() {
1832 let source = r"export default { entry: `./src/index.ts` };";
1833 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
1834 assert_eq!(result, vec!["./src/index.ts"]);
1835 }
1836
1837 #[test]
1838 fn template_literal_in_config_string() {
1839 let source = r"export default { testDir: `./tests` };";
1840 let val = extract_config_string(source, &js_path(), &["testDir"]);
1841 assert_eq!(val, Some("./tests".to_string()));
1842 }
1843
1844 #[test]
1847 fn nested_string_array_empty_path() {
1848 let source = r#"export default { items: ["a", "b"] };"#;
1849 let result = extract_config_string_array(source, &js_path(), &[]);
1850 assert!(result.is_empty());
1851 }
1852
1853 #[test]
1854 fn nested_string_empty_path() {
1855 let source = r#"export default { key: "val" };"#;
1856 let result = extract_config_string(source, &js_path(), &[]);
1857 assert!(result.is_none());
1858 }
1859
1860 #[test]
1861 fn object_keys_empty_path() {
1862 let source = r"export default { plugins: {} };";
1863 let result = extract_config_object_keys(source, &js_path(), &[]);
1864 assert!(result.is_empty());
1865 }
1866
1867 #[test]
1870 fn no_config_object_returns_empty() {
1871 let source = r"const x = 42;";
1873 let result = extract_config_string(source, &js_path(), &["key"]);
1874 assert!(result.is_none());
1875
1876 let arr = extract_config_string_array(source, &js_path(), &["items"]);
1877 assert!(arr.is_empty());
1878
1879 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1880 assert!(keys.is_empty());
1881 }
1882
1883 #[test]
1886 fn property_with_string_key() {
1887 let source = r#"export default { "string-key": "value" };"#;
1888 let val = extract_config_string(source, &js_path(), &["string-key"]);
1889 assert_eq!(val, Some("value".to_string()));
1890 }
1891
1892 #[test]
1893 fn nested_navigation_through_non_object() {
1894 let source = r#"export default { level1: "not-an-object" };"#;
1896 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
1897 assert!(val.is_none());
1898 }
1899
1900 #[test]
1903 fn variable_reference_untyped() {
1904 let source = r#"
1905 const config = {
1906 testDir: "./tests"
1907 };
1908 export default config;
1909 "#;
1910 let val = extract_config_string(source, &js_path(), &["testDir"]);
1911 assert_eq!(val, Some("./tests".to_string()));
1912 }
1913
1914 #[test]
1915 fn variable_reference_with_type_annotation() {
1916 let source = r#"
1917 import type { StorybookConfig } from '@storybook/react-vite';
1918 const config: StorybookConfig = {
1919 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
1920 framework: "@storybook/react-vite"
1921 };
1922 export default config;
1923 "#;
1924 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
1925 assert_eq!(
1926 addons,
1927 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
1928 );
1929
1930 let framework = extract_config_string(source, &ts_path(), &["framework"]);
1931 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
1932 }
1933
1934 #[test]
1935 fn variable_reference_with_define_config() {
1936 let source = r#"
1937 import { defineConfig } from 'vitest/config';
1938 const config = defineConfig({
1939 test: {
1940 include: ["**/*.test.ts"]
1941 }
1942 });
1943 export default config;
1944 "#;
1945 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1946 assert_eq!(include, vec!["**/*.test.ts"]);
1947 }
1948
1949 #[test]
1952 fn ts_satisfies_direct_export() {
1953 let source = r#"
1954 export default {
1955 testDir: "./tests"
1956 } satisfies PlaywrightTestConfig;
1957 "#;
1958 let val = extract_config_string(source, &ts_path(), &["testDir"]);
1959 assert_eq!(val, Some("./tests".to_string()));
1960 }
1961
1962 #[test]
1963 fn ts_as_direct_export() {
1964 let source = r#"
1965 export default {
1966 testDir: "./tests"
1967 } as const;
1968 "#;
1969 let val = extract_config_string(source, &ts_path(), &["testDir"]);
1970 assert_eq!(val, Some("./tests".to_string()));
1971 }
1972}