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_require_strings(source: &str, path: &Path, key: &str) -> Vec<String> {
126 extract_from_source(source, path, |program| {
127 let obj = find_config_object(program)?;
128 let prop = find_property(obj, key)?;
129 Some(collect_require_sources(&prop.value))
130 })
131 .unwrap_or_default()
132}
133
134fn extract_from_source<T>(
138 source: &str,
139 path: &Path,
140 extractor: impl FnOnce(&Program) -> Option<T>,
141) -> Option<T> {
142 let source_type = SourceType::from_path(path).unwrap_or_default();
143 let alloc = Allocator::default();
144 let parsed = Parser::new(&alloc, source, source_type).parse();
145 extractor(&parsed.program)
146}
147
148fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
157 for stmt in &program.body {
158 match stmt {
159 Statement::ExportDefaultDeclaration(decl) => {
161 let expr: Option<&Expression> = match &decl.declaration {
163 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
164 return Some(obj);
165 }
166 ExportDefaultDeclarationKind::CallExpression(_)
167 | ExportDefaultDeclarationKind::ParenthesizedExpression(_) => {
168 decl.declaration.as_expression()
170 }
171 _ => None,
172 };
173 if let Some(expr) = expr {
174 return extract_object_from_expression(expr);
175 }
176 }
177 Statement::ExpressionStatement(expr_stmt) => {
179 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
180 && is_module_exports_target(&assign.left)
181 {
182 return extract_object_from_expression(&assign.right);
183 }
184 }
185 _ => {}
186 }
187 }
188
189 if program.body.len() == 1
192 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
193 {
194 match &expr_stmt.expression {
195 Expression::ObjectExpression(obj) => return Some(obj),
196 Expression::ParenthesizedExpression(paren) => {
197 if let Expression::ObjectExpression(obj) = &paren.expression {
198 return Some(obj);
199 }
200 }
201 _ => {}
202 }
203 }
204
205 None
206}
207
208fn extract_object_from_expression<'a>(
210 expr: &'a Expression<'a>,
211) -> Option<&'a ObjectExpression<'a>> {
212 match expr {
213 Expression::ObjectExpression(obj) => Some(obj),
215 Expression::CallExpression(call) => {
217 for arg in &call.arguments {
219 match arg {
220 Argument::ObjectExpression(obj) => return Some(obj),
221 Argument::ArrowFunctionExpression(arrow) => {
223 if arrow.expression
224 && !arrow.body.statements.is_empty()
225 && let Statement::ExpressionStatement(expr_stmt) =
226 &arrow.body.statements[0]
227 {
228 return extract_object_from_expression(&expr_stmt.expression);
229 }
230 }
231 _ => {}
232 }
233 }
234 None
235 }
236 Expression::ParenthesizedExpression(paren) => {
238 extract_object_from_expression(&paren.expression)
239 }
240 _ => None,
241 }
242}
243
244fn is_module_exports_target(target: &AssignmentTarget) -> bool {
246 if let AssignmentTarget::StaticMemberExpression(member) = target
247 && let Expression::Identifier(obj) = &member.object
248 {
249 return obj.name == "module" && member.property.name == "exports";
250 }
251 false
252}
253
254fn find_property<'a>(obj: &'a ObjectExpression<'a>, key: &str) -> Option<&'a ObjectProperty<'a>> {
256 for prop in &obj.properties {
257 if let ObjectPropertyKind::ObjectProperty(p) = prop
258 && property_key_matches(&p.key, key)
259 {
260 return Some(p);
261 }
262 }
263 None
264}
265
266fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
268 match key {
269 PropertyKey::StaticIdentifier(id) => id.name == name,
270 PropertyKey::StringLiteral(s) => s.value == name,
271 _ => false,
272 }
273}
274
275fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
277 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
278}
279
280fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
282 find_property(obj, key)
283 .map(|p| expression_to_string_array(&p.value))
284 .unwrap_or_default()
285}
286
287fn get_nested_string_array_from_object(
289 obj: &ObjectExpression,
290 path: &[&str],
291) -> Option<Vec<String>> {
292 if path.is_empty() {
293 return None;
294 }
295 if path.len() == 1 {
296 return Some(get_object_string_array_property(obj, path[0]));
297 }
298 let prop = find_property(obj, path[0])?;
300 if let Expression::ObjectExpression(nested) = &prop.value {
301 get_nested_string_array_from_object(nested, &path[1..])
302 } else {
303 None
304 }
305}
306
307fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
309 if path.is_empty() {
310 return None;
311 }
312 if path.len() == 1 {
313 return get_object_string_property(obj, path[0]);
314 }
315 let prop = find_property(obj, path[0])?;
316 if let Expression::ObjectExpression(nested) = &prop.value {
317 get_nested_string_from_object(nested, &path[1..])
318 } else {
319 None
320 }
321}
322
323fn expression_to_string(expr: &Expression) -> Option<String> {
325 match expr {
326 Expression::StringLiteral(s) => Some(s.value.to_string()),
327 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
328 t.quasis.first().map(|q| q.value.raw.to_string())
330 }
331 _ => None,
332 }
333}
334
335fn expression_to_string_array(expr: &Expression) -> Vec<String> {
337 match expr {
338 Expression::ArrayExpression(arr) => arr
339 .elements
340 .iter()
341 .filter_map(|el| match el {
342 ArrayExpressionElement::SpreadElement(_) => None,
343 _ => {
344 if let Some(expr) = el.as_expression() {
345 expression_to_string(expr)
346 } else {
347 None
348 }
349 }
350 })
351 .collect(),
352 _ => vec![],
353 }
354}
355
356fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
361 let mut values = Vec::new();
362 match expr {
363 Expression::StringLiteral(s) => {
364 values.push(s.value.to_string());
365 }
366 Expression::ArrayExpression(arr) => {
367 for el in &arr.elements {
368 if let Some(inner) = el.as_expression() {
369 match inner {
370 Expression::StringLiteral(s) => {
371 values.push(s.value.to_string());
372 }
373 Expression::ArrayExpression(sub_arr) => {
375 if let Some(first) = sub_arr.elements.first()
376 && let Some(first_expr) = first.as_expression()
377 && let Some(s) = expression_to_string(first_expr)
378 {
379 values.push(s);
380 }
381 }
382 _ => {}
383 }
384 }
385 }
386 }
387 _ => {}
388 }
389 values
390}
391
392fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
394 match expr {
395 Expression::StringLiteral(s) => {
396 values.push(s.value.to_string());
397 }
398 Expression::ArrayExpression(arr) => {
399 for el in &arr.elements {
400 if let Some(expr) = el.as_expression() {
401 collect_all_string_values(expr, values);
402 }
403 }
404 }
405 Expression::ObjectExpression(obj) => {
406 for prop in &obj.properties {
407 if let ObjectPropertyKind::ObjectProperty(p) = prop {
408 collect_all_string_values(&p.value, values);
409 }
410 }
411 }
412 _ => {}
413 }
414}
415
416fn property_key_to_string(key: &PropertyKey) -> Option<String> {
418 match key {
419 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
420 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
421 _ => None,
422 }
423}
424
425fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
427 if path.is_empty() {
428 return None;
429 }
430 let prop = find_property(obj, path[0])?;
431 if path.len() == 1 {
432 if let Expression::ObjectExpression(nested) = &prop.value {
433 let keys = nested
434 .properties
435 .iter()
436 .filter_map(|p| {
437 if let ObjectPropertyKind::ObjectProperty(p) = p {
438 property_key_to_string(&p.key)
439 } else {
440 None
441 }
442 })
443 .collect();
444 return Some(keys);
445 }
446 return None;
447 }
448 if let Expression::ObjectExpression(nested) = &prop.value {
449 get_nested_object_keys(nested, &path[1..])
450 } else {
451 None
452 }
453}
454
455fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
457 if path.is_empty() {
458 return None;
459 }
460 if path.len() == 1 {
461 let prop = find_property(obj, path[0])?;
462 return Some(expression_to_string_or_array(&prop.value));
463 }
464 let prop = find_property(obj, path[0])?;
465 if let Expression::ObjectExpression(nested) = &prop.value {
466 get_nested_string_or_array(nested, &path[1..])
467 } else {
468 None
469 }
470}
471
472fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
474 match expr {
475 Expression::StringLiteral(s) => vec![s.value.to_string()],
476 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
477 .quasis
478 .first()
479 .map(|q| vec![q.value.raw.to_string()])
480 .unwrap_or_default(),
481 Expression::ArrayExpression(arr) => arr
482 .elements
483 .iter()
484 .filter_map(|el| el.as_expression().and_then(expression_to_string))
485 .collect(),
486 Expression::ObjectExpression(obj) => obj
487 .properties
488 .iter()
489 .filter_map(|p| {
490 if let ObjectPropertyKind::ObjectProperty(p) = p {
491 expression_to_string(&p.value)
492 } else {
493 None
494 }
495 })
496 .collect(),
497 _ => vec![],
498 }
499}
500
501fn collect_require_sources(expr: &Expression) -> Vec<String> {
503 let mut sources = Vec::new();
504 match expr {
505 Expression::CallExpression(call) if is_require_call(call) => {
506 if let Some(s) = get_require_source(call) {
507 sources.push(s);
508 }
509 }
510 Expression::ArrayExpression(arr) => {
511 for el in &arr.elements {
512 if let Some(inner) = el.as_expression() {
513 match inner {
514 Expression::CallExpression(call) if is_require_call(call) => {
515 if let Some(s) = get_require_source(call) {
516 sources.push(s);
517 }
518 }
519 Expression::ArrayExpression(sub_arr) => {
521 if let Some(first) = sub_arr.elements.first()
522 && let Some(Expression::CallExpression(call)) =
523 first.as_expression()
524 && is_require_call(call)
525 && let Some(s) = get_require_source(call)
526 {
527 sources.push(s);
528 }
529 }
530 _ => {}
531 }
532 }
533 }
534 }
535 _ => {}
536 }
537 sources
538}
539
540fn is_require_call(call: &CallExpression) -> bool {
542 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
543}
544
545fn get_require_source(call: &CallExpression) -> Option<String> {
547 call.arguments.first().and_then(|arg| {
548 if let Argument::StringLiteral(s) = arg {
549 Some(s.value.to_string())
550 } else {
551 None
552 }
553 })
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use std::path::PathBuf;
560
561 fn js_path() -> PathBuf {
562 PathBuf::from("config.js")
563 }
564
565 fn ts_path() -> PathBuf {
566 PathBuf::from("config.ts")
567 }
568
569 #[test]
570 fn extract_imports_basic() {
571 let source = r#"
572 import foo from 'foo-pkg';
573 import { bar } from '@scope/bar';
574 export default {};
575 "#;
576 let imports = extract_imports(source, &js_path());
577 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
578 }
579
580 #[test]
581 fn extract_default_export_object_property() {
582 let source = r#"export default { testDir: "./tests" };"#;
583 let val = extract_config_string(source, &js_path(), &["testDir"]);
584 assert_eq!(val, Some("./tests".to_string()));
585 }
586
587 #[test]
588 fn extract_define_config_property() {
589 let source = r#"
590 import { defineConfig } from 'vitest/config';
591 export default defineConfig({
592 test: {
593 include: ["**/*.test.ts", "**/*.spec.ts"],
594 setupFiles: ["./test/setup.ts"]
595 }
596 });
597 "#;
598 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
599 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
600
601 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
602 assert_eq!(setup, vec!["./test/setup.ts"]);
603 }
604
605 #[test]
606 fn extract_module_exports_property() {
607 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
608 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
609 assert_eq!(val, Some("jsdom".to_string()));
610 }
611
612 #[test]
613 fn extract_nested_string_array() {
614 let source = r#"
615 export default {
616 resolve: {
617 alias: {
618 "@": "./src"
619 }
620 },
621 test: {
622 include: ["src/**/*.test.ts"]
623 }
624 };
625 "#;
626 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
627 assert_eq!(include, vec!["src/**/*.test.ts"]);
628 }
629
630 #[test]
631 fn extract_addons_array() {
632 let source = r#"
633 export default {
634 addons: [
635 "@storybook/addon-a11y",
636 "@storybook/addon-docs",
637 "@storybook/addon-links"
638 ]
639 };
640 "#;
641 let addons = extract_config_property_strings(source, &ts_path(), "addons");
642 assert_eq!(
643 addons,
644 vec![
645 "@storybook/addon-a11y",
646 "@storybook/addon-docs",
647 "@storybook/addon-links"
648 ]
649 );
650 }
651
652 #[test]
653 fn handle_empty_config() {
654 let source = "";
655 let result = extract_config_string(source, &js_path(), &["key"]);
656 assert_eq!(result, None);
657 }
658
659 #[test]
662 fn object_keys_postcss_plugins() {
663 let source = r#"
664 module.exports = {
665 plugins: {
666 autoprefixer: {},
667 tailwindcss: {},
668 'postcss-import': {}
669 }
670 };
671 "#;
672 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
673 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
674 }
675
676 #[test]
677 fn object_keys_nested_path() {
678 let source = r#"
679 export default {
680 build: {
681 plugins: {
682 minify: {},
683 compress: {}
684 }
685 }
686 };
687 "#;
688 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
689 assert_eq!(keys, vec!["minify", "compress"]);
690 }
691
692 #[test]
693 fn object_keys_empty_object() {
694 let source = r#"export default { plugins: {} };"#;
695 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
696 assert!(keys.is_empty());
697 }
698
699 #[test]
700 fn object_keys_non_object_returns_empty() {
701 let source = r#"export default { plugins: ["a", "b"] };"#;
702 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
703 assert!(keys.is_empty());
704 }
705
706 #[test]
709 fn string_or_array_single_string() {
710 let source = r#"export default { entry: "./src/index.js" };"#;
711 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
712 assert_eq!(result, vec!["./src/index.js"]);
713 }
714
715 #[test]
716 fn string_or_array_array() {
717 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
718 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
719 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
720 }
721
722 #[test]
723 fn string_or_array_object_values() {
724 let source =
725 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
726 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
727 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
728 }
729
730 #[test]
731 fn string_or_array_nested_path() {
732 let source = r#"
733 export default {
734 build: {
735 rollupOptions: {
736 input: ["./index.html", "./about.html"]
737 }
738 }
739 };
740 "#;
741 let result = extract_config_string_or_array(
742 source,
743 &js_path(),
744 &["build", "rollupOptions", "input"],
745 );
746 assert_eq!(result, vec!["./index.html", "./about.html"]);
747 }
748
749 #[test]
750 fn string_or_array_template_literal() {
751 let source = r#"export default { entry: `./src/index.js` };"#;
752 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
753 assert_eq!(result, vec!["./src/index.js"]);
754 }
755
756 #[test]
759 fn require_strings_array() {
760 let source = r#"
761 module.exports = {
762 plugins: [
763 require('autoprefixer'),
764 require('postcss-import')
765 ]
766 };
767 "#;
768 let deps = extract_config_require_strings(source, &js_path(), "plugins");
769 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
770 }
771
772 #[test]
773 fn require_strings_with_tuples() {
774 let source = r#"
775 module.exports = {
776 plugins: [
777 require('autoprefixer'),
778 [require('postcss-preset-env'), { stage: 3 }]
779 ]
780 };
781 "#;
782 let deps = extract_config_require_strings(source, &js_path(), "plugins");
783 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
784 }
785
786 #[test]
787 fn require_strings_empty_array() {
788 let source = r#"module.exports = { plugins: [] };"#;
789 let deps = extract_config_require_strings(source, &js_path(), "plugins");
790 assert!(deps.is_empty());
791 }
792
793 #[test]
794 fn require_strings_no_require_calls() {
795 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
796 let deps = extract_config_require_strings(source, &js_path(), "plugins");
797 assert!(deps.is_empty());
798 }
799
800 #[test]
803 fn json_wrapped_in_parens_string() {
804 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
805 let val = extract_config_string(source, &js_path(), &["extends"]);
806 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
807 }
808
809 #[test]
810 fn json_wrapped_in_parens_nested_array() {
811 let source =
812 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
813 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
814 assert_eq!(types, vec!["node", "jest"]);
815
816 let include = extract_config_string_array(source, &js_path(), &["include"]);
817 assert_eq!(include, vec!["src/**/*"]);
818 }
819
820 #[test]
821 fn json_wrapped_in_parens_object_keys() {
822 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
823 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
824 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
825 }
826}