Skip to content

Commit 9052846

Browse files
branchseerclaude
andcommitted
refactor(static-config): simplify f64_to_json_number and rename FieldValue
Rewrite f64_to_json_number to follow JSON.stringify semantics using serde_json's From<f64> for the NaN/Infinity→null fallback, and i64::try_from for safe integer conversion. Rename StaticFieldValue to FieldValue for brevity. Add tests for overflow-to-infinity and -0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 52fa070 commit 9052846

File tree

3 files changed

+45
-39
lines changed

3 files changed

+45
-39
lines changed

crates/vite_static_config/README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ without needing a Node.js runtime (NAPI).
1111
## Supported patterns
1212

1313
**ESM:**
14+
1415
```js
1516
export default { run: { tasks: { build: { command: "echo build" } } } }
1617
export default defineConfig({ run: { cacheScripts: true } })
1718
```
1819

1920
**CJS:**
21+
2022
```js
21-
module.exports = { run: { tasks: { build: { command: "echo build" } } } }
22-
module.exports = defineConfig({ run: { cacheScripts: true } })
23+
module.exports = { run: { tasks: { build: { command: 'echo build' } } } };
24+
module.exports = defineConfig({ run: { cacheScripts: true } });
2325
```
2426

2527
## Config file resolution
@@ -36,14 +38,14 @@ Searches for config files in the same order as Vite's
3638

3739
## Return type
3840

39-
`resolve_static_config` returns `Option<FxHashMap<Box<str>, StaticFieldValue>>`:
41+
`resolve_static_config` returns `Option<FxHashMap<Box<str>, FieldValue>>`:
4042

4143
- **`None`** — config is not statically analyzable (no config file, parse error, no
4244
`export default`/`module.exports`, or the exported value is not an object literal).
4345
Caller should fall back to runtime evaluation (e.g. NAPI).
4446
- **`Some(map)`** — config object was successfully located:
45-
- `StaticFieldValue::Json(value)` — field value extracted as pure JSON
46-
- `StaticFieldValue::NonStatic` — field exists but contains non-JSON expressions
47+
- `FieldValue::Json(value)` — field value extracted as pure JSON
48+
- `FieldValue::NonStatic` — field exists but contains non-JSON expressions
4749
(function calls, variables, template literals with interpolation, etc.)
4850
- Key absent — field does not exist in the config object
4951

crates/vite_static_config/src/lib.rs

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use vite_path::AbsolutePath;
1313

