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:: {
9+ BindingPattern , Expression , ImportDeclarationSpecifier , ImportOrExportKind , ObjectPropertyKind ,
10+ Program , Statement , VariableDeclarationKind ,
11+ } ;
912use oxc_parser:: Parser ;
1013use oxc_span:: SourceType ;
1114use rustc_hash:: FxHashMap ;
1215use vite_path:: AbsolutePath ;
1316
17+ /// The package that exports the `defineConfig` helper static extraction trusts.
18+ const VITE_PLUS_PACKAGE : & str = "vite-plus" ;
19+ /// The name of the config helper static extraction trusts.
20+ const DEFINE_CONFIG : & str = "defineConfig" ;
21+
1422/// The result of statically analyzing a single config field's value.
1523#[ derive( Debug , Clone , PartialEq , Eq ) ]
1624pub enum FieldValue {
@@ -139,16 +147,20 @@ fn parse_js_ts_config(source: &str, extension: &str) -> FieldMap {
139147/// Find the config object in a parsed program and extract its fields.
140148///
141149/// Searches for the config value in the following patterns (in order):
142- /// 1. `export default defineConfig({ ... })`
150+ /// 1. `export default defineConfig({ ... })` with `defineConfig` trusted from `vite-plus`
143151/// 2. `export default { ... }`
144- /// 3. `module.exports = defineConfig({ ... })`
152+ /// 3. `module.exports = defineConfig({ ... })` with `defineConfig` trusted from `vite-plus`
145153/// 4. `module.exports = { ... }`
146154fn extract_config_fields ( program : & Program < ' _ > ) -> FieldMap {
155+ let has_trusted_define_config_binding = program. body . iter ( ) . any ( |stmt| {
156+ is_trusted_define_config_import ( stmt) || has_trusted_define_config_cjs_binding ( stmt)
157+ } ) ;
158+
147159 for stmt in & program. body {
148160 // ESM: export default ...
149161 if let Statement :: ExportDefaultDeclaration ( decl) = stmt {
150162 if let Some ( expr) = decl. declaration . as_expression ( ) {
151- return extract_config_from_expr ( expr) ;
163+ return extract_config_from_expr ( expr, has_trusted_define_config_binding ) ;
152164 }
153165 // export default class/function — not analyzable
154166 return FieldMap :: unanalyzable ( ) ;
@@ -161,7 +173,7 @@ fn extract_config_fields(program: &Program<'_>) -> FieldMap {
161173 m. object ( ) . is_specific_id ( "module" ) && m. static_property_name ( ) == Some ( "exports" )
162174 } )
163175 {
164- return extract_config_from_expr ( & assign. right ) ;
176+ return extract_config_from_expr ( & assign. right , has_trusted_define_config_binding ) ;
165177 }
166178 }
167179
@@ -175,13 +187,19 @@ fn extract_config_fields(program: &Program<'_>) -> FieldMap {
175187/// - `defineConfig(function() { return { ... }; })` → extract from return statement
176188/// - `{ ... }` → extract directly
177189/// - anything else → not analyzable
178- fn extract_config_from_expr ( expr : & Expression < ' _ > ) -> FieldMap {
190+ fn extract_config_from_expr (
191+ expr : & Expression < ' _ > ,
192+ has_trusted_define_config_binding : bool ,
193+ ) -> FieldMap {
179194 let expr = expr. without_parentheses ( ) ;
180195 match expr {
181196 Expression :: CallExpression ( call) => {
182197 if !call. callee . is_specific_id ( "defineConfig" ) {
183198 return FieldMap :: unanalyzable ( ) ;
184199 }
200+ if !has_trusted_define_config_binding {
201+ return FieldMap :: unanalyzable ( ) ;
202+ }
185203 let Some ( first_arg) = call. arguments . first ( ) else {
186204 return FieldMap :: unanalyzable ( ) ;
187205 } ;
@@ -246,6 +264,74 @@ fn extract_config_from_function_body(body: &oxc_ast::ast::FunctionBody<'_>) -> F
246264 FieldMap :: unanalyzable ( )
247265}
248266
267+ fn is_trusted_define_config_import ( stmt : & Statement < ' _ > ) -> bool {
268+ let Statement :: ImportDeclaration ( import_decl) = stmt else {
269+ return false ;
270+ } ;
271+ if import_decl. import_kind != ImportOrExportKind :: Value
272+ || import_decl. source . value != VITE_PLUS_PACKAGE
273+ {
274+ return false ;
275+ }
276+ import_decl. specifiers . as_ref ( ) . is_some_and ( |specifiers| {
277+ specifiers. iter ( ) . any ( |specifier| {
278+ let ImportDeclarationSpecifier :: ImportSpecifier ( specifier) = specifier else {
279+ return false ;
280+ } ;
281+ specifier. import_kind == ImportOrExportKind :: Value
282+ && specifier. local . name == DEFINE_CONFIG
283+ && specifier. imported . name ( ) == DEFINE_CONFIG
284+ } )
285+ } )
286+ }
287+
288+ fn has_trusted_define_config_cjs_binding ( stmt : & Statement < ' _ > ) -> bool {
289+ match stmt {
290+ Statement :: VariableDeclaration ( decl) => {
291+ decl. kind == VariableDeclarationKind :: Const
292+ && decl. declarations . iter ( ) . any ( |declarator| {
293+ declarator. init . as_ref ( ) . is_some_and ( |init| match & declarator. id {
294+ BindingPattern :: BindingIdentifier ( id) => {
295+ id. name == DEFINE_CONFIG && is_require_vite_plus_define_config ( init)
296+ }
297+ BindingPattern :: ObjectPattern ( pattern) => {
298+ is_require_vite_plus ( init)
299+ && pattern. properties . iter ( ) . any ( |prop| {
300+ !prop. computed
301+ && prop
302+ . key
303+ . static_name ( )
304+ . is_some_and ( |key| key == DEFINE_CONFIG )
305+ && matches ! (
306+ & prop. value,
307+ BindingPattern :: BindingIdentifier ( id)
308+ if id. name == DEFINE_CONFIG
309+ )
310+ } )
311+ }
312+ _ => false ,
313+ } )
314+ } )
315+ }
316+ _ => false ,
317+ }
318+ }
319+
320+ fn is_require_vite_plus_define_config ( expr : & Expression < ' _ > ) -> bool {
321+ expr. without_parentheses ( ) . as_member_expression ( ) . is_some_and ( |member| {
322+ member. static_property_name ( ) == Some ( DEFINE_CONFIG )
323+ && is_require_vite_plus ( member. object ( ) )
324+ } )
325+ }
326+
327+ fn is_require_vite_plus ( expr : & Expression < ' _ > ) -> bool {
328+ matches ! (
329+ expr. without_parentheses( ) ,
330+ Expression :: CallExpression ( call)
331+ if call. common_js_require( ) . is_some_and( |source| source. value == VITE_PLUS_PACKAGE )
332+ )
333+ }
334+
249335/// Count `return` statements recursively in a slice of statements.
250336/// Does not descend into nested function/arrow expressions (they have their own returns).
251337fn count_returns_in_stmts ( stmts : & [ Statement < ' _ > ] ) -> usize {
@@ -529,6 +615,19 @@ mod tests {
529615 assert_json ( & result, "num" , serde_json:: json!( 42 ) ) ;
530616 }
531617
618+ #[ test]
619+ fn plain_export_default_object_ignores_unrelated_define_config_import ( ) {
620+ let result = parse (
621+ r"
622+ import { defineConfig } from 'vite';
623+ export default {
624+ run: { cacheScripts: true },
625+ };
626+ " ,
627+ ) ;
628+ assert_json ( & result, "run" , serde_json:: json!( { "cacheScripts" : true } ) ) ;
629+ }
630+
532631 #[ test]
533632 fn export_default_empty_object ( ) {
534633 let result = parse ( "export default {}" ) ;
@@ -552,6 +651,32 @@ mod tests {
552651 assert_json ( & result, "lint" , serde_json:: json!( { "plugins" : [ "a" ] } ) ) ;
553652 }
554653
654+ #[ test]
655+ fn define_config_import_from_other_package_is_non_static ( ) {
656+ let result = parse (
657+ r"
658+ import { defineConfig } from 'vite';
659+ export default defineConfig({
660+ run: { cacheScripts: true },
661+ });
662+ " ,
663+ ) ;
664+ assert_non_static ( & result, "run" ) ;
665+ }
666+
667+ #[ test]
668+ fn define_config_type_import_from_vite_plus_is_non_static ( ) {
669+ let result = parse (
670+ r"
671+ import type { defineConfig } from 'vite-plus';
672+ export default defineConfig({
673+ run: { cacheScripts: true },
674+ });
675+ " ,
676+ ) ;
677+ assert_non_static ( & result, "run" ) ;
678+ }
679+
555680 // ── module.exports = { ... } ───────────────────────────────────────
556681
557682 #[ test]
@@ -561,7 +686,7 @@ mod tests {
561686 }
562687
563688 #[ test]
564- fn module_exports_define_config ( ) {
689+ fn module_exports_define_config_destructured_require ( ) {
565690 let result = parse_js_ts_config (
566691 r"
567692 const { defineConfig } = require('vite-plus');
@@ -574,6 +699,20 @@ mod tests {
574699 assert_json ( & result, "run" , serde_json:: json!( { "cacheScripts" : true } ) ) ;
575700 }
576701
702+ #[ test]
703+ fn module_exports_define_config_member_require ( ) {
704+ let result = parse_js_ts_config (
705+ r"
706+ const defineConfig = require('vite-plus').defineConfig;
707+ module.exports = defineConfig({
708+ run: { cacheScripts: true },
709+ });
710+ " ,
711+ "cjs" ,
712+ ) ;
713+ assert_json ( & result, "run" , serde_json:: json!( { "cacheScripts" : true } ) ) ;
714+ }
715+
577716 #[ test]
578717 fn module_exports_non_object ( ) {
579718 assert_non_static ( & parse_js_ts_config ( "module.exports = 42;" , "cjs" ) , "run" ) ;
@@ -944,6 +1083,8 @@ mod tests {
9441083 fn define_config_arrow_block_body ( ) {
9451084 let result = parse (
9461085 r"
1086+ import { defineConfig } from 'vite-plus';
1087+
9471088 export default defineConfig(({ mode }) => {
9481089 const env = loadEnv(mode, process.cwd(), '');
9491090 return {
@@ -961,6 +1102,8 @@ mod tests {
9611102 fn define_config_arrow_expression_body ( ) {
9621103 let result = parse (
9631104 r"
1105+ import { defineConfig } from 'vite-plus';
1106+
9641107 export default defineConfig(() => ({
9651108 run: { cacheScripts: true },
9661109 build: { outDir: 'dist' },
@@ -975,6 +1118,8 @@ mod tests {
9751118 fn define_config_function_expression ( ) {
9761119 let result = parse (
9771120 r"
1121+ import { defineConfig } from 'vite-plus';
1122+
9781123 export default defineConfig(function() {
9791124 return {
9801125 run: { cacheScripts: true },
0 commit comments