Skip to content

Commit ecbafe2

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, resolve_static_config now returns an empty map (instead of None). The caller sees no `run` field and returns immediately, skipping 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 locates the return expression inside the function body and extracts fields from it. Functions with multiple return statements are rejected as not statically analyzable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9052846 commit ecbafe2

File tree

1 file changed

+201
-11
lines changed
  • crates/vite_static_config/src

1 file changed

+201
-11
lines changed

crates/vite_static_config/src/lib.rs

Lines changed: 201 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ pub enum FieldValue {
2323

2424
/// The result of statically analyzing a vite config file.
2525
///
26-
/// - `None` — the config is not analyzable (no config file found, parse error,
26+
/// - `None` — the config file exists but is not analyzable (parse error,
2727
/// no `export default`, or the default export is not an object literal).
2828
/// The caller should fall back to a runtime evaluation (e.g. NAPI).
29-
/// - `Some(map)` — the default export object was successfully located.
29+
/// - `Some(map)` — the config was successfully resolved.
30+
/// - Empty map — no config file was found (caller can skip runtime evaluation).
3031
/// - Key maps to [`FieldValue::Json`] — field value was extracted.
3132
/// - Key maps to [`FieldValue::NonStatic`] — field exists but its value
3233
/// cannot be represented as pure JSON.
33-
/// - Key absent — the field does not exist in the object.
34+
/// - Key absent — the field does not exist in the config.
3435
pub type StaticConfig = Option<FxHashMap<Box<str>, FieldValue>>;
3536

3637
/// Config file names to try, in priority order.
@@ -67,7 +68,11 @@ fn resolve_config_path(dir: &AbsolutePath) -> Option<vite_path::AbsolutePathBuf>
6768
/// See [`StaticConfig`] for the return type semantics.
6869
#[must_use]
6970
pub fn resolve_static_config(dir: &AbsolutePath) -> StaticConfig {
70-
let config_path = resolve_config_path(dir)?;
71+
let Some(config_path) = resolve_config_path(dir) else {
72+
// No config file found — return empty map so the caller can
73+
// skip runtime evaluation (NAPI) entirely.
74+
return Some(FxHashMap::default());
75+
};
7176
let source = std::fs::read_to_string(&config_path).ok()?;
7277

7378
let extension = config_path.as_path().extension().and_then(|e| e.to_str()).unwrap_or("");
@@ -139,6 +144,9 @@ fn extract_config_fields(program: &Program<'_>) -> StaticConfig {
139144

140145
/// Extract the config object from an expression that is either:
141146
/// - `defineConfig({ ... })` → extract the object argument
147+
/// - `defineConfig(() => ({ ... }))` → extract from arrow function expression body
148+
/// - `defineConfig(() => { return { ... }; })` → extract from return statement
149+
/// - `defineConfig(function() { return { ... }; })` → extract from return statement
142150
/// - `{ ... }` → extract directly
143151
/// - anything else → not analyzable
144152
fn extract_config_from_expr(expr: &Expression<'_>) -> StaticConfig {
@@ -148,18 +156,110 @@ fn extract_config_from_expr(expr: &Expression<'_>) -> StaticConfig {
148156
if !call.callee.is_specific_id("defineConfig") {
149157
return None;
150158
}
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));
159+
let first_arg = call.arguments.first()?;
160+
let first_arg_expr = first_arg.as_expression()?;
161+
match first_arg_expr {
162+
Expression::ObjectExpression(obj) => Some(extract_object_fields(obj)),
163+
Expression::ArrowFunctionExpression(arrow) => {
164+
extract_config_from_function_body(&arrow.body)
165+
}
166+
Expression::FunctionExpression(func) => {
167+
extract_config_from_function_body(func.body.as_ref()?)
168+
}
169+
_ => None,
155170
}
156-
None
157171
}
158172
Expression::ObjectExpression(obj) => Some(extract_object_fields(obj)),
159173
_ => None,
160174
}
161175
}
162176

