Skip to content

Commit c037f46

Browse files
branchseerclaude
andcommitted
feat(static-config): resolve indirect exports via top-level identifier lookup
Handles the common pattern where the config object is assigned to a variable before being exported or returned: const config = defineConfig({ ... }); export default config; // ESM indirect export module.exports = config; // CJS indirect export return config; // inside defineConfig(fn) callback Resolution scans top-level VariableDeclarator nodes by name (simple binding identifiers only; destructured patterns are skipped). One level of indirection is supported — chained references (const a = b; export default a) are not resolved and fall back to NAPI as before. Fixes tanstack-start-helloworld's ~1.3s config load time. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a9bdd67 commit c037f46

File tree

1 file changed

+179
-13
lines changed
  • crates/vite_static_config/src

1 file changed

+179
-13
lines changed

crates/vite_static_config/src/lib.rs

Lines changed: 179 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//! config like `run` without needing a Node.js runtime.
66
77
use oxc_allocator::Allocator;
8-
use oxc_ast::ast::{Expression, ObjectPropertyKind, Program, Statement};
8+
use oxc_ast::ast::{BindingPattern, Expression, ObjectPropertyKind, Program, Statement};
99
use oxc_parser::Parser;
1010
use oxc_span::SourceType;
1111
use 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

Comments
 (0)