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 _ => {}
806 }
807 values
808}
809
810fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
812 match expr {
813 Expression::StringLiteral(s) => {
814 values.push(s.value.to_string());
815 }
816 Expression::ArrayExpression(arr) => {
817 for el in &arr.elements {
818 if let Some(expr) = el.as_expression() {
819 collect_all_string_values(expr, values);
820 }
821 }
822 }
823 Expression::ObjectExpression(obj) => {
824 for prop in &obj.properties {
825 if let ObjectPropertyKind::ObjectProperty(p) = prop {
826 collect_all_string_values(&p.value, values);
827 }
828 }
829 }
830 _ => {}
831 }
832}
833
834fn property_key_to_string(key: &PropertyKey) -> Option<String> {
836 match key {
837 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
838 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
839 _ => None,
840 }
841}
842
843fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
845 if path.is_empty() {
846 return None;
847 }
848 let prop = find_property(obj, path[0])?;
849 if path.len() == 1 {
850 if let Expression::ObjectExpression(nested) = &prop.value {
851 let keys = nested
852 .properties
853 .iter()
854 .filter_map(|p| {
855 if let ObjectPropertyKind::ObjectProperty(p) = p {
856 property_key_to_string(&p.key)
857 } else {
858 None
859 }
860 })
861 .collect();
862 return Some(keys);
863 }
864 return None;
865 }
866 if let Expression::ObjectExpression(nested) = &prop.value {
867 get_nested_object_keys(nested, &path[1..])
868 } else {
869 None
870 }
871}
872
873fn get_nested_expression<'a>(
875 obj: &'a ObjectExpression<'a>,
876 path: &[&str],
877) -> Option<&'a Expression<'a>> {
878 if path.is_empty() {
879 return None;
880 }
881 let prop = find_property(obj, path[0])?;
882 if path.len() == 1 {
883 return Some(&prop.value);
884 }
885 if let Expression::ObjectExpression(nested) = &prop.value {
886 get_nested_expression(nested, &path[1..])
887 } else {
888 None
889 }
890}
891
892fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
894 if path.is_empty() {
895 return None;
896 }
897 if path.len() == 1 {
898 let prop = find_property(obj, path[0])?;
899 return Some(expression_to_string_or_array(&prop.value));
900 }
901 let prop = find_property(obj, path[0])?;
902 if let Expression::ObjectExpression(nested) = &prop.value {
903 get_nested_string_or_array(nested, &path[1..])
904 } else {
905 None
906 }
907}
908
909fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
911 match expr {
912 Expression::StringLiteral(s) => vec![s.value.to_string()],
913 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
914 .quasis
915 .first()
916 .map(|q| vec![q.value.raw.to_string()])
917 .unwrap_or_default(),
918 Expression::ArrayExpression(arr) => arr
919 .elements
920 .iter()
921 .filter_map(|el| el.as_expression().and_then(expression_to_string))
922 .collect(),
923 Expression::ObjectExpression(obj) => obj
924 .properties
925 .iter()
926 .filter_map(|p| {
927 if let ObjectPropertyKind::ObjectProperty(p) = p {
928 expression_to_string(&p.value)
929 } else {
930 None
931 }
932 })
933 .collect(),
934 _ => vec![],
935 }
936}
937
938fn collect_require_sources(expr: &Expression) -> Vec<String> {
940 let mut sources = Vec::new();
941 match expr {
942 Expression::CallExpression(call) if is_require_call(call) => {
943 if let Some(s) = get_require_source(call) {
944 sources.push(s);
945 }
946 }
947 Expression::ArrayExpression(arr) => {
948 for el in &arr.elements {
949 if let Some(inner) = el.as_expression() {
950 match inner {
951 Expression::CallExpression(call) if is_require_call(call) => {
952 if let Some(s) = get_require_source(call) {
953 sources.push(s);
954 }
955 }
956 Expression::ArrayExpression(sub_arr) => {
958 if let Some(first) = sub_arr.elements.first()
959 && let Some(Expression::CallExpression(call)) =
960 first.as_expression()
961 && is_require_call(call)
962 && let Some(s) = get_require_source(call)
963 {
964 sources.push(s);
965 }
966 }
967 _ => {}
968 }
969 }
970 }
971 }
972 _ => {}
973 }
974 sources
975}
976
977fn is_require_call(call: &CallExpression) -> bool {
979 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
980}
981
982fn get_require_source(call: &CallExpression) -> Option<String> {
984 call.arguments.first().and_then(|arg| {
985 if let Argument::StringLiteral(s) = arg {
986 Some(s.value.to_string())
987 } else {
988 None
989 }
990 })
991}
992
993#[cfg(test)]
994mod tests {
995 use super::*;
996 use std::path::PathBuf;
997
998 fn js_path() -> PathBuf {
999 PathBuf::from("config.js")
1000 }
1001
1002 fn ts_path() -> PathBuf {
1003 PathBuf::from("config.ts")
1004 }
1005
1006 #[test]
1007 fn extract_imports_basic() {
1008 let source = r"
1009 import foo from 'foo-pkg';
1010 import { bar } from '@scope/bar';
1011 export default {};
1012 ";
1013 let imports = extract_imports(source, &js_path());
1014 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
1015 }
1016
1017 #[test]
1018 fn extract_default_export_object_property() {
1019 let source = r#"export default { testDir: "./tests" };"#;
1020 let val = extract_config_string(source, &js_path(), &["testDir"]);
1021 assert_eq!(val, Some("./tests".to_string()));
1022 }
1023
1024 #[test]
1025 fn extract_define_config_property() {
1026 let source = r#"
1027 import { defineConfig } from 'vitest/config';
1028 export default defineConfig({
1029 test: {
1030 include: ["**/*.test.ts", "**/*.spec.ts"],
1031 setupFiles: ["./test/setup.ts"]
1032 }
1033 });
1034 "#;
1035 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1036 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
1037
1038 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
1039 assert_eq!(setup, vec!["./test/setup.ts"]);
1040 }
1041
1042 #[test]
1043 fn extract_module_exports_property() {
1044 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
1045 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
1046 assert_eq!(val, Some("jsdom".to_string()));
1047 }
1048
1049 #[test]
1050 fn extract_nested_string_array() {
1051 let source = r#"
1052 export default {
1053 resolve: {
1054 alias: {
1055 "@": "./src"
1056 }
1057 },
1058 test: {
1059 include: ["src/**/*.test.ts"]
1060 }
1061 };
1062 "#;
1063 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
1064 assert_eq!(include, vec!["src/**/*.test.ts"]);
1065 }
1066
1067 #[test]
1068 fn extract_addons_array() {
1069 let source = r#"
1070 export default {
1071 addons: [
1072 "@storybook/addon-a11y",
1073 "@storybook/addon-docs",
1074 "@storybook/addon-links"
1075 ]
1076 };
1077 "#;
1078 let addons = extract_config_property_strings(source, &ts_path(), "addons");
1079 assert_eq!(
1080 addons,
1081 vec![
1082 "@storybook/addon-a11y",
1083 "@storybook/addon-docs",
1084 "@storybook/addon-links"
1085 ]
1086 );
1087 }
1088
1089 #[test]
1090 fn handle_empty_config() {
1091 let source = "";
1092 let result = extract_config_string(source, &js_path(), &["key"]);
1093 assert_eq!(result, None);
1094 }
1095
1096 #[test]
1099 fn object_keys_postcss_plugins() {
1100 let source = r"
1101 module.exports = {
1102 plugins: {
1103 autoprefixer: {},
1104 tailwindcss: {},
1105 'postcss-import': {}
1106 }
1107 };
1108 ";
1109 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1110 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
1111 }
1112
1113 #[test]
1114 fn object_keys_nested_path() {
1115 let source = r"
1116 export default {
1117 build: {
1118 plugins: {
1119 minify: {},
1120 compress: {}
1121 }
1122 }
1123 };
1124 ";
1125 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
1126 assert_eq!(keys, vec!["minify", "compress"]);
1127 }
1128
1129 #[test]
1130 fn object_keys_empty_object() {
1131 let source = r"export default { plugins: {} };";
1132 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1133 assert!(keys.is_empty());
1134 }
1135
1136 #[test]
1137 fn object_keys_non_object_returns_empty() {
1138 let source = r#"export default { plugins: ["a", "b"] };"#;
1139 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1140 assert!(keys.is_empty());
1141 }
1142
1143 #[test]
1146 fn string_or_array_single_string() {
1147 let source = r#"export default { entry: "./src/index.js" };"#;
1148 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1149 assert_eq!(result, vec!["./src/index.js"]);
1150 }
1151
1152 #[test]
1153 fn string_or_array_array() {
1154 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
1155 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1156 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
1157 }
1158
1159 #[test]
1160 fn string_or_array_object_values() {
1161 let source =
1162 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
1163 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1164 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
1165 }
1166
1167 #[test]
1168 fn string_or_array_nested_path() {
1169 let source = r#"
1170 export default {
1171 build: {
1172 rollupOptions: {
1173 input: ["./index.html", "./about.html"]
1174 }
1175 }
1176 };
1177 "#;
1178 let result = extract_config_string_or_array(
1179 source,
1180 &js_path(),
1181 &["build", "rollupOptions", "input"],
1182 );
1183 assert_eq!(result, vec!["./index.html", "./about.html"]);
1184 }
1185
1186 #[test]
1187 fn string_or_array_template_literal() {
1188 let source = r"export default { entry: `./src/index.js` };";
1189 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1190 assert_eq!(result, vec!["./src/index.js"]);
1191 }
1192
1193 #[test]
1196 fn require_strings_array() {
1197 let source = r"
1198 module.exports = {
1199 plugins: [
1200 require('autoprefixer'),
1201 require('postcss-import')
1202 ]
1203 };
1204 ";
1205 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1206 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
1207 }
1208
1209 #[test]
1210 fn require_strings_with_tuples() {
1211 let source = r"
1212 module.exports = {
1213 plugins: [
1214 require('autoprefixer'),
1215 [require('postcss-preset-env'), { stage: 3 }]
1216 ]
1217 };
1218 ";
1219 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1220 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
1221 }
1222
1223 #[test]
1224 fn require_strings_empty_array() {
1225 let source = r"module.exports = { plugins: [] };";
1226 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1227 assert!(deps.is_empty());
1228 }
1229
1230 #[test]
1231 fn require_strings_no_require_calls() {
1232 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
1233 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1234 assert!(deps.is_empty());
1235 }
1236
1237 #[test]
1238 fn extract_aliases_from_object_with_file_url_to_path() {
1239 let source = r#"
1240 import { defineConfig } from 'vite';
1241 import { fileURLToPath, URL } from 'node:url';
1242
1243 export default defineConfig({
1244 resolve: {
1245 alias: {
1246 "@": fileURLToPath(new URL("./src", import.meta.url))
1247 }
1248 }
1249 });
1250 "#;
1251
1252 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
1253 assert_eq!(aliases, vec![("@".to_string(), "./src".to_string())]);
1254 }
1255
1256 #[test]
1257 fn extract_aliases_from_array_form() {
1258 let source = r#"
1259 export default {
1260 resolve: {
1261 alias: [
1262 { find: "@", replacement: "./src" },
1263 { find: "$utils", replacement: "src/lib/utils" }
1264 ]
1265 }
1266 };
1267 "#;
1268
1269 let aliases = extract_config_aliases(source, &ts_path(), &["resolve", "alias"]);
1270 assert_eq!(
1271 aliases,
1272 vec![
1273 ("@".to_string(), "./src".to_string()),
1274 ("$utils".to_string(), "src/lib/utils".to_string())
1275 ]
1276 );
1277 }
1278
1279 #[test]
1280 fn extract_array_object_strings_mixed_forms() {
1281 let source = r#"
1282 export default {
1283 components: [
1284 "~/components",
1285 { path: "@/feature-components" }
1286 ]
1287 };
1288 "#;
1289
1290 let values =
1291 extract_config_array_object_strings(source, &ts_path(), &["components"], "path");
1292 assert_eq!(
1293 values,
1294 vec![
1295 "~/components".to_string(),
1296 "@/feature-components".to_string()
1297 ]
1298 );
1299 }
1300
1301 #[test]
1302 fn normalize_config_path_relative_to_root() {
1303 let config_path = PathBuf::from("/project/vite.config.ts");
1304 let root = PathBuf::from("/project");
1305
1306 assert_eq!(
1307 normalize_config_path("./src/lib", &config_path, &root),
1308 Some("src/lib".to_string())
1309 );
1310 assert_eq!(
1311 normalize_config_path("/src/lib", &config_path, &root),
1312 Some("src/lib".to_string())
1313 );
1314 }
1315
1316 #[test]
1319 fn json_wrapped_in_parens_string() {
1320 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
1321 let val = extract_config_string(source, &js_path(), &["extends"]);
1322 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
1323 }
1324
1325 #[test]
1326 fn json_wrapped_in_parens_nested_array() {
1327 let source =
1328 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
1329 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
1330 assert_eq!(types, vec!["node", "jest"]);
1331
1332 let include = extract_config_string_array(source, &js_path(), &["include"]);
1333 assert_eq!(include, vec!["src/**/*"]);
1334 }
1335
1336 #[test]
1337 fn json_wrapped_in_parens_object_keys() {
1338 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
1339 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1340 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
1341 }
1342
1343 fn json_path() -> PathBuf {
1346 PathBuf::from("config.json")
1347 }
1348
1349 #[test]
1350 fn json_file_parsed_correctly() {
1351 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
1352 let val = extract_config_string(source, &json_path(), &["key"]);
1353 assert_eq!(val, Some("value".to_string()));
1354
1355 let list = extract_config_string_array(source, &json_path(), &["list"]);
1356 assert_eq!(list, vec!["a", "b"]);
1357 }
1358
1359 #[test]
1360 fn jsonc_file_parsed_correctly() {
1361 let source = r#"{"key": "value"}"#;
1362 let path = PathBuf::from("tsconfig.jsonc");
1363 let val = extract_config_string(source, &path, &["key"]);
1364 assert_eq!(val, Some("value".to_string()));
1365 }
1366
1367 #[test]
1370 fn extract_define_config_arrow_function() {
1371 let source = r#"
1372 import { defineConfig } from 'vite';
1373 export default defineConfig(() => ({
1374 test: {
1375 include: ["**/*.test.ts"]
1376 }
1377 }));
1378 "#;
1379 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1380 assert_eq!(include, vec!["**/*.test.ts"]);
1381 }
1382
1383 #[test]
1386 fn module_exports_nested_string() {
1387 let source = r#"
1388 module.exports = {
1389 resolve: {
1390 alias: {
1391 "@": "./src"
1392 }
1393 }
1394 };
1395 "#;
1396 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
1397 assert_eq!(val, Some("./src".to_string()));
1398 }
1399
1400 #[test]
1403 fn property_strings_nested_objects() {
1404 let source = r#"
1405 export default {
1406 plugins: {
1407 group1: { a: "val-a" },
1408 group2: { b: "val-b" }
1409 }
1410 };
1411 "#;
1412 let values = extract_config_property_strings(source, &js_path(), "plugins");
1413 assert!(values.contains(&"val-a".to_string()));
1414 assert!(values.contains(&"val-b".to_string()));
1415 }
1416
1417 #[test]
1418 fn property_strings_missing_key_returns_empty() {
1419 let source = r#"export default { other: "value" };"#;
1420 let values = extract_config_property_strings(source, &js_path(), "missing");
1421 assert!(values.is_empty());
1422 }
1423
1424 #[test]
1427 fn shallow_strings_tuple_array() {
1428 let source = r#"
1429 module.exports = {
1430 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
1431 };
1432 "#;
1433 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
1434 assert_eq!(values, vec!["default", "jest-junit"]);
1435 assert!(!values.contains(&"reports".to_string()));
1437 }
1438
1439 #[test]
1440 fn shallow_strings_single_string() {
1441 let source = r#"export default { preset: "ts-jest" };"#;
1442 let values = extract_config_shallow_strings(source, &js_path(), "preset");
1443 assert_eq!(values, vec!["ts-jest"]);
1444 }
1445
1446 #[test]
1447 fn shallow_strings_missing_key() {
1448 let source = r#"export default { other: "val" };"#;
1449 let values = extract_config_shallow_strings(source, &js_path(), "missing");
1450 assert!(values.is_empty());
1451 }
1452
1453 #[test]
1456 fn nested_shallow_strings_vitest_reporters() {
1457 let source = r#"
1458 export default {
1459 test: {
1460 reporters: ["default", "vitest-sonar-reporter"]
1461 }
1462 };
1463 "#;
1464 let values =
1465 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1466 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
1467 }
1468
1469 #[test]
1470 fn nested_shallow_strings_tuple_format() {
1471 let source = r#"
1472 export default {
1473 test: {
1474 reporters: ["default", ["vitest-sonar-reporter", { outputFile: "report.xml" }]]
1475 }
1476 };
1477 "#;
1478 let values =
1479 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1480 assert_eq!(values, vec!["default", "vitest-sonar-reporter"]);
1481 }
1482
1483 #[test]
1484 fn nested_shallow_strings_missing_outer() {
1485 let source = r"export default { other: {} };";
1486 let values =
1487 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1488 assert!(values.is_empty());
1489 }
1490
1491 #[test]
1492 fn nested_shallow_strings_missing_inner() {
1493 let source = r#"export default { test: { include: ["**/*.test.ts"] } };"#;
1494 let values =
1495 extract_config_nested_shallow_strings(source, &js_path(), &["test"], "reporters");
1496 assert!(values.is_empty());
1497 }
1498
1499 #[test]
1502 fn string_or_array_missing_path() {
1503 let source = r"export default {};";
1504 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1505 assert!(result.is_empty());
1506 }
1507
1508 #[test]
1509 fn string_or_array_non_string_values() {
1510 let source = r"export default { entry: [42, true] };";
1512 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1513 assert!(result.is_empty());
1514 }
1515
1516 #[test]
1519 fn array_nested_extraction() {
1520 let source = r#"
1521 export default defineConfig({
1522 test: {
1523 projects: [
1524 {
1525 test: {
1526 setupFiles: ["./test/setup-a.ts"]
1527 }
1528 },
1529 {
1530 test: {
1531 setupFiles: "./test/setup-b.ts"
1532 }
1533 }
1534 ]
1535 }
1536 });
1537 "#;
1538 let results = extract_config_array_nested_string_or_array(
1539 source,
1540 &ts_path(),
1541 &["test", "projects"],
1542 &["test", "setupFiles"],
1543 );
1544 assert!(results.contains(&"./test/setup-a.ts".to_string()));
1545 assert!(results.contains(&"./test/setup-b.ts".to_string()));
1546 }
1547
1548 #[test]
1549 fn array_nested_empty_when_no_array() {
1550 let source = r#"export default { test: { projects: "not-an-array" } };"#;
1551 let results = extract_config_array_nested_string_or_array(
1552 source,
1553 &js_path(),
1554 &["test", "projects"],
1555 &["test", "setupFiles"],
1556 );
1557 assert!(results.is_empty());
1558 }
1559
1560 #[test]
1563 fn object_nested_extraction() {
1564 let source = r#"{
1565 "projects": {
1566 "app-one": {
1567 "architect": {
1568 "build": {
1569 "options": {
1570 "styles": ["src/styles.css"]
1571 }
1572 }
1573 }
1574 }
1575 }
1576 }"#;
1577 let results = extract_config_object_nested_string_or_array(
1578 source,
1579 &json_path(),
1580 &["projects"],
1581 &["architect", "build", "options", "styles"],
1582 );
1583 assert_eq!(results, vec!["src/styles.css"]);
1584 }
1585
1586 #[test]
1589 fn object_nested_strings_extraction() {
1590 let source = r#"{
1591 "targets": {
1592 "build": {
1593 "executor": "@angular/build:application"
1594 },
1595 "test": {
1596 "executor": "@nx/vite:test"
1597 }
1598 }
1599 }"#;
1600 let results =
1601 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
1602 assert!(results.contains(&"@angular/build:application".to_string()));
1603 assert!(results.contains(&"@nx/vite:test".to_string()));
1604 }
1605
1606 #[test]
1609 fn require_strings_direct_call() {
1610 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
1611 let deps = extract_config_require_strings(source, &js_path(), "adapter");
1612 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
1613 }
1614
1615 #[test]
1616 fn require_strings_no_matching_key() {
1617 let source = r"module.exports = { other: require('something') };";
1618 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1619 assert!(deps.is_empty());
1620 }
1621
1622 #[test]
1625 fn extract_imports_no_imports() {
1626 let source = r"export default {};";
1627 let imports = extract_imports(source, &js_path());
1628 assert!(imports.is_empty());
1629 }
1630
1631 #[test]
1632 fn extract_imports_side_effect_import() {
1633 let source = r"
1634 import 'polyfill';
1635 import './local-setup';
1636 export default {};
1637 ";
1638 let imports = extract_imports(source, &js_path());
1639 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
1640 }
1641
1642 #[test]
1643 fn extract_imports_mixed_specifiers() {
1644 let source = r"
1645 import defaultExport from 'module-a';
1646 import { named } from 'module-b';
1647 import * as ns from 'module-c';
1648 export default {};
1649 ";
1650 let imports = extract_imports(source, &js_path());
1651 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
1652 }
1653
1654 #[test]
1657 fn template_literal_in_string_or_array() {
1658 let source = r"export default { entry: `./src/index.ts` };";
1659 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
1660 assert_eq!(result, vec!["./src/index.ts"]);
1661 }
1662
1663 #[test]
1664 fn template_literal_in_config_string() {
1665 let source = r"export default { testDir: `./tests` };";
1666 let val = extract_config_string(source, &js_path(), &["testDir"]);
1667 assert_eq!(val, Some("./tests".to_string()));
1668 }
1669
1670 #[test]
1673 fn nested_string_array_empty_path() {
1674 let source = r#"export default { items: ["a", "b"] };"#;
1675 let result = extract_config_string_array(source, &js_path(), &[]);
1676 assert!(result.is_empty());
1677 }
1678
1679 #[test]
1680 fn nested_string_empty_path() {
1681 let source = r#"export default { key: "val" };"#;
1682 let result = extract_config_string(source, &js_path(), &[]);
1683 assert!(result.is_none());
1684 }
1685
1686 #[test]
1687 fn object_keys_empty_path() {
1688 let source = r"export default { plugins: {} };";
1689 let result = extract_config_object_keys(source, &js_path(), &[]);
1690 assert!(result.is_empty());
1691 }
1692
1693 #[test]
1696 fn no_config_object_returns_empty() {
1697 let source = r"const x = 42;";
1699 let result = extract_config_string(source, &js_path(), &["key"]);
1700 assert!(result.is_none());
1701
1702 let arr = extract_config_string_array(source, &js_path(), &["items"]);
1703 assert!(arr.is_empty());
1704
1705 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1706 assert!(keys.is_empty());
1707 }
1708
1709 #[test]
1712 fn property_with_string_key() {
1713 let source = r#"export default { "string-key": "value" };"#;
1714 let val = extract_config_string(source, &js_path(), &["string-key"]);
1715 assert_eq!(val, Some("value".to_string()));
1716 }
1717
1718 #[test]
1719 fn nested_navigation_through_non_object() {
1720 let source = r#"export default { level1: "not-an-object" };"#;
1722 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
1723 assert!(val.is_none());
1724 }
1725
1726 #[test]
1729 fn variable_reference_untyped() {
1730 let source = r#"
1731 const config = {
1732 testDir: "./tests"
1733 };
1734 export default config;
1735 "#;
1736 let val = extract_config_string(source, &js_path(), &["testDir"]);
1737 assert_eq!(val, Some("./tests".to_string()));
1738 }
1739
1740 #[test]
1741 fn variable_reference_with_type_annotation() {
1742 let source = r#"
1743 import type { StorybookConfig } from '@storybook/react-vite';
1744 const config: StorybookConfig = {
1745 addons: ["@storybook/addon-a11y", "@storybook/addon-docs"],
1746 framework: "@storybook/react-vite"
1747 };
1748 export default config;
1749 "#;
1750 let addons = extract_config_shallow_strings(source, &ts_path(), "addons");
1751 assert_eq!(
1752 addons,
1753 vec!["@storybook/addon-a11y", "@storybook/addon-docs"]
1754 );
1755
1756 let framework = extract_config_string(source, &ts_path(), &["framework"]);
1757 assert_eq!(framework, Some("@storybook/react-vite".to_string()));
1758 }
1759
1760 #[test]
1761 fn variable_reference_with_define_config() {
1762 let source = r#"
1763 import { defineConfig } from 'vitest/config';
1764 const config = defineConfig({
1765 test: {
1766 include: ["**/*.test.ts"]
1767 }
1768 });
1769 export default config;
1770 "#;
1771 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
1772 assert_eq!(include, vec!["**/*.test.ts"]);
1773 }
1774
1775 #[test]
1778 fn ts_satisfies_direct_export() {
1779 let source = r#"
1780 export default {
1781 testDir: "./tests"
1782 } satisfies PlaywrightTestConfig;
1783 "#;
1784 let val = extract_config_string(source, &ts_path(), &["testDir"]);
1785 assert_eq!(val, Some("./tests".to_string()));
1786 }
1787
1788 #[test]
1789 fn ts_as_direct_export() {
1790 let source = r#"
1791 export default {
1792 testDir: "./tests"
1793 } as const;
1794 "#;
1795 let val = extract_config_string(source, &ts_path(), &["testDir"]);
1796 assert_eq!(val, Some("./tests".to_string()));
1797 }
1798}