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}