diff --git a/packages/cli/BUNDLING.md b/packages/cli/BUNDLING.md
index 39bc28e288..d89dcc5c0c 100644
--- a/packages/cli/BUNDLING.md
+++ b/packages/cli/BUNDLING.md
@@ -1,16 +1,17 @@
# CLI Package Build Architecture
-This document explains how `@voidzero-dev/vite-plus` is built and how it re-exports the test package.
+This document explains how `@voidzero-dev/vite-plus` is built and how it re-exports from both the core and test packages to serve as a drop-in replacement for `vite`.
## Overview
-The CLI package uses a **3-step build process**:
+The CLI package uses a **4-step build process**:
1. **TypeScript Compilation** - Compile TypeScript source to JavaScript
2. **NAPI Binding Build** - Compile Rust code to native Node.js bindings
-3. **Test Package Export Sync** - Re-export `@voidzero-dev/vite-plus-test` under `./test/*`
+3. **Core Package Export Sync** - Re-export `@voidzero-dev/vite-plus-core` under `./client`, `./types/*`, etc.
+4. **Test Package Export Sync** - Re-export `@voidzero-dev/vite-plus-test` under `./test/*`
-This allows users to import everything from a single package (`@voidzero-dev/vite-plus`) instead of needing to know about the separate test package.
+This architecture allows users to import everything from a single package (`@voidzero-dev/vite-plus`) as a drop-in replacement for `vite`, without needing to know about the separate core and test packages.
## Build Steps
@@ -50,7 +51,39 @@ await cli.build({
The build generates platform-specific native binaries and formats the generated JavaScript wrapper with `oxfmt`.
-### Step 3: Test Package Export Sync (`syncTestPackageExports`)
+### Step 3: Core Package Export Sync (`syncCorePackageExports`)
+
+Creates shim files that re-export from `@voidzero-dev/vite-plus-core`, enabling this package to be a drop-in replacement for upstream `vite`. This is critical for compatibility with existing Vite plugins and configurations.
+
+**Prerequisites**: The core package must be built first (its `dist/vite/` directory must exist).
+
+**Export paths created**:
+
+| Export Path | Type | Description |
+| -------------------- | ---------- | --------------------------------------------------------------------------------------- |
+| `./client` | Types only | Triple-slash reference for ambient type declarations (CSS modules, asset imports, etc.) |
+| `./module-runner` | JS + Types | Re-exports the Vite module runner for SSR/environments |
+| `./internal` | JS + Types | Re-exports internal Vite APIs |
+| `./dist/client/*` | JS | Client runtime files (`.mjs`, `.cjs`) |
+| `./types/*` | Types only | Type-only re-exports using `export type *` |
+| `./types/internal/*` | Blocked | Set to `null` to prevent access to internal types |
+
+**Shim file examples**:
+
+```typescript
+// dist/client.d.ts (triple-slash reference for ambient types)
+///
+
+// dist/module-runner.js
+export * from '@voidzero-dev/vite-plus-core/module-runner';
+
+// dist/types/importMeta.d.ts (type-only export)
+export type * from '@voidzero-dev/vite-plus-core/types/importMeta.d.ts';
+```
+
+**Note on export ordering**: In `package.json`, the `./types/internal/*` export (set to `null`) must appear before `./types/*` for correct precedence. More specific patterns must precede wildcards.
+
+### Step 4: Test Package Export Sync (`syncTestPackageExports`)
Reads the test package's exports and creates shim files that re-export everything under `./test/*`:
@@ -74,6 +107,21 @@ packages/cli/
│ ├── index.cjs # Main entry (CJS)
│ ├── index.d.ts # Type declarations
│ ├── bin.js # CLI entry point
+│ ├── client.d.ts # ./client types (triple-slash ref)
+│ ├── module-runner.js # ./module-runner shim
+│ ├── module-runner.d.ts
+│ ├── internal.js # ./internal shim
+│ ├── internal.d.ts
+│ ├── client/ # Synced client runtime files
+│ │ ├── client.mjs # ESM client shim
+│ │ ├── client.d.ts
+│ │ ├── env.mjs
+│ │ └── ...
+│ ├── types/ # Synced type definitions
+│ │ ├── importMeta.d.ts # Type shims (export type *)
+│ │ ├── importGlob.d.ts
+│ │ ├── customEvent.d.ts
+│ │ └── ...
│ └── test/ # Synced test exports
│ ├── index.js # Re-exports @voidzero-dev/vite-plus-test
│ ├── index.cjs
@@ -113,6 +161,62 @@ These targets are defined in `package.json` under the `napi.targets` field.
---
+## Core Package Export Sync Details
+
+### Why Shim Files?
+
+The CLI package creates thin shim files that re-export from `@voidzero-dev/vite-plus-core` rather than bundling the actual code. This approach:
+
+1. **Enables drop-in replacement** - Users can replace `vite` with `@voidzero-dev/vite-plus` without changing imports
+2. **Keeps packages in sync** - No need to rebuild CLI when core package changes
+3. **Reduces duplication** - No file copying, just re-exports
+4. **Preserves module resolution** - Node.js resolves to the actual core package
+
+### Export Mapping (Core)
+
+| Upstream Vite Export | CLI Package Export | Description |
+| -------------------- | --------------------------------------- | ------------------------------------------ |
+| `vite/client` | `@voidzero-dev/vite-plus/client` | Ambient types for HMR, CSS modules, assets |
+| `vite/module-runner` | `@voidzero-dev/vite-plus/module-runner` | SSR/Environment module runner |
+| `vite/internal` | `@voidzero-dev/vite-plus/internal` | Internal APIs |
+| `vite/dist/client/*` | `@voidzero-dev/vite-plus/dist/client/*` | Client runtime files |
+| `vite/types/*` | `@voidzero-dev/vite-plus/types/*` | Type definitions |
+
+### Type-Only Exports
+
+For `./types/*` exports, shim files use `export type *` syntax (TypeScript 5.0+) to ensure only type information is re-exported:
+
+```typescript
+// dist/types/importMeta.d.ts
+export type * from '@voidzero-dev/vite-plus-core/types/importMeta.d.ts';
+```
+
+This is important because `./types/*` only exposes `.d.ts` files and should never include runtime code.
+
+### Internal Types Blocking
+
+The `./types/internal/*` export is set to `null` in package.json to block access to internal type definitions:
+
+```json
+"./types/internal/*": null,
+"./types/*": { "types": "./dist/types/*" }
+```
+
+The `syncTypesDir()` helper skips the top-level `internal` directory when creating shims, since access is blocked at the exports level.
+
+### Client Types (Triple-Slash Reference)
+
+The `./client` export uses a triple-slash reference instead of a regular export because Vite's `client.d.ts` contains ambient type declarations (for CSS modules, assets, etc.) that should be globally available:
+
+```typescript
+// dist/client.d.ts
+///
+```
+
+This allows TypeScript to pick up types like `import.meta.hot`, CSS module types, and asset imports without explicit imports.
+
+---
+
## Test Package Export Sync Details
### Why Shim Files?
@@ -123,7 +227,7 @@ Instead of copying the actual dist files from the test package, we create thin s
2. **Reduces duplication** - No file copying, just re-exports
3. **Preserves module resolution** - Node.js resolves to the actual test package
-### Export Mapping
+### Export Mapping (Test)
All test package exports are mapped under `./test/*`:
@@ -186,9 +290,12 @@ module.exports = require('@voidzero-dev/vite-plus-test');
**Type shim** (`dist/test/browser-playwright.d.ts`):
```typescript
+import '@voidzero-dev/vite-plus-test/browser-playwright';
export * from '@voidzero-dev/vite-plus-test/browser-playwright';
```
+Note: Type shims include a side-effect import to preserve module augmentations (e.g., `toMatchSnapshot` on the `Assertion` interface).
+
---
## Build Dependencies
@@ -216,10 +323,10 @@ This sets `release: false` in the NAPI build options, producing larger but faste
## Build Commands
```bash
-# Build the CLI package
+# Build the CLI package (requires core package to be built first)
pnpm -C packages/cli build
-# Build from monorepo root
+# Build from monorepo root (builds all dependencies first)
pnpm build --filter @voidzero-dev/vite-plus
# Debug build
@@ -232,16 +339,21 @@ VITE_PLUS_CLI_DEBUG=1 pnpm -C packages/cli build
After building, the CLI package exports:
-| Export Path | Description |
-| --------------------------- | ------------------------------- |
-| `.` | Main entry (CLI utilities) |
-| `./bin` | CLI binary entry point |
-| `./binding` | NAPI native binding |
-| `./test` | Test package main entry |
-| `./test/browser` | Browser testing utilities |
-| `./test/browser-playwright` | Playwright integration |
-| `./test/plugins/*` | Plugin shims for pnpm overrides |
-| `./package.json` | Package metadata |
+| Export Path | Description |
+| --------------------------- | ----------------------------------- |
+| `.` | Main entry (CLI utilities) |
+| `./client` | Client types (ambient declarations) |
+| `./module-runner` | Vite module runner for SSR |
+| `./internal` | Internal Vite APIs |
+| `./dist/client/*` | Client runtime files |
+| `./types/*` | Type definitions |
+| `./bin` | CLI binary entry point |
+| `./binding` | NAPI native binding |
+| `./test` | Test package main entry |
+| `./test/browser` | Browser testing utilities |
+| `./test/browser-playwright` | Playwright integration |
+| `./test/plugins/*` | Plugin shims for pnpm overrides |
+| `./package.json` | Package metadata |
See `package.json` for the complete list of exports.
@@ -252,17 +364,26 @@ See `package.json` for the complete list of exports.
### Build Flow
```
-1. buildCli() TypeScript compilation -> dist/*.js
-2. buildNapiBinding() Rust -> binding/*.node (per platform)
-3. syncTestPackageExports() Read test pkg exports -> dist/test/*
- ├── createShimForExport() Generate shim files
- ├── createConditionalShim() Handle import/require conditions
- └── updateCliPackageJson() Update exports in package.json
+1. buildCli() TypeScript compilation -> dist/*.js
+2. buildNapiBinding() Rust -> binding/*.node (per platform)
+3. syncCorePackageExports() Read core pkg dist -> dist/client/, dist/types/
+ ├── createClientShim() Triple-slash reference for ./client
+ ├── createModuleRunnerShim() JS + types for ./module-runner
+ ├── createInternalShim() JS + types for ./internal
+ ├── syncClientDir() Shims for ./dist/client/*
+ └── syncTypesDir() Type-only shims for ./types/*
+4. syncTestPackageExports() Read test pkg exports -> dist/test/*
+ ├── createShimForExport() Generate shim files
+ ├── createConditionalShim() Handle import/require conditions
+ └── updateCliPackageJson() Update exports in package.json
```
### Key Constants
```typescript
+// Core package name for Vite compatibility exports
+const CORE_PACKAGE_NAME = '@voidzero-dev/vite-plus-core';
+
// Test package name for re-exports
const TEST_PACKAGE_NAME = '@voidzero-dev/vite-plus-test';
```
@@ -275,4 +396,6 @@ The build script automatically updates `package.json`:
2. Adds new exports from test package
3. Ensures `dist/test` is in the `files` array
-This keeps the CLI package exports in sync with the test package without manual maintenance.
+Core package exports (`./client`, `./module-runner`, `./internal`, `./dist/client/*`, `./types/*`) are defined statically in `package.json` and not auto-generated, since they match upstream Vite's exports structure.
+
+This keeps the CLI package exports in sync with both upstream Vite and the test package without manual maintenance.
diff --git a/packages/cli/build.ts b/packages/cli/build.ts
index e4d72b895d..5053d7b61e 100644
--- a/packages/cli/build.ts
+++ b/packages/cli/build.ts
@@ -1,5 +1,21 @@
-import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
-import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
+/**
+ * Build script for @voidzero-dev/vite-plus CLI package
+ *
+ * This script performs four main tasks:
+ * 1. buildCli() - Compiles TypeScript sources
+ * 2. buildNapiBinding() - Builds the native Rust binding via NAPI
+ * 3. syncCorePackageExports() - Creates shim files to re-export from @voidzero-dev/vite-plus-core
+ * 4. syncTestPackageExports() - Creates shim files to re-export from @voidzero-dev/vite-plus-test
+ *
+ * The sync functions allow this package to be a drop-in replacement for 'vite' by
+ * re-exporting all the same subpaths (./client, ./types/*, etc.) while delegating
+ * to the core package for actual implementation.
+ *
+ * IMPORTANT: The core package must be built before running this script.
+ */
+
+import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs';
+import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -16,9 +32,11 @@ import {
const projectDir = dirname(fileURLToPath(import.meta.url));
const TEST_PACKAGE_NAME = '@voidzero-dev/vite-plus-test';
+const CORE_PACKAGE_NAME = '@voidzero-dev/vite-plus-core';
await buildCli();
await buildNapiBinding();
+await syncCorePackageExports();
await syncTestPackageExports();
async function buildNapiBinding() {
@@ -73,6 +91,143 @@ async function buildCli() {
}
}
+/**
+ * Sync Vite core exports from @voidzero-dev/vite-plus-core to @voidzero-dev/vite-plus
+ *
+ * Creates shim files that re-export from the core package, enabling imports like:
+ * - `import type { ... } from '@voidzero-dev/vite-plus/types/importGlob.d.ts'`
+ * - `import { ... } from '@voidzero-dev/vite-plus/module-runner'`
+ *
+ * Export paths created:
+ * - ./client - Triple-slash reference (ambient type declarations for CSS, assets, etc.)
+ * - ./module-runner - Re-exports both JS and types
+ * - ./internal - Re-exports both JS and types
+ * - ./dist/client/* - Re-exports client runtime files (.mjs, .cjs)
+ * - ./types/* - Type-only re-exports using `export type *`
+ *
+ * Note: In package.json exports, ./types/internal/* must come BEFORE ./types/*
+ * for correct precedence (more specific patterns must precede wildcards).
+ *
+ * @throws Error if core package is not built (missing dist directories)
+ */
+async function syncCorePackageExports() {
+ console.log('\nSyncing core package exports...');
+
+ const distDir = join(projectDir, 'dist');
+ const clientDir = join(distDir, 'client');
+ const typesDir = join(distDir, 'types');
+
+ // Clean up previous build
+ await rm(clientDir, { recursive: true, force: true });
+ await rm(typesDir, { recursive: true, force: true });
+ await mkdir(clientDir, { recursive: true });
+ await mkdir(typesDir, { recursive: true });
+
+ // Create ./client shim (types only) - uses triple-slash reference since client.d.ts is ambient
+ console.log(' Creating ./client');
+ await writeFile(
+ join(distDir, 'client.d.ts'),
+ `/// \n`,
+ );
+
+ // Create ./module-runner shim
+ console.log(' Creating ./module-runner');
+ await writeFile(
+ join(distDir, 'module-runner.js'),
+ `export * from '${CORE_PACKAGE_NAME}/module-runner';\n`,
+ );
+ await writeFile(
+ join(distDir, 'module-runner.d.ts'),
+ `export * from '${CORE_PACKAGE_NAME}/module-runner';\n`,
+ );
+
+ // Create ./internal shim
+ console.log(' Creating ./internal');
+ await writeFile(join(distDir, 'internal.js'), `export * from '${CORE_PACKAGE_NAME}/internal';\n`);
+ await writeFile(
+ join(distDir, 'internal.d.ts'),
+ `export * from '${CORE_PACKAGE_NAME}/internal';\n`,
+ );
+
+ // Create ./dist/client/* shims by reading core's dist/vite/client files
+ console.log(' Creating ./dist/client/*');
+ const coreClientDir = join(projectDir, '../core/dist/vite/client');
+ if (!existsSync(coreClientDir)) {
+ throw new Error(
+ `Core client artifacts not found at "${coreClientDir}". ` +
+ `Make sure ${CORE_PACKAGE_NAME} is built before building the CLI.`,
+ );
+ }
+ for (const file of readdirSync(coreClientDir)) {
+ const srcPath = join(coreClientDir, file);
+ const shimPath = join(clientDir, file);
+ // Skip directories
+ if (statSync(srcPath).isDirectory()) continue;
+ if (file.endsWith('.js') || file.endsWith('.mjs') || file.endsWith('.cjs')) {
+ await writeFile(shimPath, `export * from '${CORE_PACKAGE_NAME}/dist/client/${file}';\n`);
+ } else if (file.endsWith('.d.ts') || file.endsWith('.d.mts') || file.endsWith('.d.cts')) {
+ const baseFile = file.replace(/\.d\.[mc]?ts$/, '');
+ await writeFile(shimPath, `export * from '${CORE_PACKAGE_NAME}/dist/client/${baseFile}';\n`);
+ } else {
+ // Copy non-JS/TS files directly (e.g., CSS, source maps)
+ await copyFile(srcPath, shimPath);
+ }
+ }
+
+ // Create ./types/* shims by reading core's dist/vite/types files
+ console.log(' Creating ./types/*');
+ const coreTypesDir = join(projectDir, '../core/dist/vite/types');
+ if (!existsSync(coreTypesDir)) {
+ throw new Error(
+ `Core type definitions not found at "${coreTypesDir}". ` +
+ `Make sure ${CORE_PACKAGE_NAME} is built before building the CLI.`,
+ );
+ }
+ await syncTypesDir(coreTypesDir, typesDir, '');
+
+ console.log('\nSynced core package exports');
+}
+
+/**
+ * Recursively sync type definition files from core to CLI package
+ *
+ * Creates shim .d.ts files that re-export types from the core package.
+ * Uses `export type * from` syntax which is valid in TypeScript 5.0+.
+ *
+ * @param srcDir - Source directory containing .d.ts files
+ * @param destDir - Destination directory for shim files
+ * @param relativePath - Current path relative to types root (empty string at top level)
+ *
+ * Special handling:
+ * - Skips top-level 'internal' directory (blocked by ./types/internal/* export)
+ * - Supports .d.ts, .d.mts, and .d.cts extensions
+ * - Preserves directory structure recursively
+ */
+async function syncTypesDir(srcDir: string, destDir: string, relativePath: string) {
+ const entries = readdirSync(srcDir);
+
+ for (const entry of entries) {
+ const srcPath = join(srcDir, entry);
+ const destPath = join(destDir, entry);
+ const entryRelPath = relativePath ? `${relativePath}/${entry}` : entry;
+
+ if (statSync(srcPath).isDirectory()) {
+ // Skip top-level internal directory - it's blocked by ./types/internal/* export
+ if (entry === 'internal' && relativePath === '') continue;
+
+ await mkdir(destPath, { recursive: true });
+ await syncTypesDir(srcPath, destPath, entryRelPath);
+ } else if (/\.d\.[mc]?ts$/.test(entry)) {
+ // Create shim that re-exports from core - must include extension for wildcard exports
+ // Use 'export type *' since we're re-exporting from a .d.ts file
+ await writeFile(
+ destPath,
+ `export type * from '${CORE_PACKAGE_NAME}/types/${entryRelPath}';\n`,
+ );
+ }
+ }
+}
+
/**
* Sync exports from @voidzero-dev/vite-plus-test to @voidzero-dev/vite-plus
*
diff --git a/packages/cli/package.json b/packages/cli/package.json
index f78fdc9823..db09889720 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -58,6 +58,22 @@
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
+ "./client": {
+ "types": "./dist/client.d.ts"
+ },
+ "./module-runner": {
+ "types": "./dist/module-runner.d.ts",
+ "default": "./dist/module-runner.js"
+ },
+ "./internal": {
+ "types": "./dist/internal.d.ts",
+ "default": "./dist/internal.js"
+ },
+ "./dist/client/*": "./dist/client/*",
+ "./types/internal/*": null,
+ "./types/*": {
+ "types": "./dist/types/*"
+ },
"./bin": {
"import": "./dist/bin.js"
},