1use std::path::Path;
15
16use oxc_allocator::Allocator;
17use oxc_ast::ast::*;
18use oxc_parser::Parser;
19use oxc_span::SourceType;
20
21pub fn extract_imports(source: &str, path: &Path) -> Vec<String> {
23 extract_from_source(source, path, |program| {
24 let mut sources = Vec::new();
25 for stmt in &program.body {
26 if let Statement::ImportDeclaration(decl) = stmt {
27 sources.push(decl.source.value.to_string());
28 }
29 }
30 Some(sources)
31 })
32 .unwrap_or_default()
33}
34
35pub fn extract_config_string_array(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
37 extract_from_source(source, path, |program| {
38 let obj = find_config_object(program)?;
39 get_nested_string_array_from_object(obj, prop_path)
40 })
41 .unwrap_or_default()
42}
43
44pub fn extract_config_string(source: &str, path: &Path, prop_path: &[&str]) -> Option<String> {
46 extract_from_source(source, path, |program| {
47 let obj = find_config_object(program)?;
48 get_nested_string_from_object(obj, prop_path)
49 })
50}
51
52pub fn extract_config_property_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
59 extract_from_source(source, path, |program| {
60 let obj = find_config_object(program)?;
61 let mut values = Vec::new();
62 if let Some(prop) = find_property(obj, key) {
63 collect_all_string_values(&prop.value, &mut values);
64 }
65 Some(values)
66 })
67 .unwrap_or_default()
68}
69
70pub fn extract_config_shallow_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
77 extract_from_source(source, path, |program| {
78 let obj = find_config_object(program)?;
79 let prop = find_property(obj, key)?;
80 Some(collect_shallow_string_values(&prop.value))
81 })
82 .unwrap_or_default()
83}
84
85pub fn find_config_object_pub<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
87 find_config_object(program)
88}
89
90pub fn extract_config_object_keys(source: &str, path: &Path, prop_path: &[&str]) -> Vec<String> {
95 extract_from_source(source, path, |program| {
96 let obj = find_config_object(program)?;
97 get_nested_object_keys(obj, prop_path)
98 })
99 .unwrap_or_default()
100}
101
102pub fn extract_config_string_or_array(
109 source: &str,
110 path: &Path,
111 prop_path: &[&str],
112) -> Vec<String> {
113 extract_from_source(source, path, |program| {
114 let obj = find_config_object(program)?;
115 get_nested_string_or_array(obj, prop_path)
116 })
117 .unwrap_or_default()
118}
119
120pub fn extract_config_array_nested_string_or_array(
127 source: &str,
128 path: &Path,
129 array_path: &[&str],
130 inner_path: &[&str],
131) -> Vec<String> {
132 extract_from_source(source, path, |program| {
133 let obj = find_config_object(program)?;
134 let array_expr = get_nested_expression(obj, array_path)?;
135 let Expression::ArrayExpression(arr) = array_expr else {
136 return None;
137 };
138 let mut results = Vec::new();
139 for element in &arr.elements {
140 if let Some(Expression::ObjectExpression(element_obj)) = element.as_expression()
141 && let Some(values) = get_nested_string_or_array(element_obj, inner_path)
142 {
143 results.extend(values);
144 }
145 }
146 if results.is_empty() {
147 None
148 } else {
149 Some(results)
150 }
151 })
152 .unwrap_or_default()
153}
154
155pub fn extract_config_object_nested_string_or_array(
162 source: &str,
163 path: &Path,
164 object_path: &[&str],
165 inner_path: &[&str],
166) -> Vec<String> {
167 extract_config_object_nested(source, path, object_path, |value_obj| {
168 get_nested_string_or_array(value_obj, inner_path)
169 })
170}
171
172pub fn extract_config_object_nested_strings(
177 source: &str,
178 path: &Path,
179 object_path: &[&str],
180 inner_path: &[&str],
181) -> Vec<String> {
182 extract_config_object_nested(source, path, object_path, |value_obj| {
183 get_nested_string_from_object(value_obj, inner_path).map(|s| vec![s])
184 })
185}
186
187fn extract_config_object_nested(
192 source: &str,
193 path: &Path,
194 object_path: &[&str],
195 extract_fn: impl Fn(&ObjectExpression<'_>) -> Option<Vec<String>>,
196) -> Vec<String> {
197 extract_from_source(source, path, |program| {
198 let obj = find_config_object(program)?;
199 let obj_expr = get_nested_expression(obj, object_path)?;
200 let Expression::ObjectExpression(target_obj) = obj_expr else {
201 return None;
202 };
203 let mut results = Vec::new();
204 for prop in &target_obj.properties {
205 if let ObjectPropertyKind::ObjectProperty(p) = prop
206 && let Expression::ObjectExpression(value_obj) = &p.value
207 && let Some(values) = extract_fn(value_obj)
208 {
209 results.extend(values);
210 }
211 }
212 if results.is_empty() {
213 None
214 } else {
215 Some(results)
216 }
217 })
218 .unwrap_or_default()
219}
220
221pub fn extract_config_require_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
227 extract_from_source(source, path, |program| {
228 let obj = find_config_object(program)?;
229 let prop = find_property(obj, key)?;
230 Some(collect_require_sources(&prop.value))
231 })
232 .unwrap_or_default()
233}
234
235fn extract_from_source<T>(
244 source: &str,
245 path: &Path,
246 extractor: impl FnOnce(&Program) -> Option<T>,
247) -> Option<T> {
248 let source_type = SourceType::from_path(path).unwrap_or_default();
249 let alloc = Allocator::default();
250
251 let is_json = path
254 .extension()
255 .is_some_and(|ext| ext == "json" || ext == "jsonc");
256 if is_json {
257 let wrapped = format!("({source})");
258 let parsed = Parser::new(&alloc, &wrapped, SourceType::mjs()).parse();
259 return extractor(&parsed.program);
260 }
261
262 let parsed = Parser::new(&alloc, source, source_type).parse();
263 extractor(&parsed.program)
264}
265
266fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
275 for stmt in &program.body {
276 match stmt {
277 Statement::ExportDefaultDeclaration(decl) => {
279 let expr: Option<&Expression> = match &decl.declaration {
281 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
282 return Some(obj);
283 }
284 ExportDefaultDeclarationKind::CallExpression(_)
285 | ExportDefaultDeclarationKind::ParenthesizedExpression(_) => {
286 decl.declaration.as_expression()
288 }
289 _ => None,
290 };
291 if let Some(expr) = expr {
292 return extract_object_from_expression(expr);
293 }
294 }
295 Statement::ExpressionStatement(expr_stmt) => {
297 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
298 && is_module_exports_target(&assign.left)
299 {
300 return extract_object_from_expression(&assign.right);
301 }
302 }
303 _ => {}
304 }
305 }
306
307 if program.body.len() == 1
310 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
311 {
312 match &expr_stmt.expression {
313 Expression::ObjectExpression(obj) => return Some(obj),
314 Expression::ParenthesizedExpression(paren) => {
315 if let Expression::ObjectExpression(obj) = &paren.expression {
316 return Some(obj);
317 }
318 }
319 _ => {}
320 }
321 }
322
323 None
324}
325
326fn extract_object_from_expression<'a>(
328 expr: &'a Expression<'a>,
329) -> Option<&'a ObjectExpression<'a>> {
330 match expr {
331 Expression::ObjectExpression(obj) => Some(obj),
333 Expression::CallExpression(call) => {
335 for arg in &call.arguments {
337 match arg {
338 Argument::ObjectExpression(obj) => return Some(obj),
339 Argument::ArrowFunctionExpression(arrow) => {
341 if arrow.expression
342 && !arrow.body.statements.is_empty()
343 && let Statement::ExpressionStatement(expr_stmt) =
344 &arrow.body.statements[0]
345 {
346 return extract_object_from_expression(&expr_stmt.expression);
347 }
348 }
349 _ => {}
350 }
351 }
352 None
353 }
354 Expression::ParenthesizedExpression(paren) => {
356 extract_object_from_expression(&paren.expression)
357 }
358 _ => None,
359 }
360}
361
362fn is_module_exports_target(target: &AssignmentTarget) -> bool {
364 if let AssignmentTarget::StaticMemberExpression(member) = target
365 && let Expression::Identifier(obj) = &member.object
366 {
367 return obj.name == "module" && member.property.name == "exports";
368 }
369 false
370}
371
372fn find_property<'a>(obj: &'a ObjectExpression<'a>, key: &str) -> Option<&'a ObjectProperty<'a>> {
374 for prop in &obj.properties {
375 if let ObjectPropertyKind::ObjectProperty(p) = prop
376 && property_key_matches(&p.key, key)
377 {
378 return Some(p);
379 }
380 }
381 None
382}
383
384fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
386 match key {
387 PropertyKey::StaticIdentifier(id) => id.name == name,
388 PropertyKey::StringLiteral(s) => s.value == name,
389 _ => false,
390 }
391}
392
393fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
395 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
396}
397
398fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
400 find_property(obj, key)
401 .map(|p| expression_to_string_array(&p.value))
402 .unwrap_or_default()
403}
404
405fn get_nested_string_array_from_object(
407 obj: &ObjectExpression,
408 path: &[&str],
409) -> Option<Vec<String>> {
410 if path.is_empty() {
411 return None;
412 }
413 if path.len() == 1 {
414 return Some(get_object_string_array_property(obj, path[0]));
415 }
416 let prop = find_property(obj, path[0])?;
418 if let Expression::ObjectExpression(nested) = &prop.value {
419 get_nested_string_array_from_object(nested, &path[1..])
420 } else {
421 None
422 }
423}
424
425fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
427 if path.is_empty() {
428 return None;
429 }
430 if path.len() == 1 {
431 return get_object_string_property(obj, path[0]);
432 }
433 let prop = find_property(obj, path[0])?;
434 if let Expression::ObjectExpression(nested) = &prop.value {
435 get_nested_string_from_object(nested, &path[1..])
436 } else {
437 None
438 }
439}
440
441fn expression_to_string(expr: &Expression) -> Option<String> {
443 match expr {
444 Expression::StringLiteral(s) => Some(s.value.to_string()),
445 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
446 t.quasis.first().map(|q| q.value.raw.to_string())
448 }
449 _ => None,
450 }
451}
452
453fn expression_to_string_array(expr: &Expression) -> Vec<String> {
455 match expr {
456 Expression::ArrayExpression(arr) => arr
457 .elements
458 .iter()
459 .filter_map(|el| match el {
460 ArrayExpressionElement::SpreadElement(_) => None,
461 _ => el.as_expression().and_then(expression_to_string),
462 })
463 .collect(),
464 _ => vec![],
465 }
466}
467
468fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
473 let mut values = Vec::new();
474 match expr {
475 Expression::StringLiteral(s) => {
476 values.push(s.value.to_string());
477 }
478 Expression::ArrayExpression(arr) => {
479 for el in &arr.elements {
480 if let Some(inner) = el.as_expression() {
481 match inner {
482 Expression::StringLiteral(s) => {
483 values.push(s.value.to_string());
484 }
485 Expression::ArrayExpression(sub_arr) => {
487 if let Some(first) = sub_arr.elements.first()
488 && let Some(first_expr) = first.as_expression()
489 && let Some(s) = expression_to_string(first_expr)
490 {
491 values.push(s);
492 }
493 }
494 _ => {}
495 }
496 }
497 }
498 }
499 _ => {}
500 }
501 values
502}
503
504fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
506 match expr {
507 Expression::StringLiteral(s) => {
508 values.push(s.value.to_string());
509 }
510 Expression::ArrayExpression(arr) => {
511 for el in &arr.elements {
512 if let Some(expr) = el.as_expression() {
513 collect_all_string_values(expr, values);
514 }
515 }
516 }
517 Expression::ObjectExpression(obj) => {
518 for prop in &obj.properties {
519 if let ObjectPropertyKind::ObjectProperty(p) = prop {
520 collect_all_string_values(&p.value, values);
521 }
522 }
523 }
524 _ => {}
525 }
526}
527
528fn property_key_to_string(key: &PropertyKey) -> Option<String> {
530 match key {
531 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
532 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
533 _ => None,
534 }
535}
536
537fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
539 if path.is_empty() {
540 return None;
541 }
542 let prop = find_property(obj, path[0])?;
543 if path.len() == 1 {
544 if let Expression::ObjectExpression(nested) = &prop.value {
545 let keys = nested
546 .properties
547 .iter()
548 .filter_map(|p| {
549 if let ObjectPropertyKind::ObjectProperty(p) = p {
550 property_key_to_string(&p.key)
551 } else {
552 None
553 }
554 })
555 .collect();
556 return Some(keys);
557 }
558 return None;
559 }
560 if let Expression::ObjectExpression(nested) = &prop.value {
561 get_nested_object_keys(nested, &path[1..])
562 } else {
563 None
564 }
565}
566
567fn get_nested_expression<'a>(
569 obj: &'a ObjectExpression<'a>,
570 path: &[&str],
571) -> Option<&'a Expression<'a>> {
572 if path.is_empty() {
573 return None;
574 }
575 let prop = find_property(obj, path[0])?;
576 if path.len() == 1 {
577 return Some(&prop.value);
578 }
579 if let Expression::ObjectExpression(nested) = &prop.value {
580 get_nested_expression(nested, &path[1..])
581 } else {
582 None
583 }
584}
585
586fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
588 if path.is_empty() {
589 return None;
590 }
591 if path.len() == 1 {
592 let prop = find_property(obj, path[0])?;
593 return Some(expression_to_string_or_array(&prop.value));
594 }
595 let prop = find_property(obj, path[0])?;
596 if let Expression::ObjectExpression(nested) = &prop.value {
597 get_nested_string_or_array(nested, &path[1..])
598 } else {
599 None
600 }
601}
602
603fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
605 match expr {
606 Expression::StringLiteral(s) => vec![s.value.to_string()],
607 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
608 .quasis
609 .first()
610 .map(|q| vec![q.value.raw.to_string()])
611 .unwrap_or_default(),
612 Expression::ArrayExpression(arr) => arr
613 .elements
614 .iter()
615 .filter_map(|el| el.as_expression().and_then(expression_to_string))
616 .collect(),
617 Expression::ObjectExpression(obj) => obj
618 .properties
619 .iter()
620 .filter_map(|p| {
621 if let ObjectPropertyKind::ObjectProperty(p) = p {
622 expression_to_string(&p.value)
623 } else {
624 None
625 }
626 })
627 .collect(),
628 _ => vec![],
629 }
630}
631
632fn collect_require_sources(expr: &Expression) -> Vec<String> {
634 let mut sources = Vec::new();
635 match expr {
636 Expression::CallExpression(call) if is_require_call(call) => {
637 if let Some(s) = get_require_source(call) {
638 sources.push(s);
639 }
640 }
641 Expression::ArrayExpression(arr) => {
642 for el in &arr.elements {
643 if let Some(inner) = el.as_expression() {
644 match inner {
645 Expression::CallExpression(call) if is_require_call(call) => {
646 if let Some(s) = get_require_source(call) {
647 sources.push(s);
648 }
649 }
650 Expression::ArrayExpression(sub_arr) => {
652 if let Some(first) = sub_arr.elements.first()
653 && let Some(Expression::CallExpression(call)) =
654 first.as_expression()
655 && is_require_call(call)
656 && let Some(s) = get_require_source(call)
657 {
658 sources.push(s);
659 }
660 }
661 _ => {}
662 }
663 }
664 }
665 }
666 _ => {}
667 }
668 sources
669}
670
671fn is_require_call(call: &CallExpression) -> bool {
673 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
674}
675
676fn get_require_source(call: &CallExpression) -> Option<String> {
678 call.arguments.first().and_then(|arg| {
679 if let Argument::StringLiteral(s) = arg {
680 Some(s.value.to_string())
681 } else {
682 None
683 }
684 })
685}
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690 use std::path::PathBuf;
691
692 fn js_path() -> PathBuf {
693 PathBuf::from("config.js")
694 }
695
696 fn ts_path() -> PathBuf {
697 PathBuf::from("config.ts")
698 }
699
700 #[test]
701 fn extract_imports_basic() {
702 let source = r"
703 import foo from 'foo-pkg';
704 import { bar } from '@scope/bar';
705 export default {};
706 ";
707 let imports = extract_imports(source, &js_path());
708 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
709 }
710
711 #[test]
712 fn extract_default_export_object_property() {
713 let source = r#"export default { testDir: "./tests" };"#;
714 let val = extract_config_string(source, &js_path(), &["testDir"]);
715 assert_eq!(val, Some("./tests".to_string()));
716 }
717
718 #[test]
719 fn extract_define_config_property() {
720 let source = r#"
721 import { defineConfig } from 'vitest/config';
722 export default defineConfig({
723 test: {
724 include: ["**/*.test.ts", "**/*.spec.ts"],
725 setupFiles: ["./test/setup.ts"]
726 }
727 });
728 "#;
729 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
730 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
731
732 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
733 assert_eq!(setup, vec!["./test/setup.ts"]);
734 }
735
736 #[test]
737 fn extract_module_exports_property() {
738 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
739 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
740 assert_eq!(val, Some("jsdom".to_string()));
741 }
742
743 #[test]
744 fn extract_nested_string_array() {
745 let source = r#"
746 export default {
747 resolve: {
748 alias: {
749 "@": "./src"
750 }
751 },
752 test: {
753 include: ["src/**/*.test.ts"]
754 }
755 };
756 "#;
757 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
758 assert_eq!(include, vec!["src/**/*.test.ts"]);
759 }
760
761 #[test]
762 fn extract_addons_array() {
763 let source = r#"
764 export default {
765 addons: [
766 "@storybook/addon-a11y",
767 "@storybook/addon-docs",
768 "@storybook/addon-links"
769 ]
770 };
771 "#;
772 let addons = extract_config_property_strings(source, &ts_path(), "addons");
773 assert_eq!(
774 addons,
775 vec![
776 "@storybook/addon-a11y",
777 "@storybook/addon-docs",
778 "@storybook/addon-links"
779 ]
780 );
781 }
782
783 #[test]
784 fn handle_empty_config() {
785 let source = "";
786 let result = extract_config_string(source, &js_path(), &["key"]);
787 assert_eq!(result, None);
788 }
789
790 #[test]
793 fn object_keys_postcss_plugins() {
794 let source = r"
795 module.exports = {
796 plugins: {
797 autoprefixer: {},
798 tailwindcss: {},
799 'postcss-import': {}
800 }
801 };
802 ";
803 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
804 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
805 }
806
807 #[test]
808 fn object_keys_nested_path() {
809 let source = r"
810 export default {
811 build: {
812 plugins: {
813 minify: {},
814 compress: {}
815 }
816 }
817 };
818 ";
819 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
820 assert_eq!(keys, vec!["minify", "compress"]);
821 }
822
823 #[test]
824 fn object_keys_empty_object() {
825 let source = r"export default { plugins: {} };";
826 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
827 assert!(keys.is_empty());
828 }
829
830 #[test]
831 fn object_keys_non_object_returns_empty() {
832 let source = r#"export default { plugins: ["a", "b"] };"#;
833 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
834 assert!(keys.is_empty());
835 }
836
837 #[test]
840 fn string_or_array_single_string() {
841 let source = r#"export default { entry: "./src/index.js" };"#;
842 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
843 assert_eq!(result, vec!["./src/index.js"]);
844 }
845
846 #[test]
847 fn string_or_array_array() {
848 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
849 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
850 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
851 }
852
853 #[test]
854 fn string_or_array_object_values() {
855 let source =
856 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
857 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
858 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
859 }
860
861 #[test]
862 fn string_or_array_nested_path() {
863 let source = r#"
864 export default {
865 build: {
866 rollupOptions: {
867 input: ["./index.html", "./about.html"]
868 }
869 }
870 };
871 "#;
872 let result = extract_config_string_or_array(
873 source,
874 &js_path(),
875 &["build", "rollupOptions", "input"],
876 );
877 assert_eq!(result, vec!["./index.html", "./about.html"]);
878 }
879
880 #[test]
881 fn string_or_array_template_literal() {
882 let source = r"export default { entry: `./src/index.js` };";
883 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
884 assert_eq!(result, vec!["./src/index.js"]);
885 }
886
887 #[test]
890 fn require_strings_array() {
891 let source = r"
892 module.exports = {
893 plugins: [
894 require('autoprefixer'),
895 require('postcss-import')
896 ]
897 };
898 ";
899 let deps = extract_config_require_strings(source, &js_path(), "plugins");
900 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
901 }
902
903 #[test]
904 fn require_strings_with_tuples() {
905 let source = r"
906 module.exports = {
907 plugins: [
908 require('autoprefixer'),
909 [require('postcss-preset-env'), { stage: 3 }]
910 ]
911 };
912 ";
913 let deps = extract_config_require_strings(source, &js_path(), "plugins");
914 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
915 }
916
917 #[test]
918 fn require_strings_empty_array() {
919 let source = r"module.exports = { plugins: [] };";
920 let deps = extract_config_require_strings(source, &js_path(), "plugins");
921 assert!(deps.is_empty());
922 }
923
924 #[test]
925 fn require_strings_no_require_calls() {
926 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
927 let deps = extract_config_require_strings(source, &js_path(), "plugins");
928 assert!(deps.is_empty());
929 }
930
931 #[test]
934 fn json_wrapped_in_parens_string() {
935 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
936 let val = extract_config_string(source, &js_path(), &["extends"]);
937 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
938 }
939
940 #[test]
941 fn json_wrapped_in_parens_nested_array() {
942 let source =
943 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
944 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
945 assert_eq!(types, vec!["node", "jest"]);
946
947 let include = extract_config_string_array(source, &js_path(), &["include"]);
948 assert_eq!(include, vec!["src/**/*"]);
949 }
950
951 #[test]
952 fn json_wrapped_in_parens_object_keys() {
953 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
954 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
955 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
956 }
957
958 fn json_path() -> PathBuf {
961 PathBuf::from("config.json")
962 }
963
964 #[test]
965 fn json_file_parsed_correctly() {
966 let source = r#"{"key": "value", "list": ["a", "b"]}"#;
967 let val = extract_config_string(source, &json_path(), &["key"]);
968 assert_eq!(val, Some("value".to_string()));
969
970 let list = extract_config_string_array(source, &json_path(), &["list"]);
971 assert_eq!(list, vec!["a", "b"]);
972 }
973
974 #[test]
975 fn jsonc_file_parsed_correctly() {
976 let source = r#"{"key": "value"}"#;
977 let path = PathBuf::from("tsconfig.jsonc");
978 let val = extract_config_string(source, &path, &["key"]);
979 assert_eq!(val, Some("value".to_string()));
980 }
981
982 #[test]
985 fn extract_define_config_arrow_function() {
986 let source = r#"
987 import { defineConfig } from 'vite';
988 export default defineConfig(() => ({
989 test: {
990 include: ["**/*.test.ts"]
991 }
992 }));
993 "#;
994 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
995 assert_eq!(include, vec!["**/*.test.ts"]);
996 }
997
998 #[test]
1001 fn module_exports_nested_string() {
1002 let source = r#"
1003 module.exports = {
1004 resolve: {
1005 alias: {
1006 "@": "./src"
1007 }
1008 }
1009 };
1010 "#;
1011 let val = extract_config_string(source, &js_path(), &["resolve", "alias", "@"]);
1012 assert_eq!(val, Some("./src".to_string()));
1013 }
1014
1015 #[test]
1018 fn property_strings_nested_objects() {
1019 let source = r#"
1020 export default {
1021 plugins: {
1022 group1: { a: "val-a" },
1023 group2: { b: "val-b" }
1024 }
1025 };
1026 "#;
1027 let values = extract_config_property_strings(source, &js_path(), "plugins");
1028 assert!(values.contains(&"val-a".to_string()));
1029 assert!(values.contains(&"val-b".to_string()));
1030 }
1031
1032 #[test]
1033 fn property_strings_missing_key_returns_empty() {
1034 let source = r#"export default { other: "value" };"#;
1035 let values = extract_config_property_strings(source, &js_path(), "missing");
1036 assert!(values.is_empty());
1037 }
1038
1039 #[test]
1042 fn shallow_strings_tuple_array() {
1043 let source = r#"
1044 module.exports = {
1045 reporters: ["default", ["jest-junit", { outputDirectory: "reports" }]]
1046 };
1047 "#;
1048 let values = extract_config_shallow_strings(source, &js_path(), "reporters");
1049 assert_eq!(values, vec!["default", "jest-junit"]);
1050 assert!(!values.contains(&"reports".to_string()));
1052 }
1053
1054 #[test]
1055 fn shallow_strings_single_string() {
1056 let source = r#"export default { preset: "ts-jest" };"#;
1057 let values = extract_config_shallow_strings(source, &js_path(), "preset");
1058 assert_eq!(values, vec!["ts-jest"]);
1059 }
1060
1061 #[test]
1062 fn shallow_strings_missing_key() {
1063 let source = r#"export default { other: "val" };"#;
1064 let values = extract_config_shallow_strings(source, &js_path(), "missing");
1065 assert!(values.is_empty());
1066 }
1067
1068 #[test]
1071 fn string_or_array_missing_path() {
1072 let source = r"export default {};";
1073 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1074 assert!(result.is_empty());
1075 }
1076
1077 #[test]
1078 fn string_or_array_non_string_values() {
1079 let source = r"export default { entry: [42, true] };";
1081 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
1082 assert!(result.is_empty());
1083 }
1084
1085 #[test]
1088 fn array_nested_extraction() {
1089 let source = r#"
1090 export default defineConfig({
1091 test: {
1092 projects: [
1093 {
1094 test: {
1095 setupFiles: ["./test/setup-a.ts"]
1096 }
1097 },
1098 {
1099 test: {
1100 setupFiles: "./test/setup-b.ts"
1101 }
1102 }
1103 ]
1104 }
1105 });
1106 "#;
1107 let results = extract_config_array_nested_string_or_array(
1108 source,
1109 &ts_path(),
1110 &["test", "projects"],
1111 &["test", "setupFiles"],
1112 );
1113 assert!(results.contains(&"./test/setup-a.ts".to_string()));
1114 assert!(results.contains(&"./test/setup-b.ts".to_string()));
1115 }
1116
1117 #[test]
1118 fn array_nested_empty_when_no_array() {
1119 let source = r#"export default { test: { projects: "not-an-array" } };"#;
1120 let results = extract_config_array_nested_string_or_array(
1121 source,
1122 &js_path(),
1123 &["test", "projects"],
1124 &["test", "setupFiles"],
1125 );
1126 assert!(results.is_empty());
1127 }
1128
1129 #[test]
1132 fn object_nested_extraction() {
1133 let source = r#"{
1134 "projects": {
1135 "app-one": {
1136 "architect": {
1137 "build": {
1138 "options": {
1139 "styles": ["src/styles.css"]
1140 }
1141 }
1142 }
1143 }
1144 }
1145 }"#;
1146 let results = extract_config_object_nested_string_or_array(
1147 source,
1148 &json_path(),
1149 &["projects"],
1150 &["architect", "build", "options", "styles"],
1151 );
1152 assert_eq!(results, vec!["src/styles.css"]);
1153 }
1154
1155 #[test]
1158 fn object_nested_strings_extraction() {
1159 let source = r#"{
1160 "targets": {
1161 "build": {
1162 "executor": "@angular/build:application"
1163 },
1164 "test": {
1165 "executor": "@nx/vite:test"
1166 }
1167 }
1168 }"#;
1169 let results =
1170 extract_config_object_nested_strings(source, &json_path(), &["targets"], &["executor"]);
1171 assert!(results.contains(&"@angular/build:application".to_string()));
1172 assert!(results.contains(&"@nx/vite:test".to_string()));
1173 }
1174
1175 #[test]
1178 fn require_strings_direct_call() {
1179 let source = r"module.exports = { adapter: require('@sveltejs/adapter-node') };";
1180 let deps = extract_config_require_strings(source, &js_path(), "adapter");
1181 assert_eq!(deps, vec!["@sveltejs/adapter-node"]);
1182 }
1183
1184 #[test]
1185 fn require_strings_no_matching_key() {
1186 let source = r"module.exports = { other: require('something') };";
1187 let deps = extract_config_require_strings(source, &js_path(), "plugins");
1188 assert!(deps.is_empty());
1189 }
1190
1191 #[test]
1194 fn extract_imports_no_imports() {
1195 let source = r"export default {};";
1196 let imports = extract_imports(source, &js_path());
1197 assert!(imports.is_empty());
1198 }
1199
1200 #[test]
1201 fn extract_imports_side_effect_import() {
1202 let source = r"
1203 import 'polyfill';
1204 import './local-setup';
1205 export default {};
1206 ";
1207 let imports = extract_imports(source, &js_path());
1208 assert_eq!(imports, vec!["polyfill", "./local-setup"]);
1209 }
1210
1211 #[test]
1212 fn extract_imports_mixed_specifiers() {
1213 let source = r"
1214 import defaultExport from 'module-a';
1215 import { named } from 'module-b';
1216 import * as ns from 'module-c';
1217 export default {};
1218 ";
1219 let imports = extract_imports(source, &js_path());
1220 assert_eq!(imports, vec!["module-a", "module-b", "module-c"]);
1221 }
1222
1223 #[test]
1226 fn template_literal_in_string_or_array() {
1227 let source = r"export default { entry: `./src/index.ts` };";
1228 let result = extract_config_string_or_array(source, &ts_path(), &["entry"]);
1229 assert_eq!(result, vec!["./src/index.ts"]);
1230 }
1231
1232 #[test]
1233 fn template_literal_in_config_string() {
1234 let source = r"export default { testDir: `./tests` };";
1235 let val = extract_config_string(source, &js_path(), &["testDir"]);
1236 assert_eq!(val, Some("./tests".to_string()));
1237 }
1238
1239 #[test]
1242 fn nested_string_array_empty_path() {
1243 let source = r#"export default { items: ["a", "b"] };"#;
1244 let result = extract_config_string_array(source, &js_path(), &[]);
1245 assert!(result.is_empty());
1246 }
1247
1248 #[test]
1249 fn nested_string_empty_path() {
1250 let source = r#"export default { key: "val" };"#;
1251 let result = extract_config_string(source, &js_path(), &[]);
1252 assert!(result.is_none());
1253 }
1254
1255 #[test]
1256 fn object_keys_empty_path() {
1257 let source = r"export default { plugins: {} };";
1258 let result = extract_config_object_keys(source, &js_path(), &[]);
1259 assert!(result.is_empty());
1260 }
1261
1262 #[test]
1265 fn no_config_object_returns_empty() {
1266 let source = r"const x = 42;";
1268 let result = extract_config_string(source, &js_path(), &["key"]);
1269 assert!(result.is_none());
1270
1271 let arr = extract_config_string_array(source, &js_path(), &["items"]);
1272 assert!(arr.is_empty());
1273
1274 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
1275 assert!(keys.is_empty());
1276 }
1277
1278 #[test]
1281 fn property_with_string_key() {
1282 let source = r#"export default { "string-key": "value" };"#;
1283 let val = extract_config_string(source, &js_path(), &["string-key"]);
1284 assert_eq!(val, Some("value".to_string()));
1285 }
1286
1287 #[test]
1288 fn nested_navigation_through_non_object() {
1289 let source = r#"export default { level1: "not-an-object" };"#;
1291 let val = extract_config_string(source, &js_path(), &["level1", "level2"]);
1292 assert!(val.is_none());
1293 }
1294}