Skip to content

Commit dd117c3

Browse files
branchseerclaude
andcommitted
feat(static-config): support module.exports CJS pattern
Add support for CommonJS config files: - module.exports = { ... } - module.exports = defineConfig({ ... }) Refactored shared config extraction into extract_config_from_expr, used by both export default and module.exports paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 86d928e commit dd117c3

File tree

1 file changed

+75
-35
lines changed
  • crates/vite_static_config/src

1 file changed

+75
-35
lines changed

crates/vite_static_config/src/lib.rs

Lines changed: 75 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -108,56 +108,62 @@ fn parse_js_ts_config(source: &str, extension: &str) -> StaticConfig {
108108
return None;
109109
}
110110

111-
extract_default_export_fields(&result.program)
111+
extract_config_fields(&result.program)
112112
}
113113

114-
/// Find the default export in a parsed program and extract its object fields.
114+
/// Find the config object in a parsed program and extract its fields.
115115
///
116-
/// Returns `None` if no `export default` is found or the exported value is not
117-
/// an object literal (or `defineConfig({...})` call).
118-
///
119-
/// Supports two patterns:
116+
/// Searches for the config value in the following patterns (in order):
120117
/// 1. `export default defineConfig({ ... })`
121118
/// 2. `export default { ... }`
122-
fn extract_default_export_fields(program: &Program<'_>) -> StaticConfig {
119+
/// 3. `module.exports = defineConfig({ ... })`
120+
/// 4. `module.exports = { ... }`
121+
fn extract_config_fields(program: &Program<'_>) -> StaticConfig {
123122
for stmt in &program.body {
124-
let Statement::ExportDefaultDeclaration(decl) = stmt else {
125-
continue;
126-
};
123+
// ESM: export default ...
124+
if let Statement::ExportDefaultDeclaration(decl) = stmt {
125+
if let Some(expr) = decl.declaration.as_expression() {
126+
return extract_config_from_expr(expr);
127+
}
128+
// export default class/function — not analyzable
129+
return None;
130+
}
127131

128-
let Some(expr) = decl.declaration.as_expression() else {
129-
continue;
130-
};
132+
// CJS: module.exports = ...
133+
if let Statement::ExpressionStatement(expr_stmt) = stmt
134+
&& let Expression::AssignmentExpression(assign) = &expr_stmt.expression
135+
&& assign.left.as_member_expression().is_some_and(|m| {
136+
m.object().is_specific_id("module") && m.static_property_name() == Some("exports")
137+
})
138+
{
139+
return extract_config_from_expr(&assign.right);
140+
}
141+
}
131142

132-
// Unwrap parenthesized expressions
133-
let expr = expr.without_parentheses();
143+
None
144+
}
134145

135-
match expr {
136-
// Pattern: export default defineConfig({ ... })
137-
Expression::CallExpression(call) => {
138-
if !call.callee.is_specific_id("defineConfig") {
139-
// Unknown function call — not analyzable
140-
return None;
141-
}
142-
if let Some(first_arg) = call.arguments.first()
143-
&& let Some(Expression::ObjectExpression(obj)) = first_arg.as_expression()
144-
{
145-
return Some(extract_object_fields(obj));
146-
}
147-
// defineConfig() with non-object arg — not analyzable
146+
/// Extract the config object from an expression that is either:
147+
/// - `defineConfig({ ... })` → extract the object argument
148+
/// - `{ ... }` → extract directly
149+
/// - anything else → not analyzable
150+
fn extract_config_from_expr(expr: &Expression<'_>) -> StaticConfig {
151+
let expr = expr.without_parentheses();
152+
match expr {
153+
Expression::CallExpression(call) => {
154+
if !call.callee.is_specific_id("defineConfig") {
148155
return None;
149156
}
150-
// Pattern: export default { ... }
151-
Expression::ObjectExpression(obj) => {
157+
if let Some(first_arg) = call.arguments.first()
158+
&& let Some(Expression::ObjectExpression(obj)) = first_arg.as_expression()
159+
{
152160
return Some(extract_object_fields(obj));
153161
}
154-
// e.g. export default 42, export default someVar — not analyzable
155-
_ => return None,
162+
None
156163
}
164+
Expression::ObjectExpression(obj) => Some(extract_object_fields(obj)),
165+
_ => None,
157166
}
158-
159-
// No export default found
160-
None
161167
}
162168

163169
/// Extract fields from an object expression, converting each value to JSON.
@@ -403,6 +409,40 @@ mod tests {
403409
assert_json(&result, "lint", serde_json::json!({ "plugins": ["a"] }));
404410
}
405411

412+
// ── module.exports = { ... } ───────────────────────────────────────
413+
414+
#[test]
415+
fn module_exports_object() {
416+
let result = parse_js_ts_config("module.exports = { run: { cache: true } }", "cjs")
417+
.expect("expected analyzable config");
418+
assert_json(&result, "run", serde_json::json!({ "cache": true }));
419+
}
420+
421+
#[test]
422+
fn module_exports_define_config() {
423+
let result = parse_js_ts_config(
424+
r"
425+
const { defineConfig } = require('vite-plus');
426+
module.exports = defineConfig({
427+
run: { cacheScripts: true },
428+
});
429+
",
430+
"cjs",
431+
)
432+
.expect("expected analyzable config");
433+
assert_json(&result, "run", serde_json::json!({ "cacheScripts": true }));
434+
}
435+
436+
#[test]
437+
fn module_exports_non_object() {
438+
assert!(parse_js_ts_config("module.exports = 42;", "cjs").is_none());
439+
}
440+
441+
#[test]
442+
fn module_exports_unknown_call() {
443+
assert!(parse_js_ts_config("module.exports = otherFn({ a: 1 });", "cjs").is_none());
444+
}
445+
406446
// ── Primitive values ────────────────────────────────────────────────
407447

408448
#[test]

0 commit comments

Comments
 (0)