Skip to content

feat(config): static vite config extraction for run config#679

Merged
branchseer merged 14 commits intomainfrom
feat/static-config-extraction
Mar 12, 2026
Merged

feat(config): static vite config extraction for run config#679
branchseer merged 14 commits intomainfrom
feat/static-config-extraction

Conversation

@branchseer
Copy link
Member

@branchseer branchseer commented Mar 3, 2026

Add new vite_static_config crate that uses oxc_parser to statically extract JSON-serializable fields from vite.config.* files without executing JavaScript

Performance

Measured using Chrome trace instrumentation (PR #663).

On a cache hit, vp run loads the config, validates the fingerprint, and streams cached output — no build work. Before this PR, config loading required a NAPI round-trip to evaluate the config file. With static extraction that cost drops to ~0ms.

vp run end-to-end time (cache hit)

Project Before After Reduction
vue-mini ~217ms 23ms ~89%
oxlint-plugin-complexity ~220ms 17ms ~92%
vitepress ~282ms 32ms ~89%
vite-vue-vercel ~326ms 28ms ~91%
rollipop ~670ms 16–53ms ~92–98%
frm-stack (10 packages) ~895ms 34–69ms ~92–96%
tanstack-start-helloworld ~1,383ms ~1,394ms — (indirect export, not yet handled)

Summary

  • VitePlusConfigLoader now tries static extraction first for the run config, falling back to NAPI-based resolution only when the config cannot be statically extracted
  • Static extraction supports all common patterns:
    • export default { ... } — bare object literal
    • export default defineConfig({ ... }) — direct call
    • export default defineConfig(() => ({ ... })) — concise arrow body
    • export default defineConfig(fn) — block body with single return
    • module.exports = ... — CJS equivalents
  • When no vite.config.* file exists, returns an empty map immediately (skips NAPI entirely)
  • Spread and computed-key properties correctly invalidate previously-seen fields

Test plan

  • 50 unit tests in vite_static_config pass
  • All existing vite-plus-cli tests pass
  • cargo fmt --check passes
  • cargo shear reports no unused dependencies
  • cargo clippy clean (no new warnings)
  • CI passes on all platforms

🤖 Generated with Claude Code

@branchseer branchseer marked this pull request as draft March 4, 2026 00:02
@branchseer branchseer force-pushed the feat/static-config-extraction branch from fd94982 to 9052846 Compare March 4, 2026 02:58
@netlify
Copy link

netlify bot commented Mar 4, 2026

👷 Deploy Preview for viteplus-staging processing.

Name Link
🔨 Latest commit 9cce0f4
🔍 Latest deploy log https://app.netlify.com/projects/viteplus-staging/deploys/69b2190c8981530008a8c99b

@branchseer branchseer force-pushed the feat/static-config-extraction branch from 474fef7 to ecbafe2 Compare March 4, 2026 04:43
branchseer and others added 14 commits March 12, 2026 09:38
…n` config

Add a new `vite_static_config` crate that uses oxc_parser to statically
extract JSON-serializable fields from vite.config.* files without needing
a Node.js runtime. The `VitePlusConfigLoader` now tries static extraction
first for the `run` config and falls back to NAPI only when needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The config file resolution order was .ts first, but Vite resolves
.js, .mjs, .ts, .cjs, .mts, .cts — matching that order now.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…olver

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
resolve_static_config now returns Option<FxHashMap<..., StaticFieldValue>>:
- None: config is not analyzable (no file, parse error, no export default,
  or exported value is not an object literal) — caller should fall back to NAPI
- Some(map): config was successfully analyzed
  - Json(value): field extracted as pure JSON
  - NonStatic: field exists but value is not a JSON literal
  - key absent: field does not exist in the config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tching

- PropertyKey::static_name() replaces property_key_to_string and property_key_to_json_key
- TemplateLiteral::single_quasi() replaces manual quasis/expressions check
- Expression::is_specific_id() replaces is_define_config_call helper
- ArrayExpressionElement::is_elision()/is_spread() replaces variant matching
- ObjectPropertyKind::is_spread() replaces variant matching

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add support for CommonJS config files:
- module.exports = { ... }
- module.exports = defineConfig({ ... })

Refactored shared config extraction into extract_config_from_expr,
used by both export default and module.exports paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace umbrella `oxc` dependency with `oxc_ast`, `oxc_parser`, and
`oxc_span` for more precise dependency tracking. Add README documenting
supported patterns, config resolution order, return type semantics, and
limitations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Value

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>
…onfig file

Two improvements to static config extraction:

1. When no vite.config.* file exists in a workspace package,
   resolve_static_config now returns an empty map (instead of None).
   The caller sees no `run` field and returns immediately, skipping
   the NAPI/JS callback. This eliminates ~165ms cold Node.js init +
   ~3ms/pkg warm overhead for monorepo packages without config files.

2. Support defineConfig(fn) where fn is an arrow function or function
   expression. The extractor locates the return expression inside
   the function body and extracts fields from it. Functions with
   multiple return statements are rejected as not statically analyzable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…r 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>
`{ a: 1, ...x, b: 2 }` — the spread may override `a`, so `a` is now
marked NonStatic. Fields declared after the spread (`b`) are unaffected
since they win over any spread key. Unknown keys from the spread are
still not added to the map.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`{ a: 1, [key]: 2, b: 3 }` — `[key]` could resolve to `'a'` and
override it, so `a` is now marked NonStatic. Same logic as spreads.
Fields after the computed key are unaffected (they explicitly win).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@branchseer branchseer force-pushed the feat/static-config-extraction branch from 45787e4 to 9cce0f4 Compare March 12, 2026 01:38
@branchseer branchseer marked this pull request as ready for review March 12, 2026 01:43
@branchseer branchseer merged commit 963316e into main Mar 12, 2026
21 of 23 checks passed
@branchseer branchseer deleted the feat/static-config-extraction branch March 12, 2026 02:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants