How Vite+ Unifies Vite, Vitest, Oxlint, Oxfmt, and Rolldown into a Single CLI Experience

Vite+ provides a unified CLI experience by routing all commands through a thin JavaScript entry point that delegates to either a bundled Rolldown module for global commands or a Rust core that spawns the appropriate tool binary based on JavaScript resolvers.

The voidzero-dev/vite-plus repository eliminates the friction of managing separate CLIs for your development toolchain. By wrapping Vite, Vitest, Oxlint, Oxfmt, and Rolldown behind a single vp command, it creates a seamless workflow where configuration and execution are handled through one entry point.

The Architecture of Vite+ CLI Unification

Vite+ is built around a thin JavaScript entry point located at packages/cli/src/bin.ts. This file serves as the sole dispatcher for every command the user runs, deciding whether to handle the command locally or delegate to the Rust core.

Entry Point and Command Routing

When you invoke vp <command>, the bin.ts script immediately parses the command line. It categorizes commands into two distinct paths:

  • Global commands (create, migrate, config, mcp, staged, --version) are loaded from a bundled Rolldown module located in dist/global/
  • Tool-specific commands (build, test, lint, fmt) are passed to the Rust core via the NAPI binding run()

This design ensures that global utilities are instantly available without Rust initialization overhead, while tool-specific operations benefit from the Rust core's process management.

Global vs. Rust Core Delegation

The separation is implemented through a simple conditional check in bin.ts:

if (command === 'create') {
  // @ts-ignore — rolldown output
  await import('./global/create.js');
}

For non-global commands, bin.ts calls the NAPI run() function with resolver configurations:

run({ lint, fmt, vite, test, ... })

The Rust core then receives these resolver results, spawns the appropriate binary, and forwards CLI arguments.

Tool Resolution: How Vite+ Finds Your Binaries

Before the Rust core spawns any subprocess, it invokes a JavaScript resolver for each tool. These resolvers use Node's module resolution (resolve()) to locate binaries in your project's node_modules and return an object containing { binPath, envs }.

Vite Resolution

The packages/cli/src/resolve-vite.ts resolver locates the Vite binary within the @voidzero-dev/vite-plus-core package:

// packages/cli/src/resolve-vite.ts
export async function vite() {
  const vitePackagePath = dirname(resolve('@voidzero-dev/vite-plus-core'));
  const binPath = join(vitePackagePath, 'cli.js');
  return { binPath, envs: { ...DEFAULT_ENVS } };
}

When you run vp build, the Rust core receives this binPath and executes node <binPath> build.

Vitest Resolution

Similarly, packages/cli/src/resolve-test.ts resolves the Vitest binary from the @voidzero-dev/vite-plus-test package:

const binPath = join(dirname(resolve('@voidzero-dev/vite-plus-test')), 'dist', 'cli.js');

This allows vp test to launch Vitest with the correct entry point and environment variables.

Oxlint Resolution

The packages/cli/src/resolve-lint.ts resolver handles Oxlint and its companion tsgolint binary for type-aware linting on Windows:

const oxlintMainPath = resolve('oxlint');
const oxlintPackageRoot = dirname(dirname(oxlintMainPath));
const binPath = join(oxlintPackageRoot, 'bin', 'oxlint');
let oxlintTsgolintPath = resolve('oxlint-tsgolint/bin/tsgolint');
// ...
envs: { ...DEFAULT_ENVS, OXLINT_TSGOLINT_PATH: oxlintTsgolintPath }

This enables vp lint --fix --type-aware to propagate the necessary environment variables for advanced linting features.

Oxfmt Resolution

The packages/cli/src/resolve-fmt.ts resolver provides the simplest resolution, returning the native oxfmt executable:

const binPath = resolve('oxfmt/bin/oxfmt');

Running vp fmt --write triggers this resolver to locate the formatting binary.

Unified Configuration Schema

Vite+ extends the standard Vite configuration file (vite.config.ts) with additional top-level keys: lint, fmt, pack, run, and staged. These fields are declared in packages/cli/src/index.ts and passed to the Rust core, allowing the same vite-plus command to launch any tool with a unified configuration object.

This augmentation provides TypeScript auto-completion for Vite+ specific options while maintaining compatibility with existing Vite configurations. The Rust core consumes these fields to determine tool-specific settings when spawning subprocesses.

Rolldown Bundling Strategy

Global commands are compiled once using Rolldown via packages/cli/rolldown.config.ts. The output lives in dist/global/ and is imported lazily by bin.ts. This approach keeps global-only commands lightweight while allowing them to be written in TypeScript.

The bundling process produces self-contained ESM modules that require no additional resolution, ensuring that commands like vp create my-app execute immediately without dependency resolution overhead.

Practical Usage Examples

Running a Vite build through the unified CLI:

vp build

Under the hood, this triggers the Vite resolver and executes the build through the Rust core.

Running tests:

vp test

This invokes the Vitest resolver and spawns the test runner with proper environment propagation.

Running lint with type-aware rules:

vp lint --fix --type-aware

The Oxlint resolver discovers both the main binary and the tsgolint companion, setting OXLINT_TSGOLINT_PATH accordingly.

Formatting code:

vp fmt --write

Creating a new project using the bundled global command:

vp create my-app

This command is handled entirely by the Rolldown-bundled module in dist/global/create.js, requiring no Rust core initialization.

Summary

  • Single entry point: All commands route through packages/cli/src/bin.ts, providing a consistent vp interface regardless of the underlying tool.
  • Dual execution paths: Global commands use Rolldown-bundled modules; tool commands delegate to the Rust core with JavaScript resolvers.
  • Dynamic binary resolution: Each tool resolver (resolve-vite.ts, resolve-test.ts, resolve-lint.ts, resolve-fmt.ts) locates binaries using Node's module resolution and merges DEFAULT_ENVS with tool-specific variables.
  • Configuration unification: Vite+ augments vite.config.ts with additional fields (lint, fmt, pack, run, staged) that the Rust core consumes when spawning tools.
  • Rolldown bundling: Global commands are pre-bundled into dist/global/ for instant execution without dependency resolution.

Frequently Asked Questions

How does Vite+ decide whether to use the Rust core or the bundled module?

The bin.ts script maintains a list of global commands (create, migrate, config, mcp, staged, --version). If the invoked command matches this list, it lazily imports the corresponding module from dist/global/. All other commands are passed to the Rust core via the NAPI run() binding, which then coordinates with the JavaScript resolvers to spawn the correct binary.

Can I use Vite+ if my project already has a vite.config.ts file?

Yes. Vite+ is designed to be backward compatible with existing Vite configurations. The packages/cli/src/index.ts file augments the TypeScript types to include Vite+ specific fields (lint, fmt, etc.), but these are optional. Your existing Vite configuration will work immediately, and you can incrementally adopt Vite+ features by adding the new top-level keys to your config.

What happens if a tool binary is not found in node_modules?

The JavaScript resolvers use Node's standard module resolution (resolve()) to locate each binary. If a required package (such as oxlint or @voidzero-dev/vite-plus-test) is not installed, the resolver will throw a module resolution error before the Rust core attempts to spawn the process. This ensures clear error messages indicating which dependency is missing from your project.

Why does Vite+ use Rolldown for global commands instead of handling them in Rust?

Global commands like create and migrate require complex logic that is easier to implement and maintain in TypeScript. By bundling these commands with Rolldown into dist/global/, Vite+ keeps them self-contained and instantly executable without requiring the Rust core to implement a JavaScript resolver for these specific operations. This separation of concerns allows global utilities to remain lightweight TypeScript code while tool execution benefits from Rust's process management.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →