55//! config like `run` without needing a Node.js runtime.
66
77use oxc_allocator:: Allocator ;
8- use oxc_ast:: ast:: { Expression , ObjectPropertyKind , Program , Statement } ;
8+ use oxc_ast:: ast:: { BindingPattern , Expression , ObjectPropertyKind , Program , Statement } ;
99use oxc_parser:: Parser ;
1010use oxc_span:: SourceType ;
1111use rustc_hash:: FxHashMap ;
@@ -110,19 +110,39 @@ fn parse_js_ts_config(source: &str, extension: &str) -> StaticConfig {
110110 extract_config_fields ( & result. program )
111111}
112112
113+ /// Scan top-level statements for a `const`/`let`/`var` declarator whose simple
114+ /// binding identifier matches `name`, and return a reference to its initializer.
115+ ///
116+ /// Returns `None` if no match is found or the declarator has no initializer.
117+ /// Destructured bindings (object/array patterns) are intentionally skipped.
118+ fn find_top_level_init < ' a > ( name : & str , stmts : & ' a [ Statement < ' a > ] ) -> Option < & ' a Expression < ' a > > {
119+ for stmt in stmts {
120+ let Statement :: VariableDeclaration ( decl) = stmt else { continue } ;
121+ for declarator in & decl. declarations {
122+ let BindingPattern :: BindingIdentifier ( ident) = & declarator. id else { continue } ;
123+ if ident. name == name {
124+ return declarator. init . as_ref ( ) ;
125+ }
126+ }
127+ }
128+ None
129+ }
130+
113131/// Find the config object in a parsed program and extract its fields.
114132///
115133/// Searches for the config value in the following patterns (in order):
116134/// 1. `export default defineConfig({ ... })`
117135/// 2. `export default { ... }`
118- /// 3. `module.exports = defineConfig({ ... })`
119- /// 4. `module.exports = { ... }`
120- fn extract_config_fields ( program : & Program < ' _ > ) -> StaticConfig {
136+ /// 3. `export default config` where `config` is a top-level variable
137+ /// 4. `module.exports = defineConfig({ ... })`
138+ /// 5. `module.exports = { ... }`
139+ /// 6. `module.exports = config` where `config` is a top-level variable
140+ fn extract_config_fields < ' a > ( program : & ' a Program < ' a > ) -> StaticConfig {
121141 for stmt in & program. body {
122142 // ESM: export default ...
123143 if let Statement :: ExportDefaultDeclaration ( decl) = stmt {
124144 if let Some ( expr) = decl. declaration . as_expression ( ) {
125- return extract_config_from_expr ( expr) ;
145+ return extract_config_from_expr ( expr, & program . body ) ;
126146 }
127147 // export default class/function — not analyzable
128148 return None ;
@@ -135,7 +155,7 @@ fn extract_config_fields(program: &Program<'_>) -> StaticConfig {
135155 m. object ( ) . is_specific_id ( "module" ) && m. static_property_name ( ) == Some ( "exports" )
136156 } )
137157 {
138- return extract_config_from_expr ( & assign. right ) ;
158+ return extract_config_from_expr ( & assign. right , & program . body ) ;
139159 }
140160 }
141161
@@ -148,8 +168,12 @@ fn extract_config_fields(program: &Program<'_>) -> StaticConfig {
148168/// - `defineConfig(() => { return { ... }; })` → extract from return statement
149169/// - `defineConfig(function() { return { ... }; })` → extract from return statement
150170/// - `{ ... }` → extract directly
171+ /// - `identifier` → look up in `stmts`, then extract (one level of indirection only)
151172/// - anything else → not analyzable
152- fn extract_config_from_expr ( expr : & Expression < ' _ > ) -> StaticConfig {
173+ fn extract_config_from_expr < ' a > (
174+ expr : & ' a Expression < ' a > ,
175+ stmts : & ' a [ Statement < ' a > ] ,
176+ ) -> StaticConfig {
153177 let expr = expr. without_parentheses ( ) ;
154178 match expr {
155179 Expression :: CallExpression ( call) => {
@@ -161,15 +185,21 @@ fn extract_config_from_expr(expr: &Expression<'_>) -> StaticConfig {
161185 match first_arg_expr {
162186 Expression :: ObjectExpression ( obj) => Some ( extract_object_fields ( obj) ) ,
163187 Expression :: ArrowFunctionExpression ( arrow) => {
164- extract_config_from_function_body ( & arrow. body )
188+ extract_config_from_function_body ( & arrow. body , stmts )
165189 }
166190 Expression :: FunctionExpression ( func) => {
167- extract_config_from_function_body ( func. body . as_ref ( ) ?)
191+ extract_config_from_function_body ( func. body . as_ref ( ) ?, stmts )
168192 }
169193 _ => None ,
170194 }
171195 }
172196 Expression :: ObjectExpression ( obj) => Some ( extract_object_fields ( obj) ) ,
197+ // Resolve a top-level identifier to its initializer (one level of indirection).
198+ // Pass empty stmts on the recursive call to prevent chaining (const a = b; export default a).
199+ Expression :: Identifier ( ident) if !stmts. is_empty ( ) => {
200+ let init = find_top_level_init ( & ident. name , stmts) ?;
201+ extract_config_from_expr ( init, & [ ] )
202+ }
173203 _ => None ,
174204 }
175205}
@@ -182,7 +212,13 @@ fn extract_config_from_expr(expr: &Expression<'_>) -> StaticConfig {
182212///
183213/// Returns `None` (not analyzable) if the body contains multiple `return` statements
184214/// (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 {
215+ ///
216+ /// `module_stmts` is the program's top-level statement list, used as a fallback when
217+ /// resolving an identifier in a `return <identifier>` statement.
218+ fn extract_config_from_function_body < ' a > (
219+ body : & ' a oxc_ast:: ast:: FunctionBody < ' a > ,
220+ module_stmts : & ' a [ Statement < ' a > ] ,
221+ ) -> StaticConfig {
186222 // Reject functions with multiple returns — the config depends on control flow.
187223 if count_returns_in_stmts ( & body. statements ) > 1 {
188224 return None ;
@@ -192,10 +228,19 @@ fn extract_config_from_function_body(body: &oxc_ast::ast::FunctionBody<'_>) -> S
192228 match stmt {
193229 Statement :: ReturnStatement ( ret) => {
194230 let arg = ret. argument . as_ref ( ) ?;
195- if let Expression :: ObjectExpression ( obj) = arg. without_parentheses ( ) {
196- return Some ( extract_object_fields ( obj) ) ;
231+ match arg. without_parentheses ( ) {
232+ Expression :: ObjectExpression ( obj) => return Some ( extract_object_fields ( obj) ) ,
233+ Expression :: Identifier ( ident) => {
234+ // Look for the binding in the function body first, then at module level.
235+ let init = find_top_level_init ( & ident. name , & body. statements )
236+ . or_else ( || find_top_level_init ( & ident. name , module_stmts) ) ?;
237+ if let Expression :: ObjectExpression ( obj) = init. without_parentheses ( ) {
238+ return Some ( extract_object_fields ( obj) ) ;
239+ }
240+ return None ;
241+ }
242+ _ => return None ,
197243 }
198- return None ;
199244 }
200245 Statement :: ExpressionStatement ( expr_stmt) => {
201246 // Concise arrow: `() => ({ ... })` is represented as ExpressionStatement
@@ -999,4 +1044,125 @@ mod tests {
9991044 ) ;
10001045 assert_json ( & result, "run" , serde_json:: json!( { "cacheScripts" : true } ) ) ;
10011046 }
1047+
1048+ // ── Indirect exports (identifier resolution) ─────────────────────────
1049+
1050+ #[ test]
1051+ fn export_default_identifier_object ( ) {
1052+ let result = parse (
1053+ r"
1054+ const config = { run: { cacheScripts: true } };
1055+ export default config;
1056+ " ,
1057+ ) ;
1058+ assert_json ( & result, "run" , serde_json:: json!( { "cacheScripts" : true } ) ) ;
1059+ }
1060+
1061+ #[ test]
1062+ fn export_default_identifier_define_config ( ) {
1063+ // Real-world tanstack-start-helloworld pattern
1064+ let result = parse (
1065+ r"
1066+ import { defineConfig } from 'vite-plus';
1067+ const config = defineConfig({
1068+ run: { cacheScripts: true },
1069+ plugins: [devtools(), nitro()],
1070+ });
1071+ export default config;
1072+ " ,
1073+ ) ;
1074+ assert_json ( & result, "run" , serde_json:: json!( { "cacheScripts" : true } ) ) ;
1075+ assert_non_static ( & result, "plugins" ) ;
1076+ }
1077+
1078+ #[ test]
1079+ fn module_exports_identifier_object ( ) {
1080+ let result = parse_js_ts_config (
1081+ r"
1082+ const config = { run: { cache: true } };
1083+ module.exports = config;
1084+ " ,
1085+ "cjs" ,
1086+ )
1087+ . expect ( "expected analyzable config" ) ;
1088+ assert_json ( & result, "run" , serde_json:: json!( { "cache" : true } ) ) ;
1089+ }
1090+
1091+ #[ test]
1092+ fn module_exports_identifier_define_config ( ) {
1093+ let result = parse_js_ts_config (
1094+ r"
1095+ const { defineConfig } = require('vite-plus');
1096+ const config = defineConfig({ run: { cacheScripts: true } });
1097+ module.exports = config;
1098+ " ,
1099+ "cjs" ,
1100+ )
1101+ . expect ( "expected analyzable config" ) ;
1102+ assert_json ( & result, "run" , serde_json:: json!( { "cacheScripts" : true } ) ) ;
1103+ }
1104+
1105+ #[ test]
1106+ fn define_config_callback_return_local_identifier ( ) {
1107+ let result = parse (
1108+ r"
1109+ export default defineConfig(({ mode }) => {
1110+ const obj = { run: { cacheScripts: true }, plugins: [vue()] };
1111+ return obj;
1112+ });
1113+ " ,
1114+ ) ;
1115+ assert_json ( & result, "run" , serde_json:: json!( { "cacheScripts" : true } ) ) ;
1116+ assert_non_static ( & result, "plugins" ) ;
1117+ }
1118+
1119+ #[ test]
1120+ fn define_config_callback_return_module_level_identifier ( ) {
1121+ let result = parse (
1122+ r"
1123+ const shared = { run: { cacheScripts: true } };
1124+ export default defineConfig(() => {
1125+ return shared;
1126+ });
1127+ " ,
1128+ ) ;
1129+ assert_json ( & result, "run" , serde_json:: json!( { "cacheScripts" : true } ) ) ;
1130+ }
1131+
1132+ #[ test]
1133+ fn export_default_identifier_undeclared_is_none ( ) {
1134+ // Identifier not declared in file — not analyzable
1135+ assert ! ( parse_js_ts_config( "export default config;" , "ts" ) . is_none( ) ) ;
1136+ }
1137+
1138+ #[ test]
1139+ fn export_default_identifier_no_init_is_none ( ) {
1140+ // Variable declared without initializer — not analyzable
1141+ assert ! (
1142+ parse_js_ts_config(
1143+ r"
1144+ let config;
1145+ export default config;
1146+ " ,
1147+ "ts" ,
1148+ )
1149+ . is_none( )
1150+ ) ;
1151+ }
1152+
1153+ #[ test]
1154+ fn export_default_chained_identifier_is_none ( ) {
1155+ // Chained indirection (const a = b) — only one level is resolved
1156+ assert ! (
1157+ parse_js_ts_config(
1158+ r"
1159+ const inner = { run: {} };
1160+ const config = inner;
1161+ export default config;
1162+ " ,
1163+ "ts" ,
1164+ )
1165+ . is_none( )
1166+ ) ;
1167+ }
10021168}
0 commit comments