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
85fn extract_from_source<T>(
89 source: &str,
90 path: &Path,
91 extractor: impl FnOnce(&Program) -> Option<T>,
92) -> Option<T> {
93 let source_type = SourceType::from_path(path).unwrap_or_default();
94 let alloc = Allocator::default();
95 let parsed = Parser::new(&alloc, source, source_type).parse();
96 extractor(&parsed.program)
97}
98
99fn find_config_object<'a>(program: &'a Program) -> Option<&'a ObjectExpression<'a>> {
108 for stmt in &program.body {
109 match stmt {
110 Statement::ExportDefaultDeclaration(decl) => {
112 let expr: Option<&Expression> = match &decl.declaration {
114 ExportDefaultDeclarationKind::ObjectExpression(obj) => {
115 return Some(obj);
116 }
117 ExportDefaultDeclarationKind::CallExpression(_)
118 | ExportDefaultDeclarationKind::ParenthesizedExpression(_) => {
119 decl.declaration.as_expression()
121 }
122 _ => None,
123 };
124 if let Some(expr) = expr {
125 return extract_object_from_expression(expr);
126 }
127 }
128 Statement::ExpressionStatement(expr_stmt) => {
130 if let Expression::AssignmentExpression(assign) = &expr_stmt.expression
131 && is_module_exports_target(&assign.left)
132 {
133 return extract_object_from_expression(&assign.right);
134 }
135 }
136 _ => {}
137 }
138 }
139
140 if program.body.len() == 1
142 && let Statement::ExpressionStatement(expr_stmt) = &program.body[0]
143 && let Expression::ObjectExpression(obj) = &expr_stmt.expression
144 {
145 return Some(obj);
146 }
147
148 None
149}
150
151fn extract_object_from_expression<'a>(
153 expr: &'a Expression<'a>,
154) -> Option<&'a ObjectExpression<'a>> {
155 match expr {
156 Expression::ObjectExpression(obj) => Some(obj),
158 Expression::CallExpression(call) => {
160 for arg in &call.arguments {
162 match arg {
163 Argument::ObjectExpression(obj) => return Some(obj),
164 Argument::ArrowFunctionExpression(arrow) => {
166 if arrow.expression
167 && !arrow.body.statements.is_empty()
168 && let Statement::ExpressionStatement(expr_stmt) =
169 &arrow.body.statements[0]
170 {
171 return extract_object_from_expression(&expr_stmt.expression);
172 }
173 }
174 _ => {}
175 }
176 }
177 None
178 }
179 Expression::ParenthesizedExpression(paren) => {
181 extract_object_from_expression(&paren.expression)
182 }
183 _ => None,
184 }
185}
186
187fn is_module_exports_target(target: &AssignmentTarget) -> bool {
189 if let AssignmentTarget::StaticMemberExpression(member) = target
190 && let Expression::Identifier(obj) = &member.object
191 {
192 return obj.name == "module" && member.property.name == "exports";
193 }
194 false
195}
196
197fn find_property<'a>(obj: &'a ObjectExpression<'a>, key: &str) -> Option<&'a ObjectProperty<'a>> {
199 for prop in &obj.properties {
200 if let ObjectPropertyKind::ObjectProperty(p) = prop
201 && property_key_matches(&p.key, key)
202 {
203 return Some(p);
204 }
205 }
206 None
207}
208
209fn property_key_matches(key: &PropertyKey, name: &str) -> bool {
211 match key {
212 PropertyKey::StaticIdentifier(id) => id.name == name,
213 PropertyKey::StringLiteral(s) => s.value == name,
214 _ => false,
215 }
216}
217
218fn get_object_string_property(obj: &ObjectExpression, key: &str) -> Option<String> {
220 find_property(obj, key).and_then(|p| expression_to_string(&p.value))
221}
222
223fn get_object_string_array_property(obj: &ObjectExpression, key: &str) -> Vec<String> {
225 find_property(obj, key)
226 .map(|p| expression_to_string_array(&p.value))
227 .unwrap_or_default()
228}
229
230fn get_nested_string_array_from_object(
232 obj: &ObjectExpression,
233 path: &[&str],
234) -> Option<Vec<String>> {
235 if path.is_empty() {
236 return None;
237 }
238 if path.len() == 1 {
239 return Some(get_object_string_array_property(obj, path[0]));
240 }
241 let prop = find_property(obj, path[0])?;
243 if let Expression::ObjectExpression(nested) = &prop.value {
244 get_nested_string_array_from_object(nested, &path[1..])
245 } else {
246 None
247 }
248}
249
250fn get_nested_string_from_object(obj: &ObjectExpression, path: &[&str]) -> Option<String> {
252 if path.is_empty() {
253 return None;
254 }
255 if path.len() == 1 {
256 return get_object_string_property(obj, path[0]);
257 }
258 let prop = find_property(obj, path[0])?;
259 if let Expression::ObjectExpression(nested) = &prop.value {
260 get_nested_string_from_object(nested, &path[1..])
261 } else {
262 None
263 }
264}
265
266fn expression_to_string(expr: &Expression) -> Option<String> {
268 match expr {
269 Expression::StringLiteral(s) => Some(s.value.to_string()),
270 Expression::TemplateLiteral(t) if t.expressions.is_empty() => {
271 t.quasis.first().map(|q| q.value.raw.to_string())
273 }
274 _ => None,
275 }
276}
277
278fn expression_to_string_array(expr: &Expression) -> Vec<String> {
280 match expr {
281 Expression::ArrayExpression(arr) => arr
282 .elements
283 .iter()
284 .filter_map(|el| match el {
285 ArrayExpressionElement::SpreadElement(_) => None,
286 _ => {
287 if let Some(expr) = el.as_expression() {
288 expression_to_string(expr)
289 } else {
290 None
291 }
292 }
293 })
294 .collect(),
295 _ => vec![],
296 }
297}
298
299fn collect_shallow_string_values(expr: &Expression) -> Vec<String> {
304 let mut values = Vec::new();
305 match expr {
306 Expression::StringLiteral(s) => {
307 values.push(s.value.to_string());
308 }
309 Expression::ArrayExpression(arr) => {
310 for el in &arr.elements {
311 if let Some(inner) = el.as_expression() {
312 match inner {
313 Expression::StringLiteral(s) => {
314 values.push(s.value.to_string());
315 }
316 Expression::ArrayExpression(sub_arr) => {
318 if let Some(first) = sub_arr.elements.first()
319 && let Some(first_expr) = first.as_expression()
320 && let Some(s) = expression_to_string(first_expr)
321 {
322 values.push(s);
323 }
324 }
325 _ => {}
326 }
327 }
328 }
329 }
330 _ => {}
331 }
332 values
333}
334
335fn collect_all_string_values(expr: &Expression, values: &mut Vec<String>) {
337 match expr {
338 Expression::StringLiteral(s) => {
339 values.push(s.value.to_string());
340 }
341 Expression::ArrayExpression(arr) => {
342 for el in &arr.elements {
343 if let Some(expr) = el.as_expression() {
344 collect_all_string_values(expr, values);
345 }
346 }
347 }
348 Expression::ObjectExpression(obj) => {
349 for prop in &obj.properties {
350 if let ObjectPropertyKind::ObjectProperty(p) = prop {
351 collect_all_string_values(&p.value, values);
352 }
353 }
354 }
355 _ => {}
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use std::path::PathBuf;
363
364 fn js_path() -> PathBuf {
365 PathBuf::from("config.js")
366 }
367
368 fn ts_path() -> PathBuf {
369 PathBuf::from("config.ts")
370 }
371
372 #[test]
373 fn extract_imports_basic() {
374 let source = r#"
375 import foo from 'foo-pkg';
376 import { bar } from '@scope/bar';
377 export default {};
378 "#;
379 let imports = extract_imports(source, &js_path());
380 assert_eq!(imports, vec!["foo-pkg", "@scope/bar"]);
381 }
382
383 #[test]
384 fn extract_default_export_object_property() {
385 let source = r#"export default { testDir: "./tests" };"#;
386 let val = extract_config_string(source, &js_path(), &["testDir"]);
387 assert_eq!(val, Some("./tests".to_string()));
388 }
389
390 #[test]
391 fn extract_define_config_property() {
392 let source = r#"
393 import { defineConfig } from 'vitest/config';
394 export default defineConfig({
395 test: {
396 include: ["**/*.test.ts", "**/*.spec.ts"],
397 setupFiles: ["./test/setup.ts"]
398 }
399 });
400 "#;
401 let include = extract_config_string_array(source, &ts_path(), &["test", "include"]);
402 assert_eq!(include, vec!["**/*.test.ts", "**/*.spec.ts"]);
403
404 let setup = extract_config_string_array(source, &ts_path(), &["test", "setupFiles"]);
405 assert_eq!(setup, vec!["./test/setup.ts"]);
406 }
407
408 #[test]
409 fn extract_module_exports_property() {
410 let source = r#"module.exports = { testEnvironment: "jsdom" };"#;
411 let val = extract_config_string(source, &js_path(), &["testEnvironment"]);
412 assert_eq!(val, Some("jsdom".to_string()));
413 }
414
415 #[test]
416 fn extract_nested_string_array() {
417 let source = r#"
418 export default {
419 resolve: {
420 alias: {
421 "@": "./src"
422 }
423 },
424 test: {
425 include: ["src/**/*.test.ts"]
426 }
427 };
428 "#;
429 let include = extract_config_string_array(source, &js_path(), &["test", "include"]);
430 assert_eq!(include, vec!["src/**/*.test.ts"]);
431 }
432
433 #[test]
434 fn extract_addons_array() {
435 let source = r#"
436 export default {
437 addons: [
438 "@storybook/addon-a11y",
439 "@storybook/addon-docs",
440 "@storybook/addon-links"
441 ]
442 };
443 "#;
444 let addons = extract_config_property_strings(source, &ts_path(), "addons");
445 assert_eq!(
446 addons,
447 vec![
448 "@storybook/addon-a11y",
449 "@storybook/addon-docs",
450 "@storybook/addon-links"
451 ]
452 );
453 }
454
455 #[test]
456 fn handle_empty_config() {
457 let source = "";
458 let result = extract_config_string(source, &js_path(), &["key"]);
459 assert_eq!(result, None);
460 }
461}