1414
/// The result of statically analyzing a single config field's value.
1515
#[derive(Debug, Clone, PartialEq, Eq)]
16-
pub enum StaticFieldValue {
16+
pub enum FieldValue {
1717
/// The field value was successfully extracted as a JSON literal.
1818
Json(serde_json::Value),
1919
/// The field exists but its value is not a pure JSON literal (e.g. contains
@@ -27,11 +27,11 @@ pub enum StaticFieldValue {
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).
2929
/// - `Some(map)` — the default export object was successfully located.
30-
/// - Key maps to [`StaticFieldValue::Json`] — field value was extracted.
31-
/// - Key maps to [`StaticFieldValue::NonStatic`] — field exists but its value
30+
/// - Key maps to [`FieldValue::Json`] — field value was extracted.
31+
/// - Key maps to [`FieldValue::NonStatic`] — field exists but its value
3232
/// cannot be represented as pure JSON.
3333
/// - Key absent — the field does not exist in the object.
34-
pub type StaticConfig = Option<FxHashMap<Box<str>, StaticFieldValue>>;
34+
pub type StaticConfig = Option<FxHashMap<Box<str>, FieldValue>>;
3535

3636
/// Config file names to try, in priority order.
3737
/// This matches Vite's `DEFAULT_CONFIG_FILES`:
@@ -84,11 +84,7 @@ pub fn resolve_static_config(dir: &AbsolutePath) -> StaticConfig {
8484
fn parse_json_config(source: &str) -> StaticConfig {
8585
let value: serde_json::Value = serde_json::from_str(source).ok()?;
8686
let obj = value.as_object()?;
87-
Some(
88-
obj.iter()
89-
.map(|(k, v)| (Box::from(k.as_str()), StaticFieldValue::Json(v.clone())))
90-
.collect(),
91-
)
87+
Some(obj.iter().map(|(k, v)| (Box::from(k.as_str()), FieldValue::Json(v.clone()))).collect())
9288
}
9389

9490
/// Parse a JS/TS config file, extracting the default export object's fields.
@@ -166,11 +162,11 @@ fn extract_config_from_expr(expr: &Expression<'_>) -> StaticConfig {
166162

167163
/// Extract fields from an object expression, converting each value to JSON.
168164
/// Fields whose values cannot be represented as pure JSON are recorded as
169-
/// [`StaticFieldValue::NonStatic`]. Spread elements and computed properties
165+
/// [`FieldValue::NonStatic`]. Spread elements and computed properties
170166
/// are not representable so they are silently skipped (their keys are unknown).
171167
fn extract_object_fields(
172168
obj: &oxc_ast::ast::ObjectExpression<'_>,
173-
) -> FxHashMap<Box<str>, StaticFieldValue> {
169+
) -> FxHashMap<Box<str>, FieldValue> {
174170
let mut map = FxHashMap::default();
175171

176172
for prop in &obj.properties {
@@ -187,28 +183,25 @@ fn extract_object_fields(
187183
continue;
188184
};
189185

190-
let value =
191-
expr_to_json(&prop.value).map_or(StaticFieldValue::NonStatic, StaticFieldValue::Json);
186+
let value = expr_to_json(&prop.value).map_or(FieldValue::NonStatic, FieldValue::Json);
192187
map.insert(Box::from(key.as_ref()), value);
193188
}
194189

195190
map
196191
}
197192

198-
/// Convert an f64 to a JSON value, preserving integers when possible.
199-
#[expect(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
193+
/// Convert an f64 to a JSON value following `JSON.stringify` semantics.
194+
/// `NaN`, `Infinity`, `-Infinity` become `null`; `-0` becomes `0`.
200195
fn f64_to_json_number(value: f64) -> serde_json::Value {
201-
// If the value is a whole number that fits in i64, use integer representation
196+
// fract() == 0.0 ensures the value is a whole number, so the cast is lossless.
197+
#[expect(clippy::cast_possible_truncation)]
202198
if value.fract() == 0.0
203-
&& value.is_finite()
204-
&& value >= i64::MIN as f64
205-
&& value <= i64::MAX as f64
199+
&& let Ok(i) = i64::try_from(value as i128)
206200
{
207-
serde_json::Value::Number(serde_json::Number::from(value as i64))
208-
} else if let Some(n) = serde_json::Number::from_f64(value) {
209-
serde_json::Value::Number(n)
201+
serde_json::Value::from(i)
210202
} else {
211-
serde_json::Value::Null
203+
// From<f64> for Value: finite → Number, NaN/Infinity → Null
204+
serde_json::Value::from(value)
212205
}
213206
}
214207

@@ -285,24 +278,20 @@ mod tests {
285278

286279
/// Helper: parse JS/TS source, unwrap the `Some` (asserting it's analyzable),
287280
/// and return the field map.
288-
fn parse(source: &str) -> FxHashMap<Box<str>, StaticFieldValue> {
281+
fn parse(source: &str) -> FxHashMap<Box<str>, FieldValue> {
289282
parse_js_ts_config(source, "ts").expect("expected analyzable config")
290283
}
291284

292285
/// Shorthand for asserting a field extracted as JSON.
293-
fn assert_json(
294-
map: &FxHashMap<Box<str>, StaticFieldValue>,
295-
key: &str,
296-
expected: serde_json::Value,
297-
) {
298-
assert_eq!(map.get(key), Some(&StaticFieldValue::Json(expected)));
286+
fn assert_json(map: &FxHashMap<Box<str>, FieldValue>, key: &str, expected: serde_json::Value) {
287+
assert_eq!(map.get(key), Some(&FieldValue::Json(expected)));
299288
}
300289

301290
/// Shorthand for asserting a field is `NonStatic`.
302-
fn assert_non_static(map: &FxHashMap<Box<str>, StaticFieldValue>, key: &str) {
291+
fn assert_non_static(map: &FxHashMap<Box<str>, FieldValue>, key: &str) {
303292
assert_eq!(
304293
map.get(key),
305-
Some(&StaticFieldValue::NonStatic),
294+
Some(&FieldValue::NonStatic),
306295
"expected field {key:?} to be NonStatic"
307296
);
308297
}
@@ -459,6 +448,21 @@ mod tests {
459448
assert_json(&result, "d", serde_json::json!(-1));
460449
}
461450

451+
#[test]
452+
fn numeric_overflow_to_infinity_is_null() {
453+
// 1e999 overflows f64 to Infinity; JSON.stringify(Infinity) === "null"
454+
let result = parse("export default { a: 1e999, b: -1e999 }");
455+
assert_json(&result, "a", serde_json::Value::Null);
456+
assert_json(&result, "b", serde_json::Value::Null);
457+
}
458+
459+
#[test]
460+
fn negative_zero_is_zero() {
461+
// JSON.stringify(-0) === "0"
462+
let result = parse("export default { a: -0 }");
463+
assert_json(&result, "a", serde_json::json!(0));
464+
}
465+
462466
#[test]
463467
fn boolean_values() {
464468
let result = parse("export default { a: true, b: false }");

packages/cli/binding/src/cli.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -658,15 +658,15 @@ impl UserConfigLoader for VitePlusConfigLoader {
658658
// Try static config extraction first (no JS runtime needed)
659659
if let Some(static_fields) = vite_static_config::resolve_static_config(package_path) {
660660
match static_fields.get("run") {
661-
Some(vite_static_config::StaticFieldValue::Json(run_value)) => {
661+
Some(vite_static_config::FieldValue::Json(run_value)) => {
662662
tracing::debug!(
663663
"Using statically extracted run config for {}",
664664
package_path.as_path().display()
665665
);
666666
let run_config: UserRunConfig = serde_json::from_value(run_value.clone())?;
667667
return Ok(Some(run_config));
668668
}
669-
Some(vite_static_config::StaticFieldValue::NonStatic) => {
669+
Some(vite_static_config::FieldValue::NonStatic) => {
670670
// `run` field exists but contains non-static values — fall back to NAPI
671671
tracing::debug!(
672672
"run config is not statically analyzable for {}, falling back to NAPI",

0 commit comments

Comments
 (0)