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" },