Skip to content

Commit ea08b34

Browse files
branchseerclaude
andcommitted
feat(static-config): support defineConfig(fn) and skip NAPI when no config file
Two improvements to static config extraction: 1. When no vite.config.* file exists in a workspace package, return immediately instead of falling through to the NAPI/JS callback. This eliminates ~165ms cold Node.js init + ~3ms/pkg warm overhead for monorepo packages without config files. 2. Support defineConfig(fn) where fn is an arrow function or function expression. The extractor now locates the return expression inside the function body and extracts fields from it. This handles configs like `defineConfig(({ mode }) => { return { run: {...} }; })`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9052846 commit ea08b34

File tree

2 files changed

+127
-5
lines changed
  • crates/vite_static_config/src
  • packages/cli/binding/src

2 files changed

+127
-5
lines changed

crates/vite_static_config/src/lib.rs

Lines changed: 122 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ const CONFIG_FILE_NAMES: &[&str] = &[
4949
"vite.config.cts",
5050
];
5151

52+
/// Check whether any vite config file exists in the given directory.
53+
#[must_use]
54+
pub fn has_config_file(dir: &AbsolutePath) -> bool {
55+
resolve_config_path(dir).is_some()
56+
}
57+
5258
/// Resolve the vite config file path in the given directory.
5359
///
5460
/// Tries each config file name in priority order and returns the first one that exists.
@@ -139,6 +145,9 @@ fn extract_config_fields(program: &Program<'_>) -> StaticConfig {
139145

140146
/// Extract the config object from an expression that is either:
141147
/// - `defineConfig({ ... })` → extract the object argument
148+
/// - `defineConfig(() => ({ ... }))` → extract from arrow function expression body
149+
/// - `defineConfig(() => { return { ... }; })` → extract from return statement
150+
/// - `defineConfig(function() { return { ... }; })` → extract from return statement
142151
/// - `{ ... }` → extract directly
143152
/// - anything else → not analyzable
144153
fn extract_config_from_expr(expr: &Expression<'_>) -> StaticConfig {
@@ -148,18 +157,54 @@ fn extract_config_from_expr(expr: &Expression<'_>) -> StaticConfig {
148157
if !call.callee.is_specific_id("defineConfig") {
149158
return None;
150159
}
151-
if let Some(first_arg) = call.arguments.first()
152-
&& let Some(Expression::ObjectExpression(obj)) = first_arg.as_expression()
153-
{
154-
return Some(extract_object_fields(obj));
160+
let first_arg = call.arguments.first()?;
161+
let first_arg_expr = first_arg.as_expression()?;
162+
match first_arg_expr {
163+
Expression::ObjectExpression(obj) => Some(extract_object_fields(obj)),
164+
Expression::ArrowFunctionExpression(arrow) => {
165+
extract_config_from_function_body(&arrow.body)
166+
}
167+
Expression::FunctionExpression(func) => {
168+
extract_config_from_function_body(func.body.as_ref()?)
169+
}
170+
_ => None,
155171
}
156-
None
157172
}
158173
Expression::ObjectExpression(obj) => Some(extract_object_fields(obj)),
159174
_ => None,
160175
}
161176
}
162177

178+
/// Extract the config object from the body of a function passed to `defineConfig`.
179+
///
180+
/// Handles two patterns:
181+
/// - Concise arrow body: `() => ({ ... })` — body has a single `ExpressionStatement`
182+
/// - Block body: `() => { ... return { ... }; }` — find top-level `ReturnStatement`
183+
fn extract_config_from_function_body(body: &oxc_ast::ast::FunctionBody<'_>) -> StaticConfig {
184+
for stmt in &body.statements {
185+
match stmt {
186+
Statement::ReturnStatement(ret) => {
187+
if let Some(arg) = &ret.argument {
188+
if let Expression::ObjectExpression(obj) = arg.without_parentheses() {
189+
return Some(extract_object_fields(obj));
190+
}
191+
}
192+
return None;
193+
}
194+
Statement::ExpressionStatement(expr_stmt) => {
195+
// Concise arrow: `() => ({ ... })` is represented as ExpressionStatement
196+
if let Expression::ObjectExpression(obj) =
197+
expr_stmt.expression.without_parentheses()
198+
{
199+
return Some(extract_object_fields(obj));
200+
}
201+
}
202+
_ => {}
203+
}
204+
}
205+
None
206+
}
207+
163208
/// Extract fields from an object expression, converting each value to JSON.
164209
/// Fields whose values cannot be represented as pure JSON are recorded as
165210
/// [`FieldValue::NonStatic`]. Spread elements and computed properties
@@ -720,6 +765,78 @@ mod tests {
720765
assert_json(&result, "b", serde_json::json!(2));
721766
}
722767

768+
// ── defineConfig with function argument ────────────────────────────
769+
770+
#[test]
771+
fn define_config_arrow_block_body() {
772+
let result = parse(
773+
r"
774+
export default defineConfig(({ mode }) => {
775+
const env = loadEnv(mode, process.cwd(), '');
776+
return {
777+
run: { cacheScripts: true },
778+
plugins: [vue()],
779+
};
780+
});
781+
",
782+
);
783+
assert_json(&result, "run", serde_json::json!({ "cacheScripts": true }));
784+
assert_non_static(&result, "plugins");
785+
}
786+
787+
#[test]
788+
fn define_config_arrow_expression_body() {
789+
let result = parse(
790+
r"
791+
export default defineConfig(() => ({
792+
run: { cacheScripts: true },
793+
build: { outDir: 'dist' },
794+
}));
795+
",
796+
);
797+
assert_json(&result, "run", serde_json::json!({ "cacheScripts": true }));
798+
assert_json(&result, "build", serde_json::json!({ "outDir": "dist" }));
799+
}
800+
801+
#[test]
802+
fn define_config_function_expression() {
803+
let result = parse(
804+
r"
805+
export default defineConfig(function() {
806+
return {
807+
run: { cacheScripts: true },
808+
plugins: [react()],
809+
};
810+
});
811+
",
812+
);
813+
assert_json(&result, "run", serde_json::json!({ "cacheScripts": true }));
814+
assert_non_static(&result, "plugins");
815+
}
816+
817+
#[test]
818+
fn define_config_arrow_no_return_object() {
819+
// Arrow function that doesn't return an object literal
820+
assert!(parse_js_ts_config(
821+
r"
822+
export default defineConfig(({ mode }) => {
823+
return someFunction();
824+
});
825+
",
826+
"ts",
827+
)
828+
.is_none());
829+
}
830+
831+
#[test]
832+
fn define_config_arrow_empty_body() {
833+
assert!(parse_js_ts_config(
834+
"export default defineConfig(() => {});",
835+
"ts",
836+
)
837+
.is_none());
838+
}
839+
723840
// ── Not analyzable cases (return None) ──────────────────────────────
724841

725842
#[test]

packages/cli/binding/src/cli.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,11 @@ impl UserConfigLoader for VitePlusConfigLoader {
680680
}
681681
}
682682

683+
// If no config file exists, there's nothing for NAPI to evaluate either
684+
if !vite_static_config::has_config_file(package_path) {
685+
return Ok(None);
686+
}
687+
683688
// Fall back to NAPI-based config resolution
684689
let package_path_str = package_path
685690
.as_path()

0 commit comments

Comments
 (0)