Skip to content

Commit 04960d9

Browse files
authored
docs: add vitest deps bundle doc (#348)
1 parent fe27503 commit 04960d9

1 file changed

Lines changed: 365 additions & 0 deletions

File tree

packages/test/BUNDLING.md

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
# Test Package Bundling Architecture
2+
3+
This document explains how `@voidzero-dev/vite-plus-test` bundles vitest and its dependencies.
4+
5+
## Overview
6+
7+
The test package uses a **hybrid bundling strategy**:
8+
9+
1. **COPY** all `@vitest/*` packages (preserves browser/Node.js separation)
10+
2. **BUNDLE** only leaf dependencies like `chai`, `pathe` (reduces install size)
11+
3. **Separate entries** (`index.js` vs `index-node.js`) prevent Node.js code from loading in browsers
12+
13+
This approach avoids the critical issue of Rolldown creating shared chunks that mix Node.js-only code (like `__vite__injectQuery`) with browser code, which causes runtime crashes.
14+
15+
## Dependencies Classification
16+
17+
### Copied Packages (`dist/@vitest/`)
18+
19+
These 9 `@vitest/*` packages are **copied** (not bundled) to preserve their original file structure:
20+
21+
| Package | Purpose |
22+
| ---------------------------- | ---------------------------------------------------- |
23+
| `@vitest/runner` | Test runner core |
24+
| `@vitest/utils` | Utilities (source-map, error, display, timers, etc.) |
25+
| `@vitest/spy` | Spy/mock implementation |
26+
| `@vitest/expect` | Assertion library |
27+
| `@vitest/snapshot` | Snapshot testing |
28+
| `@vitest/mocker` | Module mocking (node, browser, automock) |
29+
| `@vitest/pretty-format` | Output formatting |
30+
| `@vitest/browser` | Browser testing support |
31+
| `@vitest/browser-playwright` | Playwright integration |
32+
33+
**Why copy instead of bundle?** Bundling would create shared chunks that mix browser-safe and Node.js-only code. Copying preserves the original separation.
34+
35+
### Bundled Leaf Dependencies (`dist/vendor/`)
36+
37+
These packages are bundled using Rolldown into `dist/vendor/*.mjs`:
38+
39+
| Package | Purpose |
40+
| --------------------- | ----------------------------------- |
41+
| `chai` | Assertion library (core of expect) |
42+
| `pathe` | Path utilities |
43+
| `tinyrainbow` | Terminal colors |
44+
| `magic-string` | String manipulation for source maps |
45+
| `estree-walker` | AST traversal |
46+
| `why-is-node-running` | Debug tool for hanging processes |
47+
48+
These were moved from `dependencies` to `devDependencies` since they're bundled.
49+
50+
### Runtime Dependencies (NOT Bundled)
51+
52+
These remain in `dependencies` and are installed with the package:
53+
54+
| Package | Reason Not Bundled |
55+
| ----------------- | --------------------------------------------- |
56+
| `sirv` | Static file server - complex runtime behavior |
57+
| `ws` | WebSocket server - native bindings |
58+
| `pixelmatch` | Image comparison - optional feature |
59+
| `pngjs` | PNG handling - optional feature |
60+
| `es-module-lexer` | ESM parsing - small, used at runtime |
61+
| `expect-type` | Type testing - small |
62+
| `obug` | Debugging - small |
63+
| `picomatch` | Glob matching - small |
64+
| `std-env` | Environment detection - small |
65+
| `tinybench` | Benchmarking - optional feature |
66+
| `tinyexec` | Command execution - small |
67+
| `tinyglobby` | File globbing - small |
68+
69+
### External Blocklist (Must NOT Bundle)
70+
71+
These packages are explicitly kept external in `EXTERNAL_BLOCKLIST` during the Rolldown build:
72+
73+
| Package | Reason |
74+
| ------------------ | ----------------------------------------- |
75+
| `playwright` | Native bindings, user must install |
76+
| `debug` | Environment detection breaks when bundled |
77+
| `happy-dom` | Optional peer dependency |
78+
| `jsdom` | Optional peer dependency |
79+
| `@edge-runtime/vm` | Optional peer dependency |
80+
| `msw`, `msw/*` | Optional peer dependency for mocking |
81+
82+
### Browser Plugin Exclude List
83+
84+
Additionally, these packages are added to the **browser plugin's exclude list** (in `patchVitestBrowserPackage`), which prevents Vite's optimizer from bundling them during browser tests:
85+
86+
| Package | Reason |
87+
| --------------------- | ------------------------------------------------ |
88+
| `lightningcss` | Native bindings |
89+
| `@tailwindcss/oxide` | Native bindings |
90+
| `tailwindcss` | Pulls in @tailwindcss/oxide |
91+
| `@vitest/browser` | Needs vendor-aliases plugin resolution |
92+
| `@vitest/ui` | Optional peer dependency |
93+
| `@vitest/mocker/node` | Imports @voidzero-dev/vite-plus-core (Node-only) |
94+
95+
This is a different mechanism than `EXTERNAL_BLOCKLIST` - it controls runtime optimization, not build-time bundling.
96+
97+
---
98+
99+
## Migration Guide
100+
101+
For maintainers developing the vitest/vite migration feature, here are the transformations needed.
102+
103+
### Import Rewrites
104+
105+
| Original Import | Rewritten Import |
106+
| ----------------------------------- | -------------------------------------------------------- |
107+
| `from "@vitest/browser-playwright"` | `from "@voidzero-dev/vite-plus-test/browser-playwright"` |
108+
| `from "vite"` | `from "@voidzero-dev/vite-plus-core"` |
109+
| `from "vite/module-runner"` | `from "@voidzero-dev/vite-plus-core/module-runner"` |
110+
111+
**Note:** pnpm overrides don't affect Node.js module resolution at config load time, so config files must use the `@voidzero-dev/vite-plus-test/browser-playwright` import path.
112+
113+
### package.json Changes
114+
115+
**Remove these devDependencies** (now bundled):
116+
117+
```json
118+
{
119+
"devDependencies": {
120+
"@vitest/browser": "...", // Remove
121+
"@vitest/browser-playwright": "...", // Remove
122+
"@vitest/ui": "..." // Remove (peer dep, not bundled but optional)
123+
}
124+
}
125+
```
126+
127+
**Add pnpm overrides**:
128+
129+
```yaml
130+
# pnpm-workspace.yaml
131+
overrides:
132+
vite: 'file:path/to/vite-plus-core.tgz'
133+
vitest: 'file:path/to/vite-plus-test.tgz'
134+
'@vitest/browser': 'file:path/to/vite-plus-test.tgz'
135+
'@vitest/browser-playwright': 'file:path/to/vite-plus-test.tgz'
136+
```
137+
138+
Or using npm package names:
139+
140+
```yaml
141+
overrides:
142+
vite: 'npm:@voidzero-dev/vite-plus-core'
143+
vitest: 'npm:@voidzero-dev/vite-plus-test'
144+
'@vitest/browser': 'npm:@voidzero-dev/vite-plus-test'
145+
'@vitest/browser-playwright': 'npm:@voidzero-dev/vite-plus-test'
146+
```
147+
148+
### Config File Updates
149+
150+
```typescript
151+
// Before
152+
import { playwright } from '@vitest/browser-playwright';
153+
154+
// After
155+
import { playwright } from '@voidzero-dev/vite-plus-test/browser-playwright';
156+
```
157+
158+
### Plugin Exports for pnpm Overrides
159+
160+
The package provides `./plugins/*` exports to enable pnpm overrides for all `@vitest/*` packages:
161+
162+
```
163+
@vitest/runner -> @voidzero-dev/vite-plus-test/plugins/runner
164+
@vitest/utils -> @voidzero-dev/vite-plus-test/plugins/utils
165+
@vitest/utils/error -> @voidzero-dev/vite-plus-test/plugins/utils-error
166+
@vitest/spy -> @voidzero-dev/vite-plus-test/plugins/spy
167+
@vitest/expect -> @voidzero-dev/vite-plus-test/plugins/expect
168+
@vitest/snapshot -> @voidzero-dev/vite-plus-test/plugins/snapshot
169+
@vitest/mocker -> @voidzero-dev/vite-plus-test/plugins/mocker
170+
@vitest/pretty-format -> @voidzero-dev/vite-plus-test/plugins/pretty-format
171+
@vitest/browser -> @voidzero-dev/vite-plus-test/plugins/browser
172+
@vitest/browser-playwright -> @voidzero-dev/vite-plus-test/plugins/browser-playwright
173+
```
174+
175+
---
176+
177+
## Ensuring Bundle Stability
178+
179+
### Build-time Validation
180+
181+
The build script includes `validateExternalDeps()` which:
182+
183+
1. Scans all bundled JS files using `oxc-parser`
184+
2. Extracts all external import specifiers
185+
3. Verifies every external dependency is declared in `dependencies` or `peerDependencies`
186+
4. Reports undeclared externals that would fail at runtime
187+
188+
If this validation fails, the build will report which packages need to be added.
189+
190+
### Testing Strategy
191+
192+
| Test Type | What It Validates |
193+
| --------------------------------------------------------------- | -------------------------------------------------- |
194+
| **Snap tests** (`packages/cli/snap-tests/vitest-browser-mode/`) | Browser mode works after bundling |
195+
| **Ecosystem CI** (`ecosystem-ci/`) | Real-world projects work with bundled vitest |
196+
| **CI workflows** | Multi-platform validation (Ubuntu, Windows, macOS) |
197+
198+
### Vitest Upgrade Checklist
199+
200+
When upgrading the vitest version:
201+
202+
1. **Update version** in `packages/test/package.json`:
203+
```json
204+
{
205+
"devDependencies": {
206+
"vitest-dev": "^NEW_VERSION",
207+
"@vitest/runner": "NEW_VERSION",
208+
"@vitest/utils": "NEW_VERSION"
209+
// ... all @vitest/* packages
210+
}
211+
}
212+
```
213+
214+
2. **Run build**:
215+
```bash
216+
pnpm -C packages/test build
217+
```
218+
219+
3. **Check for new externals**: If `validateExternalDeps()` reports new undeclared dependencies:
220+
- Add to `dependencies` if it should be installed at runtime
221+
- Add to `EXTERNAL_BLOCKLIST` if it should remain external (native bindings, optional)
222+
- If it's a new leaf dep, it will be automatically bundled
223+
224+
4. **Run tests**:
225+
```bash
226+
pnpm test
227+
```
228+
229+
5. **Run ecosystem CI**:
230+
```bash
231+
pnpm -C ecosystem-ci test
232+
```
233+
234+
### Common Upgrade Issues
235+
236+
| Issue | Cause | Solution |
237+
| ----------------------- | ------------------------------ | -------------------------------------------------- |
238+
| New undeclared external | New vitest dependency | Add to `dependencies` or `EXTERNAL_BLOCKLIST` |
239+
| Browser test crashes | Node.js code leaked to browser | Check import rewriting in `rewriteVitestImports()` |
240+
| Missing export | New @vitest/* subpath export | Add to `VITEST_PACKAGE_TO_PATH` |
241+
| pnpm override fails | New plugin export needed | Add to `createPluginExports()` |
242+
243+
---
244+
245+
## Technical Reference
246+
247+
### Build Flow
248+
249+
```
250+
1. bundleVitest() Copy vitest-dev dist/ -> dist/
251+
2. copyVitestPackages() Copy @vitest/* -> dist/@vitest/
252+
3. convertTabsToSpaces() Normalize formatting for patches
253+
4. collectLeafDependencies() Parse imports with oxc-parser
254+
5. bundleLeafDeps() Bundle chai, pathe, etc -> dist/vendor/
255+
6. rewriteVitestImports() Rewrite @vitest/*, vitest/*, vite
256+
7. patchVitestPkgRootPaths() Fix distRoot for relocated files
257+
8. patchVitestBrowserPackage() Inject vendor-aliases plugin
258+
9. patchPlaywrightLocators() Fix browser-safe imports
259+
10. Post-processing:
260+
- patchVendorPaths()
261+
- createBrowserCompatShim()
262+
- createModuleRunnerStub() Browser-safe stub
263+
- createNodeEntry() index-node.js with browser-provider
264+
- copyBrowserClientFiles()
265+
- createPluginExports() dist/plugins/* for pnpm overrides
266+
- mergePackageJson()
267+
- validateExternalDeps()
268+
```
269+
270+
### Output Structure
271+
272+
```
273+
dist/
274+
├── @vitest/ # Copied packages (browser/Node.js safe)
275+
│ ├── runner/
276+
│ ├── utils/
277+
│ ├── spy/
278+
│ ├── expect/
279+
│ ├── snapshot/
280+
│ ├── mocker/
281+
│ ├── pretty-format/
282+
│ ├── browser/
283+
│ └── browser-playwright/
284+
├── vendor/ # Bundled leaf dependencies
285+
│ ├── chai.mjs
286+
│ ├── pathe.mjs
287+
│ ├── tinyrainbow.mjs
288+
│ ├── magic-string.mjs
289+
│ ├── estree-walker.mjs
290+
│ ├── why-is-node-running.mjs
291+
│ └── vitest_*.mjs # Browser stubs
292+
├── plugins/ # Shims for pnpm overrides
293+
│ ├── runner.mjs
294+
│ ├── utils.mjs
295+
│ └── ... (33+ files)
296+
├── chunks/ # Vitest core chunks
297+
├── client/ # Browser client files
298+
├── index.js # Browser-safe entry
299+
├── index-node.js # Node.js entry (includes browser-provider)
300+
├── module-runner-stub.js # Browser-safe module-runner
301+
└── browser-compat.js # @vitest/browser compatibility shim
302+
```
303+
304+
### Browser/Node.js Separation
305+
306+
The critical design decision is maintaining separation between browser and Node.js code:
307+
308+
| Entry Point | Used By | Contains |
309+
| -------------------- | --------------------- | --------------------------------- |
310+
| `dist/index.js` | Browser tests | No Node.js-only code |
311+
| `dist/index-node.js` | Node.js (config, CLI) | Includes browser-provider exports |
312+
313+
This is achieved through:
314+
315+
1. Conditional exports in package.json (`"node": "./dist/index-node.js"`)
316+
2. Browser-safe stubs for `module-runner`
317+
3. Import rewriting to prevent Node.js code from being pulled into browser bundles
318+
4. `vendor-aliases` plugin injection to resolve imports at runtime
319+
320+
### Key Constants
321+
322+
```typescript
323+
// Packages copied to dist/@vitest/
324+
const VITEST_PACKAGES_TO_COPY = [
325+
'@vitest/runner',
326+
'@vitest/utils',
327+
'@vitest/spy',
328+
'@vitest/expect',
329+
'@vitest/snapshot',
330+
'@vitest/mocker',
331+
'@vitest/pretty-format',
332+
'@vitest/browser',
333+
'@vitest/browser-playwright',
334+
];
335+
336+
// Packages that must NOT be bundled (from build.ts lines 131-158)
337+
const EXTERNAL_BLOCKLIST = new Set([
338+
// Our own packages - resolved at runtime
339+
'@voidzero-dev/vite-plus-core',
340+
'@voidzero-dev/vite-plus-core/module-runner',
341+
'vite',
342+
'vitest',
343+
344+
// Peer dependencies - consumers must provide these
345+
'@edge-runtime/vm',
346+
'@opentelemetry/api',
347+
'happy-dom',
348+
'jsdom',
349+
350+
// Optional dependencies with bundling issues or native bindings
351+
'debug', // environment detection broken when bundled
352+
'playwright', // native bindings
353+
354+
// Runtime deps (in package.json dependencies) - not bundled, resolved at install time
355+
'sirv',
356+
'ws',
357+
'pixelmatch',
358+
'pngjs',
359+
360+
// MSW (Mock Service Worker) - optional peer dep of @vitest/mocker
361+
'msw',
362+
'msw/browser',
363+
'msw/core/http',
364+
]);
365+
```

0 commit comments

Comments
 (0)