@@ -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 .
3435pub 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]
6970pub 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
144152fn 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