Skip to content

Commit 19e8942

Browse files
authored
Merge branch 'main' into fix/create-org-manifest-tarball-fallback
2 parents 4056a38 + b9b15c4 commit 19e8942

8 files changed

Lines changed: 191 additions & 7 deletions

File tree

crates/vite_static_config/src/lib.rs

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@
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::{
9+
BindingPattern, Expression, ImportDeclarationSpecifier, ImportOrExportKind, ObjectPropertyKind,
10+
Program, Statement, VariableDeclarationKind,
11+
};
912
use oxc_parser::Parser;
1013
use oxc_span::SourceType;
1114
use rustc_hash::FxHashMap;
1215
use 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)]
1624
pub 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 = { ... }`
146154
fn 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).
251337
fn 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 },
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export function defineConfig(_config: unknown) {
2+
return {
3+
run: {
4+
tasks: {
5+
selected: {
6+
command: 'node runtime.js',
7+
dependsOn: [],
8+
},
9+
},
10+
},
11+
};
12+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "run-config-custom-define-config",
3+
"private": true
4+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log('runtime defineConfig result');
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
> vp run selected # defineConfig from a local module must use runtime config resolution
2+
$ node runtime.js
3+
runtime defineConfig result
4+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log('statically extracted config');
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"commands": [
3+
"vp run selected # defineConfig from a local module must use runtime config resolution"
4+
]
5+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineConfig } from './define-config';
2+
3+
export default defineConfig({
4+
run: {
5+
tasks: {
6+
selected: {
7+
command: 'node static.js',
8+
dependsOn: [],
9+
},
10+
},
11+
},
12+
});

0 commit comments

Comments
 (0)