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]
343pub fn normalize_config_path(raw: &str, config_path: &Path, root: &Path) -> Option<String> {
344 if raw.is_empty() {
345 return None;
346 }
347
348 let candidate = if let Some(stripped) = raw.strip_prefix('/') {
349 lexical_normalize(&root.join(stripped))
350 } else {
351 let path = Path::new(raw);
352 if path.is_absolute() {
353 lexical_normalize(path)
354 } else {
355 let base = config_path.parent().unwrap_or(root);
356 lexical_normalize(&base.join(path))
357 }
358 };
359
360 let relative = candidate.strip_prefix(root).ok()?;
361 let normalized = relative.to_string_lossy().replace('\\', "/");
362 (!normalized.is_empty()).then_some(normalized)
363}
364
365fn extract_from_source<T>(
374 source: &str,
375 path: &Path,
376 extractor: impl FnOnce(&Program) -> Option<T>,
377) -> Option<T> {
378 let source_type = SourceType::from_path(path).unwrap_or_default();
379 let alloc = Allocator::default();
380
381 let is_json = path
384 .extension()
385 .is_some_and(|ext| ext == "json" || ext == "jsonc");
386 if is_json {
387 let wrapped = format!("({source})");
388 let parsed = Parser::new(&alloc, &wrapped, SourceType::mjs()).parse();
389 return extractor(&parsed.program);
390 }
391
392 let parsed = Parser::new(&alloc, source, source_type).parse();
393 extractor(&parsed.program)
394}
395
396fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
408 for stmt in &program.body {
409 match stmt {
410 Statement::ExportDefaultDeclaration(decl) => {
412 let expr: Option<&Expression> = match &decl.declaration {
414 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
415 return Some(obj);
416 }
417 _ => decl.declaration.as_expression(),
418 };
419 if let Some(expr) = expr {
420 if let Some(obj) = extract_object_from_expression(expr) {
422 return Some(obj);
423 }
424 if let Some(name) = unwrap_to_identifier_name(expr) {
427 return find_variable_init_object(program, name);
428 }
429 }
430 }
431 Statement::ExpressionStatement(expr_stmt) => {
433 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
434 && is_module_exports_target(&assign.left)
435 {
436 return extract_object_from_expression(&assign.right);
437 }
438 }
439 _ => {}
440 }
441 }
442
443 if program.body.len() == 1
446 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
447 {
448 match &expr_stmt.expression {
449 Expression::ObjectExpression(obj) => return Some(obj),
450 Expression::ParenthesizedExpression(paren) => {
451 if let Expression::ObjectExpression(obj) = &paren.expression {
452 return Some(obj);
453 }
454 }
455 _ => {}
456 }
457 }
458
459 None
460}
461
462fn extract_object_from_expression<'a>(
464 expr: &'a Expression<'a>,
465) -> Option<&'a ObjectExpression<'a>> {
466 match expr {
467 Expression::ObjectExpression(obj) => Some(obj),
469 Expression::CallExpression(call) => {
471 for arg in &call.arguments {
473 match arg {
474 Argument::ObjectExpression(obj) => return Some(obj),
475 Argument::ArrowFunctionExpression(arrow) => {
477 if arrow.expression
478 && !arrow.body.statements.is_empty()
479 && let Statement::ExpressionStatement(expr_stmt) =
480 &arrow.body.statements[0]
481 {
482 return extract_object_from_expression(&expr_stmt.expression);
483 }
484 }
485 _ => {}
486 }
487 }
488 None
489 }
490 Expression::ParenthesizedExpression(paren) => {
492 extract_object_from_expression(&paren.expression)
493 }
494 Expression::TSSatisfiesExpression(ts_sat) => {
496 extract_object_from_expression(&ts_sat.expression)
497 }
498 Expression::TSAsExpression(ts_as) => extract_object_from_expression(&ts_as.expression),
499 _ => None,
500 }
501}
502
503fn is_module_exports_target(target: &AssignmentTarget) -> bool {
505 if let AssignmentTarget::StaticMemberExpression(member) = target
506 && let Expression::Identifier(obj) = &member.object
507 {
508 return obj.name == "module" && member.property.name == "exports";
509 }
510 false
511}
512
513fn unwrap_to_identifier_name<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
517 match expr {
518 Expression::Identifier(id) => Some(&id.name),
519 Expression::TSSatisfiesExpression(ts_sat) => unwrap_to_identifier_name(&ts_sat.expression),
520 Expression::TSAsExpression(ts_as) => unwrap_to_identifier_name(&ts_as.expression),
521 _ => None,
522 }
523}
524
525fn find_variable_init_object<'a>(
530 program: &'a Program,
531 name: &str,
532) -> Option<&'a ObjectExpression<'a>> {
533 for stmt in &program.body {
534 if let Statement::VariableDeclaration(decl) = stmt {
535 for declarator in &decl.declarations {
536 if let BindingPattern::BindingIdentifier(id) = &declarator.id
537 && id.name == name
538 && let Some(init) = &declarator.init
539 {
540 return extract_object_from_expression(init);
541 }
542 }
543 }
544 }
545 None
546}
547
548fn find_property<'a>(obj: &'a ObjectExpression<'a>, key: &str) -> Option<&'a ObjectProperty<'a>> {
550 for prop in &obj.properties {
551 if let ObjectPropertyKind::ObjectProperty(p) = prop
552 && property_key_matches(&p.key, key)
553 {
554 return Some(p);
555 }
556 }
557 None
558}
559
560fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
562 match key {
563 PropertyKey::StaticIdentifier(id) => id.name == name,
564 PropertyKey::StringLiteral(s) => s.value == name,
565 _ => false,
566 }
567}
568
569fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
571 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
572}
573
574fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
576 find_property(obj, key)
577 .map(|p| expression_to_string_array(&p.value))
578 .unwrap_or_default()
579}
580
581fn get_nested_string_array_from_object(
583 obj: &ObjectExpression,
584 path: &[&str],
585) -> Option<Vec<String>> {
586 if path.is_empty() {
587 return None;
588 }
589 if path.len() == 1 {
590 return Some(get_object_string_array_property(obj, path[0]));
591 }
592 let prop = find_property(obj, path[0])?;
594 if let Expression::ObjectExpression(nested) = &prop.value {
595 get_nested_string_array_from_object(nested, &path[1..])
596 } else {
597 None
598 }
599}
600
601fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
603 if path.is_empty() {
604 return None;
605 }
606 if path.len() == 1 {
607 return get_object_string_property(obj, path[0]);
608 }
609 let prop = find_property(obj, path[0])?;
610 if let Expression::ObjectExpression(nested) = &prop.value {
611 get_nested_string_from_object(nested, &path[1..])
612 } else {
613 None
614 }
615}
616
617fn expression_to_string(expr: &Expression) -> Option<String> {
619 match expr {
620 Expression::StringLiteral(s) => Some(s.value.to_string()),
621 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
622 t.quasis.first().map(|q| q.value.raw.to_string())
624 }
625 _ => None,
626 }
627}
628
629fn expression_to_path_string(expr: &Expression) -> Option<String> {
631 match expr {
632 Expression::ParenthesizedExpression(paren) => expression_to_path_string(&paren.expression),
633 Expression::TSAsExpression(ts_as) => expression_to_path_string(&ts_as.expression),
634 Expression::TSSatisfiesExpression(ts_sat) => expression_to_path_string(&ts_sat.expression),
635 Expression::CallExpression(call) => call_expression_to_path_string(call),
636 Expression::NewExpression(new_expr) => new_expression_to_path_string(new_expr),
637 _ => expression_to_string(expr),
638 }
639}
640
641fn call_expression_to_path_string(call: &CallExpression) -> Option<String> {
642 if matches!(&call.callee, Expression::Identifier(id) if id.name == "fileURLToPath") {
643 return call
644 .arguments
645 .first()
646 .and_then(Argument::as_expression)
647 .and_then(expression_to_path_string);
648 }
649
650 let callee_name = match &call.callee {
651 Expression::Identifier(id) => Some(id.name.as_str()),
652 Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
653 _ => None,
654 }?;
655
656 if !matches!(callee_name, "resolve" | "join") {
657 return None;
658 }
659
660 let mut segments = Vec::new();
661 for (index, arg) in call.arguments.iter().enumerate() {
662 let expr = arg.as_expression()?;
663
664 if matches!(expr, Expression::Identifier(id) if id.name == "__dirname") {
665 if index == 0 {
666 continue;
667 }
668 return None;
669 }
670
671 segments.push(expression_to_string(expr)?);
672 }
673
674 (!segments.is_empty()).then(|| join_path_segments(&segments))
675}
676
677fn new_expression_to_path_string(new_expr: &NewExpression) -> Option<String> {
678 if !matches!(&new_expr.callee, Expression::Identifier(id) if id.name == "URL") {
679 return None;
680 }
681
682 let source = new_expr
683 .arguments
684 .first()
685 .and_then(Argument::as_expression)
686 .and_then(expression_to_string)?;
687
688 let base = new_expr
689 .arguments
690 .get(1)
691 .and_then(Argument::as_expression)?;
692 is_import_meta_url_expression(base).then_some(source)
693}
694
695fn is_import_meta_url_expression(expr: &Expression) -> bool {
696 if let Expression::StaticMemberExpression(member) = expr {
697 member.property.name == "url" && matches!(member.object, Expression::MetaProperty(_))
698 } else {
699 false
700 }
701}
702
703fn join_path_segments(segments: &[String]) -> String {
704 let mut joined = PathBuf::new();
705 for segment in segments {
706 joined.push(segment);
707 }
708 joined.to_string_lossy().replace('\\', "/")
709}
710
711fn expression_to_alias_pairs(expr: &Expression) -> Vec<(String, String)> {
712 match expr {
713 Expression::ObjectExpression(obj) => obj
714 .properties
715 .iter()
716 .filter_map(|prop| {
717 let ObjectPropertyKind::ObjectProperty(prop) = prop else {
718 return None;
719 };
720 let find = property_key_to_string(&prop.key)?;
721 let replacement = expression_to_path_string(&prop.value)?;
722 Some((find, replacement))
723 })
724 .collect(),
725 Expression::ArrayExpression(arr) => arr
726 .elements
727 .iter()
728 .filter_map(|element| {
729 let Expression::ObjectExpression(obj) = element.as_expression()? else {
730 return None;
731 };
732 let find = find_property(obj, "find")
733 .and_then(|prop| expression_to_string(&prop.value))?;
734 let replacement = find_property(obj, "replacement")
735 .and_then(|prop| expression_to_path_string(&prop.value))?;
736 Some((find, replacement))
737 })
738 .collect(),
739 _ => Vec::new(),
740 }
741}
742
743fn lexical_normalize(path: &Path) -> PathBuf {
744 let mut normalized = PathBuf::new();
745
746 for component in path.components() {
747 match component {
748 std::path::Component::CurDir => {}
749 std::path::Component::ParentDir => {
750 normalized.pop();
751 }
752 _ => normalized.push(component.as_os_str()),
753 }
754 }
755
756 normalized
757}
758
759fn expression_to_string_array(expr: &Expression) -> Vec<String> {
761 match expr {
762 Expression::ArrayExpression(arr) => arr
763 .elements
764 .iter()
765 .filter_map(|el| match el {
766 ArrayExpressionElement::SpreadElement(_) => None,
767 _ => el.as_expression().and_then(expression_to_string),
768 })
769 .collect(),
770 _ => vec![],
771 }
772}
773
774fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
779 let mut values = Vec::new();
780 match expr {
781 Expression::StringLiteral(s) => {
782 values.push(s.value.to_string());
783 }
784 Expression::ArrayExpression(arr) => {
785 for el in &arr.elements {
786 if let Some(inner) = el.as_expression() {
787 match inner {
788 Expression::StringLiteral(s) => {
789 values.push(s.value.to_string());
790 }
791 Expression::ArrayExpression(sub_arr) => {
793 if let Some(first) = sub_arr.elements.first()
794 && let Some(first_expr) = first.as_expression()
795 && let Some(s) = expression_to_string(first_expr)
796 {
797 values.push(s);
798 }
799 }
800 _ => {}
801 }
802 }
803 }
804 }
805 Expression::ObjectExpression(obj) => {
807 for prop in &obj.properties {
808 if let ObjectPropertyKind::ObjectProperty(p) = prop {
809 match &p.value {
810 Expression::StringLiteral(s) => {
811 values.push(s.value.to_string());
812 }
813 Expression::ArrayExpression(sub_arr) => {
815 if let Some(first) = sub_arr.elements.first()
816 && let Some(first_expr) = first.as_expression()
817 && let Some(s) = expression_to_string(first_expr)
818 {
819 values.push(s);
820 }
821 }
822 _ => {}
823 }
824 }
825 }
826 }
827 _ => {}
828 }
829 values
830}
831
832fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
834 match expr {
835 Expression::StringLiteral(s) => {
836 values.push(s.value.to_string());
837 }
838 Expression::ArrayExpression(arr) => {
839 for el in &arr.elements {
840 if let Some(expr) = el.as_expression() {
841 collect_all_string_values(expr, values);
842 }
843 }
844 }
845 Expression::ObjectExpression(obj) => {
846 for prop in &obj.properties {
847 if let ObjectPropertyKind::ObjectProperty(p) = prop {
848 collect_all_string_values(&p.value, values);
849 }
850 }
851 }
852 _ => {}
853 }
854}
855
856fn property_key_to_string(key: &PropertyKey) -> Option<String> {
858 match key {
859 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
860 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
861 _ => None,
862 }
863}
864
865fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
867 if path.is_empty() {
868 return None;
869 }
870 let prop = find_property(obj, path[0])?;
871 if path.len() == 1 {
872 if let Expression::ObjectExpression(nested) = &prop.value {
873 let keys = nested
874 .properties
875 .iter()
876 .filter_map(|p| {
877 if let ObjectPropertyKind::ObjectProperty(p) = p {
878 property_key_to_string(&p.key)
879 } else {
880 None
881 }
882 })
883 .collect();
884 return Some(keys);
885 }
886 return None;
887 }
888 if let Expression::ObjectExpression(nested) = &prop.value {
889 get_nested_object_keys(nested, &path[1..])
890 } else {
891 None
892 }
893}
894
895fn get_nested_expression<'a>(
897 obj: &'a ObjectExpression<'a>,
898 path: &[&str],
899) -> Option<&'a Expression<'a>> {
900 if path.is_empty() {
901 return None;
902 }
903 let prop = find_property(obj, path[0])?;
904 if path.len() == 1 {
905 return Some(&prop.value);
906 }
907 if let Expression::ObjectExpression(nested) = &prop.value {
908 get_nested_expression(nested, &path[1..])
909 } else {
910 None
911 }
912}
913
914fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
916 if path.is_empty() {
917 return None;
918 }
919 if path.len() == 1 {
920 let prop = find_property(obj, path[0])?;
921 return Some(expression_to_string_or_array(&prop.value));
922 }
923 let prop = find_property(obj, path[0])?;
924 if let Expression::ObjectExpression(nested) = &prop.value {
925 get_nested_string_or_array(nested, &path[1..])
926 } else {
927 None
928 }
929}
930
931fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
933 match expr {
934 Expression::StringLiteral(s) => vec![s.value.to_string()],
935 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
936 .quasis
937 .first()
938 .map(|q| vec![q.value.raw.to_string()])
939 .unwrap_or_default(),
940 Expression::ArrayExpression(arr) => arr
941 .elements
942 .iter()
943 .filter_map(|el| el.as_expression().and_then(expression_to_string))
944 .collect(),
945 Expression::ObjectExpression(obj) => obj
946 .properties
947 .iter()
948 .filter_map(|p| {
949 if let ObjectPropertyKind::ObjectProperty(p) = p {
950 expression_to_string(&p.value)
951 } else {
952 None
953 }
954 })
955 .collect(),
956 _ => vec![],
957 }
958}
959
960fn collect_require_sources(expr: &Expression) -> Vec<String> {
962 let mut sources = Vec::new();
963 match expr {
964 Expression::CallExpression(call) if is_require_call(call) => {
965 if let Some(s) = get_require_source(call) {
966 sources.push(s);
967 }
968 }
969 Expression::ArrayExpression(arr) => {
970 for el in &arr.elements {
971 if let Some(inner) = el.as_expression() {
972 match inner {
973 Expression::CallExpression(call) if is_require_call(call) => {
974 if let Some(s) = get_require_source(call) {
975 sources.push(s);
976 }
977 }
978 Expression::ArrayExpression(sub_arr) => {
980 if let Some(first) = sub_arr.elements.first()
981 && let Some(Expression::CallExpression(call)) =
982 first.as_expression()
983 && is_require_call(call)
984 && let Some(s) = get_require_source(call)
985 {
986 sources.push(s);
987 }
988 }
989 _ => {}
990 }
991 }
992 }
993 }
994 _ => {}
995 }
996 sources
997}
998
999fn is_require_call(call: &CallExpression) -> bool {
1001 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
1002}
1003
1004fn get_require_source(call: &CallExpression) -> Option<String> {
1006 call.arguments.first().and_then(|arg| {
1007 if let Argument::StringLiteral(s) = arg {
1008 Some(s.value.to_string())
1009 } else {
1010 None
1011 }
1012 })
1013}
1014
1015#[cfg(test)]
1016mod tests {
1017 use super::*;
1018 use std::path::PathBuf;
1019
1020 fn js_path() -> PathBuf {
1021 PathBuf::from("config.js")
1022 }
1023
1024 fn ts_path() -> PathBuf {
1025 PathBuf::from("config.ts")
1026 }
1027
1028 #[test]
1029 fn extract_imports_basic() {
1030 let source = r"
1031 import foo from 'foo-pkg';
1032 import { bar } from '@scope/bar';
1033 export default {};
1034 ";
1035 let imports = extract_imports(source, &js_path());
1036 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
1037 }
1038
1039 #[test]
1040 fn extract_default_export_object_property() {
1041 let source = r#"export default { testDir: "./tests" };"#;
1042 let val = extract_config_string(source, &js_path(), &["testDir"]);
1043 assert_eq!(val, Some("./tests".to_string()));
1044 }
1045
1046 #[test]
1047 fn extract_define_config_property() {
1048 let source = r#"
1049 import { defineConfig } from 'vitest/config';
1050 export default defineConfig({
1051 test: {
1052 include: ["**/*.test.ts", "**/*.spec.ts"],
1053 setupFiles: ["./test/setup.ts"]
1054 }
1055 });
1056 "#;
1057 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1058 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
1059
1060 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
1061 assert_eq!(setup, vec!["./test/setup.ts"]);
1062 }
1063
1064 #[test]
1065 fn extract_module_exports_property() {
1066 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
1067 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
1068 assert_eq!(val, Some("jsdom".to_string()));
1069 }
1070
1071 #[test]
1072 fn extract_nested_string_array() {
1073 let source = r#"
1074 export default {
1075 resolve: {
1076 alias: {
1077 "@": "./src"
1078 }
1079 },
1080 test: {
1081 include: ["src/**/*.test.ts"]
1082 }
1083 };
1084 "#;
1085 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
1086 assert_eq!(include, vec!["src/**/*.test.ts"]);
1087 }
1088
1089 #[test]
1090 fn extract_addons_array() {
1091 let source = r#"
1092 export default {
1093 addons: [
1094 "@storybook/addon-a11y",
1095 "@storybook/addon-docs",
1096 "@storybook/addon-links"
1097 ]
1098 };
1099 "#;
1100 let addons = extract_config_property_strings(source, &ts_path(), "addons");
1101 assert_eq!(
1102 addons,
1103 vec![
1104 "@storybook/addon-a11y",
1105 "@storybook/addon-docs",
1106 "@storybook/addon-links"
1107 ]
1108 );
1109 }
1110
1111 #[test]
1112 fn handle_empty_config() {
1113 let source = "";
1114 let result = extract_config_string(source, &js_path(), &["key"]);
1115 assert_eq!(result, None);
1116 }
1117
1118 #[test]
1121 fn object_keys_postcss_plugins() {
1122 let source = r"
1123 module.exports = {
1124 plugins: {
1125 autoprefixer: {},
1126 tailwindcss: {},
1127 'postcss-import': {}
1128 }
1129 };
1130 ";
1131 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1132 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
1133 }
1134
1135 #[test]
1136 fn object_keys_nested_path() {
1137 let source = r"
1138 export default {
1139 build: {
1140 plugins: {
1141 minify: {},
1142 compress: {}
1143 }
1144 }
1145 };
1146 ";
1147 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
1148 assert_eq!(keys, vec!["minify", "compress"]);
1149 }
1150
1151 #[test]
1152 fn object_keys_empty_object() {
1153 let source = r"export default { plugins: {} };";
1154 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1155 assert!(keys.is_empty());
1156 }
1157
1158 #[test]
1159 fn object_keys_non_object_returns_empty() {
1160 let source = r#"export default { plugins: ["a", "b"] };"#;
1161 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1162 assert!(keys.is_empty());
1163 }
1164
1165 #[test]
1168 fn string_or_array_single_string() {
1169 let source = r#"export default { entry: "./src/index.js" };"#;
1170 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1171 assert_eq!(result, vec!["./src/index.js"]);
1172 }
1173
1174 #[test]
1175 fn string_or_array_array() {
1176 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
1177 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1178 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
1179 }
1180
1181 #[test]
1182 fn string_or_array_object_values() {
1183 let source =
1184 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
1185 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1186 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
1187 }
1188
1189 #[test]
1190 fn string_or_array_nested_path() {
1191 let source = r#"
1192 export default {
1193 build: {
1194 rollupOptions: {
1195 input: ["./index.html", "./about.html"]
1196 }
1197 }
1198 };
1199 "#;
1200 let result = extract_config_string_or_array(
1201 source,
1202 &js_path(),
1203 &["build", "rollupOptions", "input"],
1204 );
1205 assert_eq!(result, vec!["./index.html", "./about.html"]);
1206 }
1207
1208 #[test]
1209 fn string_or_array_template_literal() {
1210 let source = r"export default { entry: `./src/index.js` };";
1211 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1212 assert_eq!(result, vec!["./src/index.js"]);
1213 }
1214
1215 #[test]
1218 fn require_strings_array() {
1219 let source = r"
1220 module.exports = {
1221 plugins: [
1222 require('autoprefixer'),
1223 require('postcss-import')
1224 ]
1225 };
1226 ";
1227 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1228 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
1229 }
1230
1231 #[test]
1232 fn require_strings_with_tuples() {
1233 let source = r"
1234 module.exports = {
1235 plugins: [
1236 require('autoprefixer'),
1237 [require('postcss-preset-env'), { stage: 3 }]
1238 ]
1239 };
1240 ";
1241 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1242 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
1243 }
1244
1245 #[test]
1246 fn require_strings_empty_array() {
1247 let source = r"module.exports = { plugins: [] };";
1248 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1249 assert!(deps.is_empty());
1250 }
1251
1252 #[test]
1253 fn require_strings_no_require_calls() {
1254 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
1255 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1256 assert!(deps.is_empty());
1257 }
1258
1259 #[test]
1260 fn extract_aliases_from_object_with_file_url_to_path() {
1261 let source = r#"
1262 import { defineConfig } from 'vite';
1263 import { fileURLToPath, URL } from 'node:url';
1264
1265 export default defineConfig({
1266 resolve: {
1267 alias: {
1268 "@": fileURLToPath(new URL("./src", import.meta.url))
1269 }
1270 }
1271 });
1272 "#;
1273
1274 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
1275 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
1276 }
1277
1278 #[test]
1279 fn extract_aliases_from_array_form() {
1280 let source = r#"
1281 export default {
1282 resolve: {
1283 alias: [
1284 { find: "@", replacement: "./src" },
1285 { find: "$utils", replacement: "src/lib/utils" }
1286 ]
1287 }
1288 };
1289 "#;
1290
1291 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
1292 assert_eq!(
1293 aliases,
1294 vec![
1295 ("@".to_string(), "./src".to_string()),
1296 ("$utils".to_string(), "src/lib/utils".to_string())
1297 ]
1298 );
1299 }
1300
1301 #[test]
1302 fn extract_array_object_strings_mixed_forms() {
1303 let source = r#"
1304 export default {
1305 components: [
1306 "~/components",
1307 { path: "@/feature-components" }
1308 ]
1309 };
1310 "#;
1311
1312 let values =
1313 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
1314 assert_eq!(
1315 values,
1316 vec![
1317 "~/components".to_string(),
1318 "@/feature-components".to_string()
1319 ]
1320 );
1321 }
1322
1323 #[test]
1324 fn normalize_config_path_relative_to_root() {
1325 let config_path = PathBuf::from("/project/vite.config.ts");
1326 let root = PathBuf::from("/project");
1327
1328 assert_eq!(
1329 normalize_config_path("./src/lib", &config_path, &root),
1330 Some("src/lib".to_string())
1331 );
1332 assert_eq!(
1333 normalize_config_path("/src/lib", &config_path, &root),
1334 Some("src/lib".to_string())
1335 );
1336 }
1337
1338 #[test]
1341 fn json_wrapped_in_parens_string() {
1342 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
1343 let val = extract_config_string(source, &js_path(), &["extends"]);
1344 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
1345 }
1346
1347 #[test]
1348 fn json_wrapped_in_parens_nested_array() {
1349 let source =
1350 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
1351 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
1352 assert_eq!(types, vec!["node", "jest"]);
1353
1354 let include = extract_config_string_array(source, &js_path(), &["include"]);
1355 assert_eq!(include, vec!["src/**/*"]);
1356 }
1357
1358 #[test]
1359 fn json_wrapped_in_parens_object_keys() {
1360 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
1361 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1362 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
1363 }
1364
1365 fn json_path() -> PathBuf {
1368 PathBuf::from("config.json")
1369 }
1370
1371 #[test]
1372 fn json_file_parsed_correctly() {
1373 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
1374 let val = extract_config_string(source, &json_path(), &["key"]);
1375 assert_eq!(val, Some("value".to_string()));
1376
1377 let list = extract_config_string_array(source, &json_path(), &["list"]);
1378 assert_eq!(list, vec!["a", "b"]);
1379 }
1380
1381 #[test]
1382 fn jsonc_file_parsed_correctly() {
1383 let source = r#"{"key": "value"}"#;
1384 let path = PathBuf::from("tsconfig.jsonc");
1385 let val = extract_config_string(source, &path, &["key"]);
1386 assert_eq!(val, Some("value".to_string()));
1387 }
1388
1389 #[test]
1392 fn extract_define_config_arrow_function() {
1393 let source = r#"
1394 import { defineConfig } from 'vite';
1395 export default defineConfig(() => ({
1396 test: {
1397 include: ["**/*.test.ts"]
1398 }
1399 }));
1400 "#;
1401 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1402 assert_eq!(include, vec!["**/*.test.ts"]);
1403 }
1404
1405 #[test]
1408 fn module_exports_nested_string() {
1409 let source = r#"
1410 module.exports = {
1411 resolve: {
1412 alias: {
1413 "@": "./src"
1414 }
1415 }
1416 };
1417 "#;
1418 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
1419 assert_eq!(val, Some("./src".to_string()));
1420 }
1421
1422 #[test]
1425 fn property_strings_nested_objects() {
1426 let source = r#"
1427 export default {
1428 plugins: {
1429 group1: { a: "val-a" },
1430 group2: { b: "val-b" }
1431 }
1432 };
1433 "#;
1434 let values = extract_config_property_strings(source, &js_path(), "plugins");
1435 assert!(values.contains(&"val-a".to_string()));
1436 assert!(values.contains(&"val-b".to_string()));
1437 }
1438
1439 #[test]
1440 fn property_strings_missing_key_returns_empty() {
1441 let source = r#"export default { other: "value" };"#;
1442 let values = extract_config_property_strings(source, &js_path(), "missing");
1443 assert!(values.is_empty());
1444 }
1445
1446 #[test]
1449 fn shallow_strings_tuple_array() {
1450 let source = r#"
1451 module.exports = {
1452 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
1453 };
1454 "#;
1455 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
1456 assert_eq!(values, vec!["default", "jest-junit"]);
1457 assert!(!values.contains(&"reports".to_string()));
1459 }
1460
1461 #[test]
1462 fn shallow_strings_single_string() {
1463 let source = r#"export default { preset: "ts-jest" };"#;
1464 let values = extract_config_shallow_strings(source, &js_path(), "preset");
1465 assert_eq!(values, vec!["ts-jest"]);
1466 }
1467
1468 #[test]
1469 fn shallow_strings_missing_key() {
1470 let source = r#"export default { other: "val" };"#;
1471 let values = extract_config_shallow_strings(source, &js_path(), "missing");
1472 assert!(values.is_empty());
1473 }
1474
1475 #[test]
1478 fn nested_shallow_strings_vitest_reporters() {
1479 let source = r#"
1480 export default {
1481 test: {
1482 reporters: ["default", "vitest-sonar-reporter"]
1483 }
1484 };
1485 "#;
1486 let values =
1487 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1488 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
1489 }
1490
1491 #[test]
1492 fn nested_shallow_strings_tuple_format() {
1493 let source = r#"
1494 export default {
1495 test: {
1496 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
1497 }
1498 };
1499 "#;
1500 let values =
1501 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1502 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
1503 }
1504
1505 #[test]
1506 fn nested_shallow_strings_missing_outer() {
1507 let source = r"export default { other: {} };";
1508 let values =
1509 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1510 assert!(values.is_empty());
1511 }
1512
1513 #[test]
1514 fn nested_shallow_strings_missing_inner() {
1515 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
1516 let values =
1517 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1518 assert!(values.is_empty());
1519 }
1520
1521 #[test]
1524 fn string_or_array_missing_path() {
1525 let source = r"export default {};";
1526 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1527 assert!(result.is_empty());
1528 }
1529
1530 #[test]
1531 fn string_or_array_non_string_values() {
1532 let source = r"export default { entry: [42, true] };";
1534 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1535 assert!(result.is_empty());
1536 }
1537
1538 #[test]
1541 fn array_nested_extraction() {
1542 let source = r#"
1543 export default defineConfig({
1544 test: {
1545 projects: [
1546 {
1547 test: {
1548 setupFiles: ["./test/setup-a.ts"]
1549 }
1550 },
1551 {
1552 test: {
1553 setupFiles: "./test/setup-b.ts"
1554 }
1555 }
1556 ]
1557 }
1558 });
1559 "#;
1560 let results = extract_config_array_nested_string_or_array(
1561 source,
1562 &ts_path(),
1563 &["test", "projects"],
1564 &["test", "setupFiles"],
1565 );
1566 assert!(results.contains(&"./test/setup-a.ts".to_string()));
1567 assert!(results.contains(&"./test/setup-b.ts".to_string()));
1568 }
1569
1570 #[test]
1571 fn array_nested_empty_when_no_array() {
1572 let source = r#"export default { test: { projects: "not-an-array" } };"#;
1573 let results = extract_config_array_nested_string_or_array(
1574 source,
1575 &js_path(),
1576 &["test", "projects"],
1577 &["test", "setupFiles"],
1578 );
1579 assert!(results.is_empty());
1580 }
1581
1582 #[test]
1585 fn object_nested_extraction() {
1586 let source = r#"{
1587 "projects": {
1588 "app-one": {
1589 "architect": {
1590 "build": {
1591 "options": {
1592 "styles": ["src/styles.css"]
1593 }
1594 }
1595 }
1596 }
1597 }
1598 }"#;
1599 let results = extract_config_object_nested_string_or_array(
1600 source,
1601 &json_path(),
1602 &["projects"],
1603 &["architect", "build", "options", "styles"],
1604 );
1605 assert_eq!(results, vec!["src/styles.css"]);
1606 }
1607
1608 #[test]
1611 fn object_nested_strings_extraction() {
1612 let source = r#"{
1613 "targets": {
1614 "build": {
1615 "executor": "@angular/build:application"
1616 },
1617 "test": {
1618 "executor": "@nx/vite:test"
1619 }
1620 }
1621 }"#;
1622 let results =
1623 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
1624 assert!(results.contains(&"@angular/build:application".to_string()));
1625 assert!(results.contains(&"@nx/vite:test".to_string()));
1626 }
1627
1628 #[test]
1631 fn require_strings_direct_call() {
1632 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
1633 let deps = extract_config_require_strings(source, &js_path(), "adapter");
1634 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
1635 }
1636
1637 #[test]
1638 fn require_strings_no_matching_key() {
1639 let source = r"module.exports = { other: require('something') };";
1640 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1641 assert!(deps.is_empty());
1642 }
1643
1644 #[test]
1647 fn extract_imports_no_imports() {
1648 let source = r"export default {};";
1649 let imports = extract_imports(source, &js_path());
1650 assert!(imports.is_empty());
1651 }
1652
1653 #[test]
1654 fn extract_imports_side_effect_import() {
1655 let source = r"
1656 import 'polyfill';
1657 import './local-setup';
1658 export default {};
1659 ";
1660 let imports = extract_imports(source, &js_path());
1661 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
1662 }
1663
1664 #[test]
1665 fn extract_imports_mixed_specifiers() {
1666 let source = r"
1667 import defaultExport from 'module-a';
1668 import { named } from 'module-b';
1669 import * as ns from 'module-c';
1670 export default {};
1671 ";
1672 let imports = extract_imports(source, &js_path());
1673 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
1674 }
1675
1676 #[test]
1679 fn template_literal_in_string_or_array() {
1680 let source = r"export default { entry: `./src/index.ts` };";
1681 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
1682 assert_eq!(result, vec!["./src/index.ts"]);
1683 }
1684
1685 #[test]
1686 fn template_literal_in_config_string() {
1687 let source = r"export default { testDir: `./tests` };";
1688 let val = extract_config_string(source, &js_path(), &["testDir"]);
1689 assert_eq!(val, Some("./tests".to_string()));
1690 }
1691
1692 #[test]
1695 fn nested_string_array_empty_path() {
1696 let source = r#"export default { items: ["a", "b"] };"#;
1697 let result = extract_config_string_array(source, &js_path(), &[]);
1698 assert!(result.is_empty());
1699 }
1700
1701 #[test]
1702 fn nested_string_empty_path() {
1703 let source = r#"export default { key: "val" };"#;
1704 let result = extract_config_string(source, &js_path(), &[]);
1705 assert!(result.is_none());
1706 }
1707
1708 #[test]
1709 fn object_keys_empty_path() {
1710 let source = r"export default { plugins: {} };";
1711 let result = extract_config_object_keys(source, &js_path(), &[]);
1712 assert!(result.is_empty());
1713 }
1714
1715 #[test]
1718 fn no_config_object_returns_empty() {
1719 let source = r"const x = 42;";
1721 let result = extract_config_string(source, &js_path(), &["key"]);
1722 assert!(result.is_none());
1723
1724 let arr = extract_config_string_array(source, &js_path(), &["items"]);
1725 assert!(arr.is_empty());
1726
1727 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1728 assert!(keys.is_empty());
1729 }
1730
1731 #[test]
1734 fn property_with_string_key() {
1735 let source = r#"export default { "string-key": "value" };"#;
1736 let val = extract_config_string(source, &js_path(), &["string-key"]);
1737 assert_eq!(val, Some("value".to_string()));
1738 }
1739
1740 #[test]
1741 fn nested_navigation_through_non_object() {
1742 let source = r#"export default { level1: "not-an-object" };"#;
1744 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
1745 assert!(val.is_none());
1746 }
1747
1748 #[test]
1751 fn variable_reference_untyped() {
1752 let source = r#"
1753 const config = {
1754 testDir: "./tests"
1755 };
1756 export default config;
1757 "#;
1758 let val = extract_config_string(source, &js_path(), &["testDir"]);
1759 assert_eq!(val, Some("./tests".to_string()));
1760 }
1761
1762 #[test]
1763 fn variable_reference_with_type_annotation() {
1764 let source = r#"
1765 import type { StorybookConfig } from '@storybook/react-vite';
1766 const config: StorybookConfig = {
1767 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
1768 framework: "@storybook/react-vite"
1769 };
1770 export default config;
1771 "#;
1772 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
1773 assert_eq!(
1774 addons,
1775 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
1776 );
1777
1778 let framework = extract_config_string(source, &ts_path(), &["framework"]);
1779 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
1780 }
1781
1782 #[test]
1783 fn variable_reference_with_define_config() {
1784 let source = r#"
1785 import { defineConfig } from 'vitest/config';
1786 const config = defineConfig({
1787 test: {
1788 include: ["**/*.test.ts"]
1789 }
1790 });
1791 export default config;
1792 "#;
1793 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1794 assert_eq!(include, vec!["**/*.test.ts"]);
1795 }
1796
1797 #[test]
1800 fn ts_satisfies_direct_export() {
1801 let source = r#"
1802 export default {
1803 testDir: "./tests"
1804 } satisfies PlaywrightTestConfig;
1805 "#;
1806 let val = extract_config_string(source, &ts_path(), &["testDir"]);
1807 assert_eq!(val, Some("./tests".to_string()));
1808 }
1809
1810 #[test]
1811 fn ts_as_direct_export() {
1812 let source = r#"
1813 export default {
1814 testDir: "./tests"
1815 } as const;
1816 "#;
1817 let val = extract_config_string(source, &ts_path(), &["testDir"]);
1818 assert_eq!(val, Some("./tests".to_string()));
1819 }
1820}