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 _ => el.as_expression().and_then(expression_to_string),
344 })
345 .collect(),
346 _ => vec![],
347 }
348}
349
350fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
355 let mut values = Vec::new();
356 match expr {
357 Expression::StringLiteral(s) => {
358 values.push(s.value.to_string());
359 }
360 Expression::ArrayExpression(arr) => {
361 for el in &arr.elements {
362 if let Some(inner) = el.as_expression() {
363 match inner {
364 Expression::StringLiteral(s) => {
365 values.push(s.value.to_string());
366 }
367 Expression::ArrayExpression(sub_arr) => {
369 if let Some(first) = sub_arr.elements.first()
370 && let Some(first_expr) = first.as_expression()
371 && let Some(s) = expression_to_string(first_expr)
372 {
373 values.push(s);
374 }
375 }
376 _ => {}
377 }
378 }
379 }
380 }
381 _ => {}
382 }
383 values
384}
385
386fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
388 match expr {
389 Expression::StringLiteral(s) => {
390 values.push(s.value.to_string());
391 }
392 Expression::ArrayExpression(arr) => {
393 for el in &arr.elements {
394 if let Some(expr) = el.as_expression() {
395 collect_all_string_values(expr, values);
396 }
397 }
398 }
399 Expression::ObjectExpression(obj) => {
400 for prop in &obj.properties {
401 if let ObjectPropertyKind::ObjectProperty(p) = prop {
402 collect_all_string_values(&p.value, values);
403 }
404 }
405 }
406 _ => {}
407 }
408}
409
410fn property_key_to_string(key: &PropertyKey) -> Option<String> {
412 match key {
413 PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
414 PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
415 _ => None,
416 }
417}
418
419fn get_nested_object_keys(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
421 if path.is_empty() {
422 return None;
423 }
424 let prop = find_property(obj, path[0])?;
425 if path.len() == 1 {
426 if let Expression::ObjectExpression(nested) = &prop.value {
427 let keys = nested
428 .properties
429 .iter()
430 .filter_map(|p| {
431 if let ObjectPropertyKind::ObjectProperty(p) = p {
432 property_key_to_string(&p.key)
433 } else {
434 None
435 }
436 })
437 .collect();
438 return Some(keys);
439 }
440 return None;
441 }
442 if let Expression::ObjectExpression(nested) = &prop.value {
443 get_nested_object_keys(nested, &path[1..])
444 } else {
445 None
446 }
447}
448
449fn get_nested_string_or_array(obj: &ObjectExpression, path: &[&str]) -> Option<Vec<String>> {
451 if path.is_empty() {
452 return None;
453 }
454 if path.len() == 1 {
455 let prop = find_property(obj, path[0])?;
456 return Some(expression_to_string_or_array(&prop.value));
457 }
458 let prop = find_property(obj, path[0])?;
459 if let Expression::ObjectExpression(nested) = &prop.value {
460 get_nested_string_or_array(nested, &path[1..])
461 } else {
462 None
463 }
464}
465
466fn expression_to_string_or_array(expr: &Expression) -> Vec<String> {
468 match expr {
469 Expression::StringLiteral(s) => vec![s.value.to_string()],
470 Expression::TemplateLiteral(t) if t.expressions.is_empty() => t
471 .quasis
472 .first()
473 .map(|q| vec![q.value.raw.to_string()])
474 .unwrap_or_default(),
475 Expression::ArrayExpression(arr) => arr
476 .elements
477 .iter()
478 .filter_map(|el| el.as_expression().and_then(expression_to_string))
479 .collect(),
480 Expression::ObjectExpression(obj) => obj
481 .properties
482 .iter()
483 .filter_map(|p| {
484 if let ObjectPropertyKind::ObjectProperty(p) = p {
485 expression_to_string(&p.value)
486 } else {
487 None
488 }
489 })
490 .collect(),
491 _ => vec![],
492 }
493}
494
495fn collect_require_sources(expr: &Expression) -> Vec<String> {
497 let mut sources = Vec::new();
498 match expr {
499 Expression::CallExpression(call) if is_require_call(call) => {
500 if let Some(s) = get_require_source(call) {
501 sources.push(s);
502 }
503 }
504 Expression::ArrayExpression(arr) => {
505 for el in &arr.elements {
506 if let Some(inner) = el.as_expression() {
507 match inner {
508 Expression::CallExpression(call) if is_require_call(call) => {
509 if let Some(s) = get_require_source(call) {
510 sources.push(s);
511 }
512 }
513 Expression::ArrayExpression(sub_arr) => {
515 if let Some(first) = sub_arr.elements.first()
516 && let Some(Expression::CallExpression(call)) =
517 first.as_expression()
518 && is_require_call(call)
519 && let Some(s) = get_require_source(call)
520 {
521 sources.push(s);
522 }
523 }
524 _ => {}
525 }
526 }
527 }
528 }
529 _ => {}
530 }
531 sources
532}
533
534fn is_require_call(call: &CallExpression) -> bool {
536 matches!(&call.callee, Expression::Identifier(id) if id.name == "require")
537}
538
539fn get_require_source(call: &CallExpression) -> Option<String> {
541 call.arguments.first().and_then(|arg| {
542 if let Argument::StringLiteral(s) = arg {
543 Some(s.value.to_string())
544 } else {
545 None
546 }
547 })
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use std::path::PathBuf;
554
555 fn js_path() -> PathBuf {
556 PathBuf::from("config.js")
557 }
558
559 fn ts_path() -> PathBuf {
560 PathBuf::from("config.ts")
561 }
562
563 #[test]
564 fn extract_imports_basic() {
565 let source = r#"
566 import foo from 'foo-pkg';
567 import { bar } from '@scope/bar';
568 export default {};
569 "#;
570 let imports = extract_imports(source, &js_path());
571 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
572 }
573
574 #[test]
575 fn extract_default_export_object_property() {
576 let source = r#"export default { testDir: "./tests" };"#;
577 let val = extract_config_string(source, &js_path(), &["testDir"]);
578 assert_eq!(val, Some("./tests".to_string()));
579 }
580
581 #[test]
582 fn extract_define_config_property() {
583 let source = r#"
584 import { defineConfig } from 'vitest/config';
585 export default defineConfig({
586 test: {
587 include: ["**/*.test.ts", "**/*.spec.ts"],
588 setupFiles: ["./test/setup.ts"]
589 }
590 });
591 "#;
592 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
593 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
594
595 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
596 assert_eq!(setup, vec!["./test/setup.ts"]);
597 }
598
599 #[test]
600 fn extract_module_exports_property() {
601 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
602 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
603 assert_eq!(val, Some("jsdom".to_string()));
604 }
605
606 #[test]
607 fn extract_nested_string_array() {
608 let source = r#"
609 export default {
610 resolve: {
611 alias: {
612 "@": "./src"
613 }
614 },
615 test: {
616 include: ["src/**/*.test.ts"]
617 }
618 };
619 "#;
620 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
621 assert_eq!(include, vec!["src/**/*.test.ts"]);
622 }
623
624 #[test]
625 fn extract_addons_array() {
626 let source = r#"
627 export default {
628 addons: [
629 "@storybook/addon-a11y",
630 "@storybook/addon-docs",
631 "@storybook/addon-links"
632 ]
633 };
634 "#;
635 let addons = extract_config_property_strings(source, &ts_path(), "addons");
636 assert_eq!(
637 addons,
638 vec![
639 "@storybook/addon-a11y",
640 "@storybook/addon-docs",
641 "@storybook/addon-links"
642 ]
643 );
644 }
645
646 #[test]
647 fn handle_empty_config() {
648 let source = "";
649 let result = extract_config_string(source, &js_path(), &["key"]);
650 assert_eq!(result, None);
651 }
652
653 #[test]
656 fn object_keys_postcss_plugins() {
657 let source = r#"
658 module.exports = {
659 plugins: {
660 autoprefixer: {},
661 tailwindcss: {},
662 'postcss-import': {}
663 }
664 };
665 "#;
666 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
667 assert_eq!(keys, vec!["autoprefixer", "tailwindcss", "postcss-import"]);
668 }
669
670 #[test]
671 fn object_keys_nested_path() {
672 let source = r#"
673 export default {
674 build: {
675 plugins: {
676 minify: {},
677 compress: {}
678 }
679 }
680 };
681 "#;
682 let keys = extract_config_object_keys(source, &js_path(), &["build", "plugins"]);
683 assert_eq!(keys, vec!["minify", "compress"]);
684 }
685
686 #[test]
687 fn object_keys_empty_object() {
688 let source = r#"export default { plugins: {} };"#;
689 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
690 assert!(keys.is_empty());
691 }
692
693 #[test]
694 fn object_keys_non_object_returns_empty() {
695 let source = r#"export default { plugins: ["a", "b"] };"#;
696 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
697 assert!(keys.is_empty());
698 }
699
700 #[test]
703 fn string_or_array_single_string() {
704 let source = r#"export default { entry: "./src/index.js" };"#;
705 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
706 assert_eq!(result, vec!["./src/index.js"]);
707 }
708
709 #[test]
710 fn string_or_array_array() {
711 let source = r#"export default { entry: ["./src/a.js", "./src/b.js"] };"#;
712 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
713 assert_eq!(result, vec!["./src/a.js", "./src/b.js"]);
714 }
715
716 #[test]
717 fn string_or_array_object_values() {
718 let source =
719 r#"export default { entry: { main: "./src/main.js", vendor: "./src/vendor.js" } };"#;
720 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
721 assert_eq!(result, vec!["./src/main.js", "./src/vendor.js"]);
722 }
723
724 #[test]
725 fn string_or_array_nested_path() {
726 let source = r#"
727 export default {
728 build: {
729 rollupOptions: {
730 input: ["./index.html", "./about.html"]
731 }
732 }
733 };
734 "#;
735 let result = extract_config_string_or_array(
736 source,
737 &js_path(),
738 &["build", "rollupOptions", "input"],
739 );
740 assert_eq!(result, vec!["./index.html", "./about.html"]);
741 }
742
743 #[test]
744 fn string_or_array_template_literal() {
745 let source = r#"export default { entry: `./src/index.js` };"#;
746 let result = extract_config_string_or_array(source, &js_path(), &["entry"]);
747 assert_eq!(result, vec!["./src/index.js"]);
748 }
749
750 #[test]
753 fn require_strings_array() {
754 let source = r#"
755 module.exports = {
756 plugins: [
757 require('autoprefixer'),
758 require('postcss-import')
759 ]
760 };
761 "#;
762 let deps = extract_config_require_strings(source, &js_path(), "plugins");
763 assert_eq!(deps, vec!["autoprefixer", "postcss-import"]);
764 }
765
766 #[test]
767 fn require_strings_with_tuples() {
768 let source = r#"
769 module.exports = {
770 plugins: [
771 require('autoprefixer'),
772 [require('postcss-preset-env'), { stage: 3 }]
773 ]
774 };
775 "#;
776 let deps = extract_config_require_strings(source, &js_path(), "plugins");
777 assert_eq!(deps, vec!["autoprefixer", "postcss-preset-env"]);
778 }
779
780 #[test]
781 fn require_strings_empty_array() {
782 let source = r#"module.exports = { plugins: [] };"#;
783 let deps = extract_config_require_strings(source, &js_path(), "plugins");
784 assert!(deps.is_empty());
785 }
786
787 #[test]
788 fn require_strings_no_require_calls() {
789 let source = r#"module.exports = { plugins: ["a", "b"] };"#;
790 let deps = extract_config_require_strings(source, &js_path(), "plugins");
791 assert!(deps.is_empty());
792 }
793
794 #[test]
797 fn json_wrapped_in_parens_string() {
798 let source = r#"({"extends": "@tsconfig/node18/tsconfig.json"})"#;
799 let val = extract_config_string(source, &js_path(), &["extends"]);
800 assert_eq!(val, Some("@tsconfig/node18/tsconfig.json".to_string()));
801 }
802
803 #[test]
804 fn json_wrapped_in_parens_nested_array() {
805 let source =
806 r#"({"compilerOptions": {"types": ["node", "jest"]}, "include": ["src/**/*"]})"#;
807 let types = extract_config_string_array(source, &js_path(), &["compilerOptions", "types"]);
808 assert_eq!(types, vec!["node", "jest"]);
809
810 let include = extract_config_string_array(source, &js_path(), &["include"]);
811 assert_eq!(include, vec!["src/**/*"]);
812 }
813
814 #[test]
815 fn json_wrapped_in_parens_object_keys() {
816 let source = r#"({"plugins": {"autoprefixer": {}, "tailwindcss": {}}})"#;
817 let keys = extract_config_object_keys(source, &js_path(), &["plugins"]);
818 assert_eq!(keys, vec!["autoprefixer", "tailwindcss"]);
819 }
820}