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

> Discover how Vite+ unifies Vite, Vitest, Oxlint, Oxfmt, and Rolldown into a single CLI. Streamline your frontend workflow with this powerful integration.

- Repository: [VoidZero/vite-plus](https://github.com/voidzero-dev/vite-plus)
- Tags: architecture
- Published: 2026-03-16

---

**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`](https://github.com/voidzero-dev/vite-plus/blob/main/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`](https://github.com/voidzero-dev/vite-plus/blob/main/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`](https://github.com/voidzero-dev/vite-plus/blob/main/bin.ts):

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

```

For non-global commands, [`bin.ts`](https://github.com/voidzero-dev/vite-plus/blob/main/bin.ts) calls the NAPI `run()` function with resolver configurations:

```typescript
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`](https://github.com/voidzero-dev/vite-plus/blob/main/packages/cli/src/resolve-vite.ts) resolver locates the Vite binary within the `@voidzero-dev/vite-plus-core` package:

```typescript
// 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`](https://github.com/voidzero-dev/vite-plus/blob/main/packages/cli/src/resolve-test.ts) resolves the Vitest binary from the `@voidzero-dev/vite-plus-test` package:

```typescript
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`](https://github.com/voidzero-dev/vite-plus/blob/main/packages/cli/src/resolve-lint.ts) resolver handles Oxlint and its companion `tsgolint` binary for type-aware linting on Windows:

```typescript
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`](https://github.com/voidzero-dev/vite-plus/blob/main/packages/cli/src/resolve-fmt.ts) resolver provides the simplest resolution, returning the native `oxfmt` executable:

```typescript
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`](https://github.com/voidzero-dev/vite-plus/blob/main/vite.config.ts)) with additional top-level keys: `lint`, `fmt`, `pack`, `run`, and `staged`. These fields are declared in [`packages/cli/src/index.ts`](https://github.com/voidzero-dev/vite-plus/blob/main/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`](https://github.com/voidzero-dev/vite-plus/blob/main/packages/cli/rolldown.config.ts). The output lives in `dist/global/` and is imported lazily by [`bin.ts`](https://github.com/voidzero-dev/vite-plus/blob/main/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:

```bash
vp build

```

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

Running tests:

```bash
vp test

```

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

Running lint with type-aware rules:

```bash
vp lint --fix --type-aware

```

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

Formatting code:

```bash
vp fmt --write

```

Creating a new project using the bundled global command:

```bash
vp create my-app

```

This command is handled entirely by the Rolldown-bundled module in [`dist/global/create.js`](https://github.com/voidzero-dev/vite-plus/blob/main/dist/global/create.js), requiring no Rust core initialization.

## Summary

- **Single entry point**: All commands route through [`packages/cli/src/bin.ts`](https://github.com/voidzero-dev/vite-plus/blob/main/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`](https://github.com/voidzero-dev/vite-plus/blob/main/resolve-vite.ts), [`resolve-test.ts`](https://github.com/voidzero-dev/vite-plus/blob/main/resolve-test.ts), [`resolve-lint.ts`](https://github.com/voidzero-dev/vite-plus/blob/main/resolve-lint.ts), [`resolve-fmt.ts`](https://github.com/voidzero-dev/vite-plus/blob/main/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`](https://github.com/voidzero-dev/vite-plus/blob/main/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`](https://github.com/voidzero-dev/vite-plus/blob/main/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`](https://github.com/voidzero-dev/vite-plus/blob/main/vite.config.ts) file?

Yes. Vite+ is designed to be backward compatible with existing Vite configurations. The [`packages/cli/src/index.ts`](https://github.com/voidzero-dev/vite-plus/blob/main/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.