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_from_source(source, path, |program| {
168 let obj = find_config_object(program)?;
169 let obj_expr = get_nested_expression(obj, object_path)?;
170 let Expression::ObjectExpression(target_obj) = obj_expr else {
171 return None;
172 };
173 let mut results = Vec::new();
174 for prop in &target_obj.properties {
175 if let ObjectPropertyKind::ObjectProperty(p) = prop
176 && let Expression::ObjectExpression(value_obj) = &p.value
177 && let Some(values) = get_nested_string_or_array(value_obj, inner_path)
178 {
179 results.extend(values);
180 }
181 }
182 if results.is_empty() {
183 None
184 } else {
185 Some(results)
186 }
187 })
188 .unwrap_or_default()
189}
190
191pub fn extract_config_object_nested_strings(
196 source: &str,
197 path: &Path,
198 object_path: &[&str],
199 inner_path: &[&str],
200) -> Vec<String> {
201 extract_from_source(source, path, |program| {
202 let obj = find_config_object(program)?;
203 let obj_expr = get_nested_expression(obj, object_path)?;
204 let Expression::ObjectExpression(target_obj) = obj_expr else {
205 return None;
206 };
207 let mut results = Vec::new();
208 for prop in &target_obj.properties {
209 if let ObjectPropertyKind::ObjectProperty(p) = prop
210 && let Expression::ObjectExpression(value_obj) = &p.value
211 && let Some(value) = get_nested_string_from_object(value_obj, inner_path)
212 {
213 results.push(value);
214 }
215 }
216 if results.is_empty() {
217 None
218 } else {
219 Some(results)
220 }
221 })
222 .unwrap_or_default()
223}
224
225pub fn extract_config_require_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
231 extract_from_source(source, path, |program| {
232 let obj = find_config_object(program)?;
233 let prop = find_property(obj, key)?;
234 Some(collect_require_sources(&prop.value))
235 })
236 .unwrap_or_default()
237}
238
239fn extract_from_source<T>(
248 source: &str,
249 path: &Path,
250 extractor: impl FnOnce(&Program) -> Option<T>,
251) -> Option<T> {
252 let source_type = SourceType::from_path(path).unwrap_or_default();
253 let alloc = Allocator::default();
254
255 let is_json = path
258 .extension()
259 .is_some_and(|ext| ext == "json" || ext == "jsonc");
260 if is_json {
261 let wrapped = format!("({source})");
262 let parsed = Parser::new(&alloc, &wrapped, SourceType::mjs()).parse();
263 return extractor(&parsed.program);
264 }
265
266 let parsed = Parser::new(&alloc, source, source_type).parse();
267 extractor(&parsed.program)
268}
269
270fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
279 for stmt in &program.body {
280 match stmt {
281 Statement::ExportDefaultDeclaration(decl) => {
283 let expr: Option<&Expression> = match &decl.declaration {
285 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
286 return Some(obj);
287 }
288 ExportDefaultDeclarationKind::CallExpression(_)
289 | ExportDefaultDeclarationKind::ParenthesizedExpression(_) => {
290 decl.declaration.as_expression()
292 }
293 _ => None,
294 };
295 if let Some(expr) = expr {
296 return extract_object_from_expression(expr);
297 }
298 }
299 Statement::ExpressionStatement(expr_stmt) => {
301 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
302 && is_module_exports_target(&assign.left)
303 {
304 return extract_object_from_expression(&assign.right);
305 }
306 }
307 _ => {}
308 }
309 }
310
311 if program.body.len() == 1
314 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
315 {
316 match &expr_stmt.expression {
317 Expression::ObjectExpression(obj) => return Some(obj),
318 Expression::ParenthesizedExpression(paren) => {
319 if let Expression::ObjectExpression(obj) = &paren.expression {
320 return Some(obj);
321 }
322 }
323 _ => {}
324 }
325 }
326
327 None
328}
329
330fn extract_object_from_expression<'a>(
332 expr: &'a Expression<'a>,
333) -> Option<&'a ObjectExpression<'a>> {
334 match expr {
335 Expression::ObjectExpression(obj) => Some(obj),
337 Expression::CallExpression(call) => {
339 for arg in &call.arguments {
341 match arg {
342 Argument::ObjectExpression(obj) => return Some(obj),
343 Argument::ArrowFunctionExpression(arrow) => {
345 if arrow.expression
346 && !arrow.body.statements.is_empty()
347 && let Statement::ExpressionStatement(expr_stmt) =
348 &arrow.body.statements[0]
349 {
350 return extract_object_from_expression(&expr_stmt.expression);
351 }
352 }
353 _ => {}
354 }
355 }
356 None
357 }
358 Expression::ParenthesizedExpression(paren) => {
360 extract_object_from_expression(&paren.expression)
361 }
362 _ => None,
363 }
364}
365
366fn is_module_exports_target(target: &AssignmentTarget) -> bool {
368 if let AssignmentTarget::StaticMemberExpression(member) = target
369 && let Expression::Identifier(obj) = &member.object
370 {
371 return obj.name == "module" && member.property.name == "exports";
372 }
373 false
374}
375
376fn find_property<'a>(obj: &'a ObjectExpression<'a>, key: &str) -> Option<&'a ObjectProperty<'a>> {
378 for prop in &obj.properties {
379 if let ObjectPropertyKind::ObjectProperty(p) = prop
380 && property_key_matches(&p.key, key)
381 {
382 return Some(p);
383 }
384 }
385 None
386}
387
388fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
390 match key {
391 PropertyKey::StaticIdentifier(id) => id.name == name,
392 PropertyKey::StringLiteral(s) => s.value == name,
393 _ => false,
394 }
395}
396
397fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
399 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
400}
401
402fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
404 find_property(obj, key)
405 .map(|p| expression_to_string_array(&p.value))
406 .unwrap_or_default()
407}
408
409fn get_nested_string_array_from_object(
411 obj: &ObjectExpression,
412 path: &[&str],
413) -> Option<Vec<String>> {
414 if path.is_empty() {
415 return None;
416 }
417 if path.len() == 1 {
418 return Some(get_object_string_array_property(obj, path[0]));
419 }
420 let prop = find_property(obj, path[0])?;
422 if let Expression::ObjectExpression(nested) = &prop.value {
423 get_nested_string_array_from_object(nested, &path[1..])
424 } else {
425 None
426 }
427}
428
429fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
431 if path.is_empty() {
432 return None;
433 }
434 if path.len() == 1 {
435 return get_object_string_property(obj, path[0]);
436 }
437 let prop = find_property(obj, path[0])?;
438 if let Expression::ObjectExpression(nested) = &prop.value {
439 get_nested_string_from_object(nested, &path[1..])
440 } else {
441 None
442 }
443}
444
445fn expression_to_string(expr: &Expression) -> Option<String> {
447 match expr {
448 Expression::StringLiteral(s) => Some(s.value.to_string()),
449 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
450 t.quasis.first().map(|q| q.value.raw.to_string())
452 }
453 _ => None,
454 }
455}
456
457fn expression_to_string_array(expr: &Expression) -> Vec<String> {
459 match expr {
460 Expression::ArrayExpression(arr) => arr
461 .elements
462 .iter()
463 .filter_map(|el| match el {
464 ArrayExpressionElement::SpreadElement(_) => None,
465 _ => el.as_expression().and_then(expression_to_string),
466 })
467 .collect(),
468 _ => vec![],
469 }
470}
471
472fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
477 let mut values = Vec::new();
478 match expr {
479 Expression::StringLiteral(s) => {
480 values.push(s.value.to_string());
481 }
482 Expression::ArrayExpression(arr) => {
483 for el in &arr.elements {
484 if let Some(inner) = el.as_expression() {
485 match inner {
486 Expression::StringLiteral(s) => {
487 values.push(s.value.to_string());
488 }
489 Expression::ArrayExpression(sub_arr) => {
491 if let Some(first) = sub_arr.elements.first()
492 && let Some(first_expr) = first.as_expression()
493 && let Some(s) = expression_to_string(first_expr)
494 {
495 values.push(s);
496 }
497 }
498 _ => {}
499 }
500 }
501 }
502 }
503 _ => {}
504 }
505 values
506}
507
508fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
510 match expr {
511 Expression::StringLiteral(s) => {
512 values.push(s.value.to_string());
513 }
514 Expression::ArrayExpression(arr) => {
515 for el in &arr.elements {
516 if let Some(expr) = el.as_expression() {
517 collect_all_string_values(expr, values);
518 }
519 }
520 }
521 Expression::ObjectExpression(obj) => {
522 for prop in &obj.properties {
523 if let ObjectPropertyKind::ObjectProperty(p) = prop {
524 collect_all_string_values(&p.value, values);
525 }
526 }
527 }
528 _ => {}
529 }
530}
531
532fn property_key_to_string(key: &PropertyKey) -> Option<String> {
534 match key {
535 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
536 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
537 _ => None,
538 }
539}
540
541fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
543 if path.is_empty() {
544 return None;
545 }
546 let prop = find_property(obj, path[0])?;
547 if path.len() == 1 {
548 if let Expression::ObjectExpression(nested) = &prop.value {
549 let keys = nested
550 .properties
551 .iter()
552 .filter_map(|p| {
553 if let ObjectPropertyKind::ObjectProperty(p) = p {
554 property_key_to_string(&p.key)
555 } else {
556 None
557 }
558 })
559 .collect();
560 return Some(keys);
561 }
562 return None;
563 }
564 if let Expression::ObjectExpression(nested) = &prop.value {
565 get_nested_object_keys(nested, &path[1..])
566 } else {
567 None
568 }
569}
570
571fn get_nested_expression<'a>(
573 obj: &'a ObjectExpression<'a>,
574 path: &[&str],
575) -> Option<&'a Expression<'a>> {
576 if path.is_empty() {
577 return None;
578 }
579 let prop = find_property(obj, path[0])?;
580 if path.len() == 1 {
581 return Some(&prop.value);
582 }
583 if let Expression::ObjectExpression(nested) = &prop.value {
584 get_nested_expression(nested, &path[1..])
585 } else {
586 None
587 }
588}
589
590fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
592 if path.is_empty() {
593 return None;
594 }
595 if path.len() == 1 {
596 let prop = find_property(obj, path[0])?;
597 return Some(expression_to_string_or_array(&prop.value));
598 }
599 let prop = find_property(obj, path[0])?;
600 if let Expression::ObjectExpression(nested) = &prop.value {
601 get_nested_string_or_array(nested, &path[1..])
602 } else {
603 None
604 }
605}
606
607fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
609 match expr {
610 Expression::StringLiteral(s) => vec![s.value.to_string()],
611 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
612 .quasis
613 .first()
614 .map(|q| vec![q.value.raw.to_string()])
615 .unwrap_or_default(),
616 Expression::ArrayExpression(arr) => arr
617 .elements
618 .iter()
619 .filter_map(|el| el.as_expression().and_then(expression_to_string))
620 .collect(),
621 Expression::ObjectExpression(obj) => obj
622 .properties
623 .iter()
624 .filter_map(|p| {
625 if let ObjectPropertyKind::ObjectProperty(p) = p {
626 expression_to_string(&p.value)
627 } else {
628 None
629 }
630 })
631 .collect(),
632 _ => vec![],
633 }
634}
635
636fn collect_require_sources(expr: &Expression) -> Vec<String> {
638 let mut sources = Vec::new();
639 match expr {
640 Expression::CallExpression(call) if is_require_call(call) => {
641 if let Some(s) = get_require_source(call) {
642 sources.push(s);
643 }
644 }
645 Expression::ArrayExpression(arr) => {
646 for el in &arr.elements {
647 if let Some(inner) = el.as_expression() {
648 match inner {
649 Expression::CallExpression(call) if is_require_call(call) => {
650 if let Some(s) = get_require_source(call) {
651 sources.push(s);
652 }
653 }
654 Expression::ArrayExpression(sub_arr) => {
656 if let Some(first) = sub_arr.elements.first()
657 && let Some(Expression::CallExpression(call)) =
658 first.as_expression()
659 && is_require_call(call)
660 && let Some(s) = get_require_source(call)
661 {
662 sources.push(s);
663 }
664 }
665 _ => {}
666 }
667 }
668 }
669 }
670 _ => {}
671 }
672 sources
673}
674
675fn is_require_call(call: &CallExpression) -> bool {
677 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
678}
679
680fn get_require_source(call: &CallExpression) -> Option<String> {
682 call.arguments.first().and_then(|arg| {
683 if let Argument::StringLiteral(s) = arg {
684 Some(s.value.to_string())
685 } else {
686 None
687 }
688 })
689}
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694 use std::path::PathBuf;
695
696 fn js_path() -> PathBuf {
697 PathBuf::from("config.js")
698 }
699
700 fn ts_path() -> PathBuf {
701 PathBuf::from("config.ts")
702 }
703
704 #[test]
705 fn extract_imports_basic() {
706 let source = r#"
707 import foo from 'foo-pkg';
708 import { bar } from '@scope/bar';
709 export default {};
710 "#;
711 let imports = extract_imports(source, &js_path());
712 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
713 }
714
715 #[test]
716 fn extract_default_export_object_property() {
717 let source = r#"export default { testDir: "./tests" };"#;
718 let val = extract_config_string(source, &js_path(), &["testDir"]);
719 assert_eq!(val, Some("./tests".to_string()));
720 }
721
722 #[test]
723 fn extract_define_config_property() {
724 let source = r#"
725 import { defineConfig } from 'vitest/config';
726 export default defineConfig({
727 test: {
728 include: ["**/*.test.ts", "**/*.spec.ts"],
729 setupFiles: ["./test/setup.ts"]
730 }
731 });
732 "#;
733 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
734 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
735
736 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
737 assert_eq!(setup, vec!["./test/setup.ts"]);
738 }
739
740 #[test]
741 fn extract_module_exports_property() {
742 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
743 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
744 assert_eq!(val, Some("jsdom".to_string()));
745 }
746
747 #[test]
748 fn extract_nested_string_array() {
749 let source = r#"
750 export default {
751 resolve: {
752 alias: {
753 "@": "./src"
754 }
755 },
756 test: {
757 include: ["src/**/*.test.ts"]
758 }
759 };
760 "#;
761 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
762 assert_eq!(include, vec!["src/**/*.test.ts"]);
763 }
764
765 #[test]
766 fn extract_addons_array() {
767 let source = r#"
768 export default {
769 addons: [
770 "@storybook/addon-a11y",
771 "@storybook/addon-docs",
772 "@storybook/addon-links"
773 ]
774 };
775 "#;
776 let addons = extract_config_property_strings(source, &ts_path(), "addons");
777 assert_eq!(
778 addons,
779 vec![
780 "@storybook/addon-a11y",
781 "@storybook/addon-docs",
782 "@storybook/addon-links"
783 ]
784 );
785 }
786
787 #[test]
788 fn handle_empty_config() {
789 let source = "";
790 let result = extract_config_string(source, &js_path(), &["key"]);
791 assert_eq!(result, None);
792 }
793
794 #[test]
797 fn object_keys_postcss_plugins() {
798 let source = r#"
799 module.exports = {
800 plugins: {
801 autoprefixer: {},
802 tailwindcss: {},
803 'postcss-import': {}
804 }
805 };
806 "#;
807 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
808 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
809 }
810
811 #[test]
812 fn object_keys_nested_path() {
813 let source = r#"
814 export default {
815 build: {
816 plugins: {
817 minify: {},
818 compress: {}
819 }
820 }
821 };
822 "#;
823 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
824 assert_eq!(keys, vec!["minify", "compress"]);
825 }
826
827 #[test]
828 fn object_keys_empty_object() {
829 let source = r#"export default { plugins: {} };"#;
830 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
831 assert!(keys.is_empty());
832 }
833
834 #[test]
835 fn object_keys_non_object_returns_empty() {
836 let source = r#"export default { plugins: ["a", "b"] };"#;
837 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
838 assert!(keys.is_empty());
839 }
840
841 #[test]
844 fn string_or_array_single_string() {
845 let source = r#"export default { entry: "./src/index.js" };"#;
846 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
847 assert_eq!(result, vec!["./src/index.js"]);
848 }
849
850 #[test]
851 fn string_or_array_array() {
852 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
853 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
854 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
855 }
856
857 #[test]
858 fn string_or_array_object_values() {
859 let source =
860 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
861 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
862 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
863 }
864
865 #[test]
866 fn string_or_array_nested_path() {
867 let source = r#"
868 export default {
869 build: {
870 rollupOptions: {
871 input: ["./index.html", "./about.html"]
872 }
873 }
874 };
875 "#;
876 let result = extract_config_string_or_array(
877 source,
878 &js_path(),
879 &["build", "rollupOptions", "input"],
880 );
881 assert_eq!(result, vec!["./index.html", "./about.html"]);
882 }
883
884 #[test]
885 fn string_or_array_template_literal() {
886 let source = r#"export default { entry: `./src/index.js` };"#;
887 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
888 assert_eq!(result, vec!["./src/index.js"]);
889 }
890
891 #[test]
894 fn require_strings_array() {
895 let source = r#"
896 module.exports = {
897 plugins: [
898 require('autoprefixer'),
899 require('postcss-import')
900 ]
901 };
902 "#;
903 let deps = extract_config_require_strings(source, &js_path(), "plugins");
904 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
905 }
906
907 #[test]
908 fn require_strings_with_tuples() {
909 let source = r#"
910 module.exports = {
911 plugins: [
912 require('autoprefixer'),
913 [require('postcss-preset-env'), { stage: 3 }]
914 ]
915 };
916 "#;
917 let deps = extract_config_require_strings(source, &js_path(), "plugins");
918 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
919 }
920
921 #[test]
922 fn require_strings_empty_array() {
923 let source = r#"module.exports = { plugins: [] };"#;
924 let deps = extract_config_require_strings(source, &js_path(), "plugins");
925 assert!(deps.is_empty());
926 }
927
928 #[test]
929 fn require_strings_no_require_calls() {
930 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
931 let deps = extract_config_require_strings(source, &js_path(), "plugins");
932 assert!(deps.is_empty());
933 }
934
935 #[test]
938 fn json_wrapped_in_parens_string() {
939 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
940 let val = extract_config_string(source, &js_path(), &["extends"]);
941 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
942 }
943
944 #[test]
945 fn json_wrapped_in_parens_nested_array() {
946 let source =
947 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
948 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
949 assert_eq!(types, vec!["node", "jest"]);
950
951 let include = extract_config_string_array(source, &js_path(), &["include"]);
952 assert_eq!(include, vec!["src/**/*"]);
953 }
954
955 #[test]
956 fn json_wrapped_in_parens_object_keys() {
957 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
958 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
959 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
960 }
961}