177+
/// Extract the config object from the body of a function passed to `defineConfig`.
178+
///
179+
/// Handles two patterns:
180+
/// - Concise arrow body: `() => ({ ... })` — body has a single `ExpressionStatement`
181+
/// - Block body with exactly one return: `() => { ... return { ... }; }`
182+
///
183+
/// Returns `None` (not analyzable) if the body contains multiple `return` statements
184+
/// (at any nesting depth), since the returned config would depend on runtime control flow.
185+
fn extract_config_from_function_body(body: &oxc_ast::ast::FunctionBody<'_>) -> StaticConfig {
186+
// Reject functions with multiple returns — the config depends on control flow.
187+
if count_returns_in_stmts(&body.statements) > 1 {
188+
return None;
189+
}
190+
191+
for stmt in &body.statements {
192+
match stmt {
193+
Statement::ReturnStatement(ret) => {
194+
let arg = ret.argument.as_ref()?;
195+
if let Expression::ObjectExpression(obj) = arg.without_parentheses() {
196+
return Some(extract_object_fields(obj));
197+
}
198+
return None;
199+
}
200+
Statement::ExpressionStatement(expr_stmt) => {
201+
// Concise arrow: `() => ({ ... })` is represented as ExpressionStatement
202+
if let Expression::ObjectExpression(obj) =
203+
expr_stmt.expression.without_parentheses()
204+
{
205+
return Some(extract_object_fields(obj));
206+
}
207+
}
208+
_ => {}
209+
}
210+
}
211+
None
212+
}
213+
214+
/// Count `return` statements recursively in a slice of statements.
215+
/// Does not descend into nested function/arrow expressions (they have their own returns).
216+
fn count_returns_in_stmts(stmts: &[Statement<'_>]) -> usize {
217+
let mut count = 0;
218+
for stmt in stmts {
219+
count += count_returns_in_stmt(stmt);
220+
}
221+
count
222+
}
223+
224+
fn count_returns_in_stmt(stmt: &Statement<'_>) -> usize {
225+
match stmt {
226+
Statement::ReturnStatement(_) => 1,
227+
Statement::BlockStatement(block) => count_returns_in_stmts(&block.body),
228+
Statement::IfStatement(if_stmt) => {
229+
let mut n = count_returns_in_stmt(&if_stmt.consequent);
230+
if let Some(alt) = &if_stmt.alternate {
231+
n += count_returns_in_stmt(alt);
232+
}
233+
n
234+
}
235+
Statement::SwitchStatement(switch) => {
236+
let mut n = 0;
237+
for case in &switch.cases {
238+
n += count_returns_in_stmts(&case.consequent);
239+
}
240+
n
241+
}
242+
Statement::TryStatement(try_stmt) => {
243+
let mut n = count_returns_in_stmts(&try_stmt.block.body);
244+
if let Some(handler) = &try_stmt.handler {
245+
n += count_returns_in_stmts(&handler.body.body);
246+
}
247+
if let Some(finalizer) = &try_stmt.finalizer {
248+
n += count_returns_in_stmts(&finalizer.body);
249+
}
250+
n
251+
}
252+
Statement::ForStatement(s) => count_returns_in_stmt(&s.body),
253+
Statement::ForInStatement(s) => count_returns_in_stmt(&s.body),
254+
Statement::ForOfStatement(s) => count_returns_in_stmt(&s.body),
255+
Statement::WhileStatement(s) => count_returns_in_stmt(&s.body),
256+
Statement::DoWhileStatement(s) => count_returns_in_stmt(&s.body),
257+
Statement::LabeledStatement(s) => count_returns_in_stmt(&s.body),
258+
Statement::WithStatement(s) => count_returns_in_stmt(&s.body),
259+
_ => 0,
260+
}
261+
}
262+
163263
/// Extract fields from an object expression, converting each value to JSON.
164264
/// Fields whose values cannot be represented as pure JSON are recorded as
165265
/// [`FieldValue::NonStatic`]. Spread elements and computed properties
@@ -339,10 +439,11 @@ mod tests {
339439
}
340440

341441
#[test]
342-
fn returns_none_for_no_config() {
442+
fn returns_empty_map_for_no_config() {
343443
let dir = TempDir::new().unwrap();
344444
let dir_path = vite_path::AbsolutePathBuf::new(dir.path().to_path_buf()).unwrap();
345-
assert!(resolve_static_config(&dir_path).is_none());
445+
let result = resolve_static_config(&dir_path).unwrap();
446+
assert!(result.is_empty());
346447
}
347448

348449
// ── JSON config parsing ─────────────────────────────────────────────
@@ -720,6 +821,95 @@ mod tests {
720821
assert_json(&result, "b", serde_json::json!(2));
721822
}
722823

824+
// ── defineConfig with function argument ────────────────────────────
825+
826+
#[test]
827+
fn define_config_arrow_block_body() {
828+
let result = parse(
829+
r"
830+
export default defineConfig(({ mode }) => {
831+
const env = loadEnv(mode, process.cwd(), '');
832+
return {
833+
run: { cacheScripts: true },
834+
plugins: [vue()],
835+
};
836+
});
837+
",
838+
);
839+
assert_json(&result, "run", serde_json::json!({ "cacheScripts": true }));
840+
assert_non_static(&result, "plugins");
841+
}
842+
843+
#[test]
844+
fn define_config_arrow_expression_body() {
845+
let result = parse(
846+
r"
847+
export default defineConfig(() => ({
848+
run: { cacheScripts: true },
849+
build: { outDir: 'dist' },
850+
}));
851+
",
852+
);
853+
assert_json(&result, "run", serde_json::json!({ "cacheScripts": true }));
854+
assert_json(&result, "build", serde_json::json!({ "outDir": "dist" }));
855+
}
856+
857+
#[test]
858+
fn define_config_function_expression() {
859+
let result = parse(
860+
r"
861+
export default defineConfig(function() {
862+
return {
863+
run: { cacheScripts: true },
864+
plugins: [react()],
865+
};
866+
});
867+
",
868+
);
869+
assert_json(&result, "run", serde_json::json!({ "cacheScripts": true }));
870+
assert_non_static(&result, "plugins");
871+
}
872+
873+
#[test]
874+
fn define_config_arrow_no_return_object() {
875+
// Arrow function that doesn't return an object literal
876+
assert!(
877+
parse_js_ts_config(
878+
r"
879+
export default defineConfig(({ mode }) => {
880+
return someFunction();
881+
});
882+
",
883+
"ts",
884+
)
885+
.is_none()
886+
);
887+
}
888+
889+
#[test]
890+
fn define_config_arrow_multiple_returns() {
891+
// Multiple top-level returns → not analyzable
892+
assert!(
893+
parse_js_ts_config(
894+
r"
895+
export default defineConfig(({ mode }) => {
896+
if (mode === 'production') {
897+
return { run: { cacheScripts: true } };
898+
}
899+
return { run: { cacheScripts: false } };
900+
});
901+
",
902+
"ts",
903+
)
904+
.is_none()
905+
);
906+
}
907+
908+
#[test]
909+
fn define_config_arrow_empty_body() {
910+
assert!(parse_js_ts_config("export default defineConfig(() => {});", "ts",).is_none());
911+
}
912+
723913
// ── Not analyzable cases (return None) ──────────────────────────────
724914

725915
#[test]

0 commit comments

Comments
 (0)