You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
If these packages were bundled through Rolldown, the bundler would create shared chunks that mix browser-safe code with Node.js-only code (e.g., __vite__injectQuery). When the browser test runner loads these mixed chunks, it crashes with errors like "Identifier already declared" because Node.js-specific code is not valid in browser contexts.
By copying the original file structure, the browser/Node.js separation that upstream Vitest maintains is preserved. The dist/index.js entry stays browser-safe, and dist/index-node.js includes the Node.js-only browser-provider exports.
Tier 2: Bundled Leaf Dependencies
These 6 packages are bundled by Rolldown into individual files under dist/vendor/:
Package
Vendor File
Purpose
chai
dist/vendor/chai.mjs
Assertion library (core of expect)
pathe
dist/vendor/pathe.mjs
Cross-platform path utilities
tinyrainbow
dist/vendor/tinyrainbow.mjs
Terminal color output
magic-string
dist/vendor/magic-string.mjs
String manipulation with source maps
estree-walker
dist/vendor/estree-walker.mjs
AST traversal utility
why-is-node-running
dist/vendor/why-is-node-running.mjs
Debug tool for hanging processes
Why Bundle These?
These are pure JavaScript leaf dependencies with no native bindings, no environment-specific behavior, and no reason to stay as separate installed packages. Bundling them:
Reduces install size (fewer node_modules entries)
Eliminates version resolution complexity for consumers
They were moved from dependencies to devDependencies since they ship inside the bundle
After bundling, all imports to these packages throughout the copied @vitest/* files are rewritten to point to ../vendor/<package>.mjs relative paths.
Tier 3: External Dependencies (NOT Bundled)
3a. Runtime Dependencies (dependencies in package.json)
These are installed alongside the package at npm install time:
Package
Reason NOT Bundled
sirv
Static file server with complex runtime behavior — bundling would break its dynamic module loading and middleware patterns
ws
WebSocket server — has optional native binding dependencies (bufferutil, utf-8-validate) that can't be bundled
pixelmatch
Image comparison library — optional feature (browser visual testing), kept separate to avoid bloating the core bundle
pngjs
PNG encoding/decoding — optional feature companion to pixelmatch
es-module-lexer
ESM import/export parser — small WASM-based module; bundling WASM modules is fragile and the package is tiny
expect-type
Type-level testing utility — small CJS package that needs special default → named export extraction (CJS_REEXPORT_PACKAGES)
obug
Debugging utility — very small, not worth the bundling complexity
picomatch
Glob pattern matching — very small, widely shared dependency
std-env
Environment detection (CI, browser, Node.js) — must run unbundled to correctly detect the runtime environment
tinybench
Benchmarking library — optional feature (vitest bench), kept separate
tinyexec
Child process execution — small, straightforward runtime dependency
tinyglobby
File globbing — small, uses native filesystem APIs that must remain unbundled
3b. Peer/Optional Dependencies (consumers must install)
Package
Reason NOT Bundled
playwright
Native bindings — includes platform-specific browser binaries; cannot be bundled, user must install matching version
webdriverio
Native bindings — platform-specific WebDriver binaries; same reason as playwright
happy-dom
Optional peer dependency — alternative DOM implementation for jsdom; only needed if user chooses this environment
jsdom
Optional peer dependency — DOM implementation for Node.js testing; large dependency tree, only needed if user opts in
@edge-runtime/vm
Optional peer dependency — edge runtime VM for testing edge environments; niche use case
@opentelemetry/api
Optional peer dependency — OpenTelemetry instrumentation; only for users who want tracing
@vitest/ui
Optional peer dependency — Vitest's web UI dashboard; separate install since most users don't need it
vite
Sibling package — resolved at runtime to @voidzero-dev/vite-plus-core (the bundled Vite); cannot self-bundle
3c. Explicitly Blocklisted (EXTERNAL_BLOCKLIST)
These are in build.ts lines 143-172 and are explicitly prevented from being bundled during the Rolldown leaf-dep bundling phase:
Package
Specific Reason
@voidzero-dev/vite-plus-core
Own package — resolved at runtime via package.json dependencies
@voidzero-dev/vite-plus-core/module-runner
Subpath of own core package
vite
Alias for the core package (rewritten during import rewriting)
vitest
Self-reference — would create circular bundling
debug
Environment detection breaks when bundled — debug uses process.env.DEBUG and feature-detects its output target (TTY, browser console, etc.) at module load time. When bundled, the detection code runs in the wrong context and debug() stops producing output
@standard-schema/spec
Types-only import — imported by @vitest/expect purely for TypeScript types; has no runtime code to bundle
msw
Optional peer dependency of @vitest/mocker — Mock Service Worker is large and only needed if users want MSW-based mocking
msw/browser
Subpath of msw
msw/core/http
Subpath of msw
3d. Browser Plugin Exclude List (Runtime Optimization)
A separate mechanism from EXTERNAL_BLOCKLIST — these are added to Vite's browser optimizer exclude list (in patchVitestBrowserPackage) to prevent pre-bundling during browser tests:
After copying and bundling, the build applies 19 patching/creation functions to transform upstream Vitest into a fully integrated vite-plus component. Each patch exists for a specific technical reason.
1. bundleVitest() — Vite Import Rewriting (line 436)
What: Copies vitest-dev's dist/ into the package root, rewriting all vite/vitest import specifiers in .js, .mjs, .cjs, .d.ts, .d.cts files during copy.
Why: Vitest imports from "vite" everywhere. In vite-plus, the bundled Vite lives in @voidzero-dev/vite-plus-core. Without this rewrite, vitest would try to resolve upstream vite at runtime and fail.
Transformations:
Before
After
from 'vite'
from '@voidzero-dev/vite-plus-core'
from 'vite/module-runner'
from '@voidzero-dev/vite-plus-core/module-runner'
import('vite')
import('@voidzero-dev/vite-plus-core')
require('vite')
require('@voidzero-dev/vite-plus-core')
declare module "vite"
declare module "@voidzero-dev/vite-plus-core"
import('vitest')
import('@voidzero-dev/vite-plus-test')
The import('vitest') rewrite is critical for globals.d.ts, which declares types like typeof import('vitest')['test']. Without it, vitest is not resolvable from the package context in pnpm's strict layout. TypeScript silently treats unresolved dynamic type imports as any, but oxlint's type-aware linting treats them as error types, causing no-unsafe-call errors.
2. brandVitest() — CLI Rebranding (line 492)
What: Patches dist/chunks/cac.*.js (CLI parser) and dist/chunks/cli-api.*.js (banner display) to rebrand vitest CLI output.
Why: When users run vp test, the CLI should show vite-plus branding and version, not upstream vitest. This prevents user confusion about which tool they're running.
What: Copies dist/ directories from 11 @vitest/* packages in node_modules to dist/@vitest/. Also copies root .d.ts files from @vitest/browser (e.g., context.d.ts, matchers.d.ts).
Why: Bundling these packages would create shared Rolldown chunks mixing browser-safe and Node.js-only code. Copying preserves upstream's careful browser/Node.js separation.
What: Replaces all tab characters with two spaces in every dist/**/*.js file.
Why: Subsequent patching functions use space-based string matching patterns. Without normalization, patches that search for specific indented code would fail on files using tabs. This makes all pattern matching reliable and portable.
What: Parses all copied .js files with oxc-parser, extracts every import specifier (static imports, re-exports, dynamic imports), and identifies external npm packages that should be bundled.
Why: Rather than maintaining a manual list of leaf dependencies, this function dynamically discovers which packages are actually imported. This ensures the bundled set stays in sync with upstream changes automatically.
What: Runs Rolldown to bundle discovered leaf dependencies into individual dist/vendor/*.mjs files. Also generates .d.ts stubs via rolldown-plugin-dts.
Why: These pure JS packages (chai, pathe, etc.) have no reason to remain as separate installed packages. Bundling them reduces install size and eliminates version resolution issues. Each is bundled independently (not into shared chunks) to maintain the browser/Node.js separation principle.
What: Rewrites all import specifiers in dist/@vitest/**, dist/*.js, dist/*.d.ts, and dist/chunks/** using AST-based analysis (oxc-parser). Transforms bare specifiers to relative paths.
Why: After copying and bundling, files are in new locations. Bare specifiers like from '@vitest/runner' or from 'chai' won't resolve from dist/. Every import must become a relative path to the actual file location.
Key rewrites:
Before
After
from '@vitest/runner'
from '../runner/index.js' (relative)
from 'chai'
from '../vendor/chai.mjs' (relative)
from 'vitest'
from './index.js' (relative)
from 'vitest/node'
from './node.js' (relative)
Special handling:
Files inside @vitest/browser/ preserve bare vitest/browser — handled by the vendor-aliases virtual module plugin at runtime
@vitest/mocker entry files: removes redundant side-effect imports from vendor
All vitest-dev files: strips @voidzero-dev/vite-plus-core/module-runner side-effect imports to prevent browser hanging
What: Adjusts relative path calculations in dist/vendor/*.mjs files.
Why: Bundled vendor code inherited path calculations from the original packages that assume files are in dist/. But vendor files are one level deeper at dist/vendor/. Going up "../.." from dist/vendor/file.mjs reaches the wrong directory.
What: Patches the VitestCoreResolver plugin's resolveId in dist/chunks/cli-api.*.js to recognize vite-plus package names.
Why: CLI's export * from '@voidzero-dev/vite-plus-test' creates a re-export chain that breaks module identity in Vite's SSR transform. When expect.extend() mutates the chai module (adding custom matchers), those changes aren't visible through the re-export chain because SSR creates separate module instances. By resolving vite-plus/test and @voidzero-dev/vite-plus-test directly to dist/index.js, the resolver bypasses the re-export chain and ensures a single module instance.
What: Patches dist/@vitest/*/index.js for all 11 copied packages, replacing the pkgRoot/distRoot path calculation.
Why: Original packages calculate their root as resolve(import.meta.url, "../.."), assuming files are at node_modules/@vitest/pkg/dist/index.js. In the bundled output, files are at dist/@vitest/pkg/index.js — the directory structure is different. The original calculation would resolve to the wrong directory.
What: Applies 5 patches to dist/@vitest/browser/index.js:
Injects the vitest:vendor-aliases Vite plugin
Adds native dependency exclusions to the browser optimizer
Removes broken "vitest > expect-type" include patterns
Adds BrowserContext alias handling for vite-plus package paths
Injects VP_VERSION env var to prevent "Running mixed versions" warning
Why (for each):
Vendor aliases plugin: During browser tests, Vite processes imports. The @vitest/* and vitest/* bare specifiers need custom resolution to find the copied files in dist/@vitest/. This plugin intercepts those imports and resolves them to the correct paths. It also provides browser-safe stubs for Node.js-only modules like vite/module-runner.
Native dep exclusions: Packages like lightningcss, @tailwindcss/oxide, and tailwindcss contain native bindings that crash Vite's browser optimizer. They must be excluded from pre-bundling.
Include pattern removal: Vitest uses "vitest > expect-type" patterns to tell Vite's optimizer to bundle specific nested dependencies. These patterns don't work when the dependency tree is restructured by bundling.
BrowserContext aliases: When vitest isn't overridden via pnpm, users import from vite-plus/test or @voidzero-dev/vite-plus-test. The BrowserContext plugin must recognize these aliases to return the correct virtual module.
Version env var: Vitest checks that the CLI version matches the library version. Since vite-plus wraps vitest, the version check would fail without injecting the correct version.
What: Patches dist/@vitest/browser-*/locators.js for playwright, webdriverio, and preview providers.
Why: The original locator files import { page, server } from ../browser/index.js, which contains Node.js server code. In browser context, server doesn't exist. The patch changes the import to use context.js (browser-safe) and replaces server.config.browser.locators with window.__vitest_worker__.config.browser.locators.
13. createBrowserCompatShim() — @vitest/browser Override Support (line 1744)
What: Creates dist/browser-compat.js with re-exports of browser-provider symbols.
Why: When this package is used as a pnpm override for @vitest/browser, code that imports from @vitest/browser needs to find exports like defineBrowserProvider, defineBrowserCommand, parseKeyDef, resolveScreenshotPath. This shim re-exports them from the copied @vitest/browser/index.js.
What: Creates dist/module-runner-stub.js with stub implementations of Vite's module runner classes.
Why: The real vite/module-runner contains Node.js-only code (process.platform, Buffer, fs) that crashes browsers. Browser code may import it transitively. The stub provides placeholder classes (EvaluatedModules, ModuleRunner, ESModulesEvaluator) and helper functions that either no-op or throw meaningful errors in the browser.
15. createNodeEntry() — Node.js-Specific Entry Point (line 1842)
What: Creates dist/index-node.js that re-exports everything from index.js plus browser-provider symbols from @vitest/browser/index.js.
Why: Browser code must use index.js (safe). Node.js code (CLI, config loading) needs additional exports like defineBrowserProvider, defineBrowserCommand, etc. The conditional export in package.json ("node": "./dist/index-node.js") routes Node.js to this file automatically. The "browser" condition is placed BEFORE "node" because vitest passes custom --conditions browser to worker processes (e.g., Nuxt edge/cloudflare presets). Without this ordering, Node.js would match "node" first, loading index-node.js which imports ws, but with --conditions browser, ws resolves to its browser stub that doesn't export WebSocketServer, causing a SyntaxError.
What: Rewrites TypeScript module augmentation declarations in dist/chunks/global.d.*.d.ts and adds BrowserCommands re-export to dist/@vitest/browser/context.d.ts.
Why: TypeScript module augmentations use bare specifiers like declare module "@vitest/expect". Since @vitest/expect is bundled inside dist/@vitest/expect/, the bare specifier doesn't resolve. The patch rewrites augmentations to use relative paths (declare module "../@vitest/expect/index.js") and merges the augmented interfaces directly into the target .d.ts files so TypeScript can find them.
17. patchChaiTypeReference() — Chai Type Resolution (line 2300)
What: Adds /// <reference types="chai" /> to dist/@vitest/expect/index.d.ts.
Why:@vitest/expect types use the Chai namespace (e.g., Chai.ChaiPlugin) without explicit reference. When @vitest/expect is bundled inside the package, TypeScript can't automatically discover @types/chai. Without the reference directive, chai-specific features like the .not property are missing from type completions.
18. patchMockerHoistedModule() — Mock API Source Recognition (line 2331)
What: Patches the hoistMocks equality check in dist/@vitest/mocker/node.js (or its chunk).
Why: Vitest's mocker checks if vi is imported from 'vitest' to validate mock API usage. When users import from 'vite-plus/test' or '@voidzero-dev/vite-plus-test', the check fails and throws "There are some problems in resolving the mocks API". The patch adds these package names as valid sources.
19. patchServerDepsInline() — Module Identity for expect.extend() (line 2387)
What: Patches dist/chunks/cli-api.*.js to auto-inline specific third-party packages in the ModuleRunnerTransform plugin's configResolved handler.
Why: Libraries like @testing-library/jest-dom, @storybook/test, and jest-extended call expect.extend() internally to add custom matchers. Under npm/pnpm override, these libraries create separate module instances of chai — one from the library's own node_modules and one from vite-plus's bundled version. When expect.extend() registers matchers on the library's chai instance, tests using vite-plus's chai instance don't see the matchers. Auto-inlining forces these libraries through Vite's module runner, which resolves them to the same chai instance.
Auto-inlined packages:
@testing-library/jest-dom
@storybook/test
jest-extended
The patch uses createRequire().resolve() to check if each package is installed before adding it, so uninstalled packages are silently skipped.
What: Creates dist/plugins/*.mjs shim files, one for each @vitest/* package and subpath.
Why: pnpm overrides redirect @vitest/runner → @voidzero-dev/vite-plus-test. But the package's main export is the vitest entry, not @vitest/runner. These shim files provide dedicated export paths (./plugins/runner) that re-export from the correct copied file (../@vitest/runner/index.js).
What: Merges upstream vitest's package.json fields (exports, peerDependencies, engines, etc.) into the vite-plus-test package.json. Adds conditional Node.js/browser exports, browser-provider exports, plugin exports, and records the bundled vitest version.
Why: The published package needs to expose all of vitest's export paths so consumers can import any vitest/* subpath. It also removes bundled browser providers from peerDependencies (users don't need to install them separately) and adds custom export paths for vite-plus-specific usage patterns.
What: Scans all dist/**/*.{js,mjs,cjs} files with oxc-parser, extracts every external import specifier, and verifies each is declared in dependencies or peerDependencies.
Why: After all the copying, bundling, and rewriting, an undeclared external dependency would silently fail at runtime in consumer projects. This validation catches any dependency that was missed — whether from a vitest upgrade introducing a new dep, or a rewriting bug leaving a bare specifier intact.
TODO: Patches That Could Be Contributed Back to Upstream Vitest
The patches above fall into two categories: vite-plus-specific (namespace rewriting, bundling mechanics) and genuine upstream improvements that benefit all vitest users. Below is the analysis.
Clearly Upstreamable
TODO 1 Exclude native bindings from browser optimizer (patchVitestBrowserPackage)
What: Add lightningcss, @tailwindcss/oxide, and tailwindcss to the browser optimizeDeps.exclude list in @vitest/browser.
Why upstream needs this: Upstream vitest's exclude list (in @vitest/browser/dist/index.js) does NOT include these packages. When users run vitest browser mode in a project using Tailwind CSS v4 or Lightning CSS, Vite's dependency optimizer tries to pre-bundle these native-binding packages, which crashes because native .node binaries can't be optimized. This affects all vitest browser mode users with these dependencies, not just vite-plus.
Evidence: Upstream exclude list only has: vitest, vitest/browser, @vitest/*, std-env, tinybench, tinyspy, tinyrainbow, pathe, msw, msw/browser. No native binding packages.
Contribution form: PR to packages/browser/src/node/plugin.ts in vitest-dev/vitest adding these to the exclude array.
TODO 2: Make mocker hoistMocks source check extensible (patchMockerHoistedModule)
What: The hoistMocks function in @vitest/mocker hardcodes hoistedModule = "vitest" and checks if (hoistedModule === source). This means any wrapper package that re-exports vitest's vi object (not just vite-plus, but any custom test harness) will fail with "There are some problems in resolving the mocks API."
Why upstream needs this: The hardcoded check prevents the vitest ecosystem from building wrapper packages. The hoistMocks options already accept hoistedModule as a parameter, but the default is hardcoded and there's no mechanism for wrapper packages to register alternative source names. A more flexible approach (e.g., accepting an array of valid source names, or a callback) would enable the ecosystem.
Evidence:@vitest/mocker/dist/chunk-hoistMocks.js line 368: hoistedModule = "vitest" with strict equality on line 384.
Contribution form: PR to @vitest/mocker to accept hoistedModule as string | string[], or add a hoistedModules array option. This is a non-breaking change since the existing string option still works.
TODO 3: Auto-inline packages that use expect.extend() (patchServerDepsInline)
What: Automatically add @testing-library/jest-dom, @storybook/test, and jest-extended to server.deps.inline when they're installed.
Why upstream needs this: The module identity problem with expect.extend() affects any vitest user using npm/pnpm overrides (not just vite-plus). When these libraries call require('vitest').expect.extend(matchers), the override mechanism can create a separate module instance, so matchers register on a different chai instance than the test runner uses. This manifests as "expect(...).toBeInTheDocument is not a function" — a common vitest issue. Auto-inlining ensures these libraries share the same module instance.
Evidence: The issue is well-documented in vite-plus issue #897, but the root cause (module identity under overrides) is a general vitest concern.
Contribution form: PR to vitest's ModuleRunnerTransform plugin (configResolved handler) to auto-detect and inline known expect.extend() packages. Include a test.server.deps.autoInline config option to control this behavior.
TODO 4: Export condition ordering for --conditions browser (mergePackageJson)
What: The "browser" export condition must come before "node" in vitest's package.json exports.
Why upstream needs this: When frameworks like Nuxt set edge/cloudflare presets, vitest passes --conditions browser to worker processes. Node.js resolvers check conditions in order — if "node" comes before "browser", Node.js matches "node" first, loads the Node.js entry which imports ws, but with --conditions browser active, ws resolves to its browser stub (ws/browser.js) that doesn't export WebSocketServer, causing a SyntaxError. This affects all vitest users with Nuxt edge/cloudflare presets.
Evidence: Documented in vite-plus issue #831. Upstream vitest's package.json doesn't have separate browser/node entries for the main export (it uses a single "import" field), so upstream may not hit this today — but if vitest ever adds conditional exports (which is a growing pattern), the ordering matters.
Contribution form: If/when vitest adds conditional exports, ensure "browser" precedes "node". Could also be contributed as documentation or a test case.
Potentially Upstreamable (Design Improvements)
TODO 5: Browser-safe module-runner stub (createModuleRunnerStub)
What: Provide an official browser-safe stub for vite/module-runner that exports placeholder classes instead of Node.js-only code.
Why upstream could benefit: The real vite/module-runner contains process.platform, Buffer, and fs imports that crash browsers. Any vitest browser mode code that transitively imports vite/module-runner will hang. Currently upstream vitest avoids this through careful import structure, but it's fragile — any refactoring that accidentally pulls in module-runner will break browser mode silently.
Contribution form: PR to vite itself to provide a "browser" export condition for vite/module-runner that exports safe stubs, similar to how ws provides ws/browser.js.
TODO 6: Browser provider locators should not import server (patchBrowserProviderLocators)
What: The locator files in @vitest/browser-playwright, @vitest/browser-webdriverio, and @vitest/browser-preview import { page, server } from vitest/browser. In browser context, server is unnecessary — the config is available via window.__vitest_worker__.config.
Why upstream could benefit: Importing server from the main browser index pulls in Node.js server code into the browser module graph. While upstream vitest avoids issues through its module resolution, this coupling is architecturally fragile. If the locator files used window.__vitest_worker__.config.browser.locators instead of server.config.browser.locators, it would be cleaner and more resilient.
Evidence:@vitest/browser-playwright/dist/locators.js line 2: import { page, server } from 'vitest/browser';
Contribution form: PR to @vitest/browser-* locator implementations to use window.__vitest_worker__.config instead of the server import for configuration access.
NOT Upstreamable (Vite-Plus-Specific)
These patches exist solely because of vite-plus's bundling strategy (embedding vitest inside a different package namespace) and have no upstream equivalent:
Patch
Why It's Vite-Plus-Specific
bundleVitest()
Rewrites vite → @voidzero-dev/vite-plus-core (namespace change)
brandVitest()
Rebrands CLI to "vp test" (product branding)
copyVitestPackages()
Copies @vitest/* to dist/ (bundling strategy)
convertTabsToSpaces()
Normalizes formatting for subsequent patches
collectLeafDependencies()
Discovers deps to bundle (bundling strategy)
bundleLeafDeps()
Bundles chai, pathe, etc. into vendor/ (bundling strategy)
rewriteVitestImports()
Rewrites bare specifiers to relative paths (bundling strategy)
patchVendorPaths()
Fixes depth calculation for vendor/ subdirectory
patchVitestCoreResolver()
Adds vite-plus package name recognition to resolver
patchVitestPkgRootPaths()
Fixes distRoot after file relocation
patchVitestBrowserPackage() (vendor-aliases)
Injects plugin for custom resolution of relocated packages
createBrowserCompatShim()
Shim for @vitest/browser pnpm override
createNodeEntry()
Separate browser/node entry points for bundled package
patchModuleAugmentations()
Fixes TS augmentations using bare specifiers that don't resolve after bundling
patchChaiTypeReference()
Adds /// <reference types="chai" /> lost during bundling
createPluginExports()
Creates shim files for pnpm overrides
mergePackageJson()
Assembles package.json for published package
validateExternalDeps()
Build integrity check for bundled output
Key Design Decisions
Browser/Node.js separation is the primary architectural constraint. The entire hybrid strategy exists because Rolldown's chunk-splitting would mix browser and Node.js code, causing runtime crashes in browser test mode.
Import rewriting is pervasive. Every import to vite, @vitest/*, and leaf dependencies is rewritten to point to the correct location within the dist/ tree. This is handled by rewriteVitestImports() using oxc-parser for accurate AST-based transformations.
vendor-aliases plugin is injected at runtime. Since imports are rewritten to relative paths within dist/, a Vite plugin (vendor-aliases) is injected into @vitest/browser to resolve @vitest/* and vitest/* imports correctly when Vite processes them during browser tests.
Build-time validation catches drift.validateExternalDeps() scans all bundled files and verifies every external import is declared in dependencies or peerDependencies, preventing silent runtime failures when upgrading Vitest.
Overview
The
@voidzero-dev/vite-plus-testpackage (inpackages/test/) bundles Vitest v4.1.2 using a hybrid 3-tier bundling strategy:@vitest/*packages are copied verbatim intodist/@vitest/dist/vendor/*.mjsThe build is orchestrated by
packages/test/build.ts(~2,600 lines) and uses Rolldown as the bundler.Tier 1: Copied Packages (Copied As-Is)
These 11
@vitest/*packages are copied fromnode_modulestodist/@vitest/, preserving their original file structure:@vitest/runner@vitest/utils@vitest/spy@vitest/expect@vitest/snapshot@vitest/mocker@vitest/pretty-format@vitest/browser@vitest/browser-playwright@vitest/browser-webdriverio@vitest/browser-previewWhy Copy Instead of Bundle?
Critical reason: Browser/Node.js code separation.
If these packages were bundled through Rolldown, the bundler would create shared chunks that mix browser-safe code with Node.js-only code (e.g.,
__vite__injectQuery). When the browser test runner loads these mixed chunks, it crashes with errors like "Identifier already declared" because Node.js-specific code is not valid in browser contexts.By copying the original file structure, the browser/Node.js separation that upstream Vitest maintains is preserved. The
dist/index.jsentry stays browser-safe, anddist/index-node.jsincludes the Node.js-only browser-provider exports.Tier 2: Bundled Leaf Dependencies
These 6 packages are bundled by Rolldown into individual files under
dist/vendor/:chaidist/vendor/chai.mjsexpect)pathedist/vendor/pathe.mjstinyrainbowdist/vendor/tinyrainbow.mjsmagic-stringdist/vendor/magic-string.mjsestree-walkerdist/vendor/estree-walker.mjswhy-is-node-runningdist/vendor/why-is-node-running.mjsWhy Bundle These?
These are pure JavaScript leaf dependencies with no native bindings, no environment-specific behavior, and no reason to stay as separate installed packages. Bundling them:
node_modulesentries)dependenciestodevDependenciessince they ship inside the bundleAfter bundling, all imports to these packages throughout the copied
@vitest/*files are rewritten to point to../vendor/<package>.mjsrelative paths.Tier 3: External Dependencies (NOT Bundled)
3a. Runtime Dependencies (
dependenciesin package.json)These are installed alongside the package at
npm installtime:sirvwsbufferutil,utf-8-validate) that can't be bundledpixelmatchpngjspixelmatches-module-lexerexpect-typedefault→ named export extraction (CJS_REEXPORT_PACKAGES)obugpicomatchstd-envtinybenchtinyexectinyglobby3b. Peer/Optional Dependencies (consumers must install)
playwrightwebdriveriohappy-domjsdom@edge-runtime/vm@opentelemetry/api@vitest/uivite@voidzero-dev/vite-plus-core(the bundled Vite); cannot self-bundle3c. Explicitly Blocklisted (
EXTERNAL_BLOCKLIST)These are in
build.tslines 143-172 and are explicitly prevented from being bundled during the Rolldown leaf-dep bundling phase:@voidzero-dev/vite-plus-core@voidzero-dev/vite-plus-core/module-runnervitevitestdebugdebugusesprocess.env.DEBUGand feature-detects its output target (TTY, browser console, etc.) at module load time. When bundled, the detection code runs in the wrong context anddebug()stops producing output@standard-schema/spec@vitest/expectpurely for TypeScript types; has no runtime code to bundlemsw@vitest/mocker— Mock Service Worker is large and only needed if users want MSW-based mockingmsw/browsermswmsw/core/httpmsw3d. Browser Plugin Exclude List (Runtime Optimization)
A separate mechanism from
EXTERNAL_BLOCKLIST— these are added to Vite's browser optimizer exclude list (inpatchVitestBrowserPackage) to prevent pre-bundling during browser tests:lightningcss@tailwindcss/oxidetailwindcss@tailwindcss/oxide(native)@vitest/browser*@vitest/ui@vitest/mocker/node@voidzero-dev/vite-plus-corewhich is Node.js-onlyBuild Pipeline Summary
Output Structure
All Patches Applied to Vitest Dist Files
After copying and bundling, the build applies 19 patching/creation functions to transform upstream Vitest into a fully integrated vite-plus component. Each patch exists for a specific technical reason.
1.
bundleVitest()— Vite Import Rewriting (line 436)What: Copies vitest-dev's
dist/into the package root, rewriting all vite/vitest import specifiers in.js,.mjs,.cjs,.d.ts,.d.ctsfiles during copy.Why: Vitest imports
from "vite"everywhere. In vite-plus, the bundled Vite lives in@voidzero-dev/vite-plus-core. Without this rewrite, vitest would try to resolve upstream vite at runtime and fail.Transformations:
from 'vite'from '@voidzero-dev/vite-plus-core'from 'vite/module-runner'from '@voidzero-dev/vite-plus-core/module-runner'import('vite')import('@voidzero-dev/vite-plus-core')require('vite')require('@voidzero-dev/vite-plus-core')declare module "vite"declare module "@voidzero-dev/vite-plus-core"import('vitest')import('@voidzero-dev/vite-plus-test')The
import('vitest')rewrite is critical forglobals.d.ts, which declares types liketypeof import('vitest')['test']. Without it,vitestis not resolvable from the package context in pnpm's strict layout. TypeScript silently treats unresolved dynamic type imports asany, but oxlint's type-aware linting treats them aserrortypes, causingno-unsafe-callerrors.2.
brandVitest()— CLI Rebranding (line 492)What: Patches
dist/chunks/cac.*.js(CLI parser) anddist/chunks/cli-api.*.js(banner display) to rebrand vitest CLI output.Why: When users run
vp test, the CLI should show vite-plus branding and version, not upstream vitest. This prevents user confusion about which tool they're running.Transformations:
cac("vitest")cac("vp test")var version = "1.0.0"var version = process.env.VP_VERSION || "1.0.0"/^vitest\/\d+\.\d+\.\d+$//^vp test\/[\d.]+$/$ vitest --help --expand-help$ vp test --help --expand-helpprintBanner()3.
copyVitestPackages()— Preserve File Structure (line 593)What: Copies
dist/directories from 11@vitest/*packages innode_modulestodist/@vitest/. Also copies root.d.tsfiles from@vitest/browser(e.g.,context.d.ts,matchers.d.ts).Why: Bundling these packages would create shared Rolldown chunks mixing browser-safe and Node.js-only code. Copying preserves upstream's careful browser/Node.js separation.
4.
convertTabsToSpaces()— Formatting Normalization (line 1371)What: Replaces all tab characters with two spaces in every
dist/**/*.jsfile.Why: Subsequent patching functions use space-based string matching patterns. Without normalization, patches that search for specific indented code would fail on files using tabs. This makes all pattern matching reliable and portable.
5.
collectLeafDependencies()— Dependency Discovery (line 671)What: Parses all copied
.jsfiles withoxc-parser, extracts every import specifier (static imports, re-exports, dynamic imports), and identifies external npm packages that should be bundled.Why: Rather than maintaining a manual list of leaf dependencies, this function dynamically discovers which packages are actually imported. This ensures the bundled set stays in sync with upstream changes automatically.
Filtering rules:
@vitest/*,vitest/*,vite/*, Node.js builtins,EXTERNAL_BLOCKLISTentries, subpath imports (#)6.
bundleLeafDeps()— Leaf Dependency Bundling (line 782)What: Runs Rolldown to bundle discovered leaf dependencies into individual
dist/vendor/*.mjsfiles. Also generates.d.tsstubs viarolldown-plugin-dts.Why: These pure JS packages (chai, pathe, etc.) have no reason to remain as separate installed packages. Bundling them reduces install size and eliminates version resolution issues. Each is bundled independently (not into shared chunks) to maintain the browser/Node.js separation principle.
Config: Platform
neutral, treeshakefalse, externals include Node builtins + blocklist +@vitest/*+vitest/*+vite/*.7.
rewriteVitestImports()— Import Path Rewriting (line 891)What: Rewrites all import specifiers in
dist/@vitest/**,dist/*.js,dist/*.d.ts, anddist/chunks/**using AST-based analysis (oxc-parser). Transforms bare specifiers to relative paths.Why: After copying and bundling, files are in new locations. Bare specifiers like
from '@vitest/runner'orfrom 'chai'won't resolve fromdist/. Every import must become a relative path to the actual file location.Key rewrites:
from '@vitest/runner'from '../runner/index.js'(relative)from 'chai'from '../vendor/chai.mjs'(relative)from 'vitest'from './index.js'(relative)from 'vitest/node'from './node.js'(relative)Special handling:
@vitest/browser/preserve barevitest/browser— handled by the vendor-aliases virtual module plugin at runtime@vitest/mockerentry files: removes redundant side-effect imports from vendor@voidzero-dev/vite-plus-core/module-runnerside-effect imports to prevent browser hanging8.
patchVendorPaths()— Vendor Directory Depth Fix (line 1259)What: Adjusts relative path calculations in
dist/vendor/*.mjsfiles.Why: Bundled vendor code inherited path calculations from the original packages that assume files are in
dist/. But vendor files are one level deeper atdist/vendor/. Going up"../.."fromdist/vendor/file.mjsreaches the wrong directory.Transformations:
resolve(fileURLToPath(import.meta.url), "../..")resolve(fileURLToPath(import.meta.url), "../../..")resolve(__dirname, "context.js")resolve(__dirname, "../context.js")9.
patchVitestCoreResolver()— Module Identity Fix (line 1315)What: Patches the
VitestCoreResolverplugin'sresolveIdindist/chunks/cli-api.*.jsto recognize vite-plus package names.Why: CLI's
export * from '@voidzero-dev/vite-plus-test'creates a re-export chain that breaks module identity in Vite's SSR transform. Whenexpect.extend()mutates the chai module (adding custom matchers), those changes aren't visible through the re-export chain because SSR creates separate module instances. By resolvingvite-plus/testand@voidzero-dev/vite-plus-testdirectly todist/index.js, the resolver bypasses the re-export chain and ensures a single module instance.Added resolution rules:
"vite-plus/test"→dist/index.js(direct, bypasses re-export)"@voidzero-dev/vite-plus-test"→dist/index.js(direct)"vite-plus/test/*"→ delegates to"vitest/*"resolution"@voidzero-dev/vite-plus-test/*"→ delegates to"vitest/*"resolution10.
patchVitestPkgRootPaths()— Package Root Recalculation (line 1393)What: Patches
dist/@vitest/*/index.jsfor all 11 copied packages, replacing thepkgRoot/distRootpath calculation.Why: Original packages calculate their root as
resolve(import.meta.url, "../.."), assuming files are atnode_modules/@vitest/pkg/dist/index.js. In the bundled output, files are atdist/@vitest/pkg/index.js— the directory structure is different. The original calculation would resolve to the wrong directory.Before:
After:
11.
patchVitestBrowserPackage()— Browser Plugin Injection (line 1442)What: Applies 5 patches to
dist/@vitest/browser/index.js:vitest:vendor-aliasesVite plugin"vitest > expect-type"include patternsVP_VERSIONenv var to prevent "Running mixed versions" warningWhy (for each):
Vendor aliases plugin: During browser tests, Vite processes imports. The
@vitest/*andvitest/*bare specifiers need custom resolution to find the copied files indist/@vitest/. This plugin intercepts those imports and resolves them to the correct paths. It also provides browser-safe stubs for Node.js-only modules likevite/module-runner.Native dep exclusions: Packages like
lightningcss,@tailwindcss/oxide, andtailwindcsscontain native bindings that crash Vite's browser optimizer. They must be excluded from pre-bundling.Include pattern removal: Vitest uses
"vitest > expect-type"patterns to tell Vite's optimizer to bundle specific nested dependencies. These patterns don't work when the dependency tree is restructured by bundling.BrowserContext aliases: When
vitestisn't overridden via pnpm, users import fromvite-plus/testor@voidzero-dev/vite-plus-test. The BrowserContext plugin must recognize these aliases to return the correct virtual module.Version env var: Vitest checks that the CLI version matches the library version. Since vite-plus wraps vitest, the version check would fail without injecting the correct version.
12.
patchBrowserProviderLocators()— Browser-Safe Provider Imports (line 1661)What: Patches
dist/@vitest/browser-*/locators.jsfor playwright, webdriverio, and preview providers.Why: The original locator files import
{ page, server }from../browser/index.js, which contains Node.js server code. In browser context,serverdoesn't exist. The patch changes the import to usecontext.js(browser-safe) and replacesserver.config.browser.locatorswithwindow.__vitest_worker__.config.browser.locators.Before:
After:
13.
createBrowserCompatShim()— @vitest/browser Override Support (line 1744)What: Creates
dist/browser-compat.jswith re-exports of browser-provider symbols.Why: When this package is used as a pnpm override for
@vitest/browser, code that imports from@vitest/browserneeds to find exports likedefineBrowserProvider,defineBrowserCommand,parseKeyDef,resolveScreenshotPath. This shim re-exports them from the copied@vitest/browser/index.js.14.
createModuleRunnerStub()— Browser-Safe Module Runner (line 1779)What: Creates
dist/module-runner-stub.jswith stub implementations of Vite's module runner classes.Why: The real
vite/module-runnercontains Node.js-only code (process.platform,Buffer,fs) that crashes browsers. Browser code may import it transitively. The stub provides placeholder classes (EvaluatedModules,ModuleRunner,ESModulesEvaluator) and helper functions that either no-op or throw meaningful errors in the browser.15.
createNodeEntry()— Node.js-Specific Entry Point (line 1842)What: Creates
dist/index-node.jsthat re-exports everything fromindex.jsplus browser-provider symbols from@vitest/browser/index.js.Why: Browser code must use
index.js(safe). Node.js code (CLI, config loading) needs additional exports likedefineBrowserProvider,defineBrowserCommand, etc. The conditional export inpackage.json("node": "./dist/index-node.js") routes Node.js to this file automatically. The"browser"condition is placed BEFORE"node"because vitest passes custom--conditions browserto worker processes (e.g., Nuxt edge/cloudflare presets). Without this ordering, Node.js would match"node"first, loadingindex-node.jswhich importsws, but with--conditions browser,wsresolves to its browser stub that doesn't exportWebSocketServer, causing a SyntaxError.16.
patchModuleAugmentations()— TypeScript Module Augmentation Fix (line 2100)What: Rewrites TypeScript module augmentation declarations in
dist/chunks/global.d.*.d.tsand addsBrowserCommandsre-export todist/@vitest/browser/context.d.ts.Why: TypeScript module augmentations use bare specifiers like
declare module "@vitest/expect". Since@vitest/expectis bundled insidedist/@vitest/expect/, the bare specifier doesn't resolve. The patch rewrites augmentations to use relative paths (declare module "../@vitest/expect/index.js") and merges the augmented interfaces directly into the target.d.tsfiles so TypeScript can find them.17.
patchChaiTypeReference()— Chai Type Resolution (line 2300)What: Adds
/// <reference types="chai" />todist/@vitest/expect/index.d.ts.Why:
@vitest/expecttypes use the Chai namespace (e.g.,Chai.ChaiPlugin) without explicit reference. When@vitest/expectis bundled inside the package, TypeScript can't automatically discover@types/chai. Without the reference directive, chai-specific features like the.notproperty are missing from type completions.18.
patchMockerHoistedModule()— Mock API Source Recognition (line 2331)What: Patches the
hoistMocksequality check indist/@vitest/mocker/node.js(or its chunk).Why: Vitest's mocker checks if
viis imported from'vitest'to validate mock API usage. When users import from'vite-plus/test'or'@voidzero-dev/vite-plus-test', the check fails and throws "There are some problems in resolving the mocks API". The patch adds these package names as valid sources.Before:
After:
19.
patchServerDepsInline()— Module Identity for expect.extend() (line 2387)What: Patches
dist/chunks/cli-api.*.jsto auto-inline specific third-party packages in theModuleRunnerTransformplugin'sconfigResolvedhandler.Why: Libraries like
@testing-library/jest-dom,@storybook/test, andjest-extendedcallexpect.extend()internally to add custom matchers. Under npm/pnpm override, these libraries create separate module instances ofchai— one from the library's ownnode_modulesand one from vite-plus's bundled version. Whenexpect.extend()registers matchers on the library'schaiinstance, tests using vite-plus'schaiinstance don't see the matchers. Auto-inlining forces these libraries through Vite's module runner, which resolves them to the samechaiinstance.Auto-inlined packages:
@testing-library/jest-dom@storybook/testjest-extendedThe patch uses
createRequire().resolve()to check if each package is installed before adding it, so uninstalled packages are silently skipped.20.
createPluginExports()— pnpm Override Shims (line 2455)What: Creates
dist/plugins/*.mjsshim files, one for each@vitest/*package and subpath.Why: pnpm overrides redirect
@vitest/runner→@voidzero-dev/vite-plus-test. But the package's main export is the vitest entry, not@vitest/runner. These shim files provide dedicated export paths (./plugins/runner) that re-export from the correct copied file (../@vitest/runner/index.js).21.
mergePackageJson()— Package Metadata Assembly (line 250)What: Merges upstream vitest's
package.jsonfields (exports, peerDependencies, engines, etc.) into the vite-plus-testpackage.json. Adds conditional Node.js/browser exports, browser-provider exports, plugin exports, and records the bundled vitest version.Why: The published package needs to expose all of vitest's export paths so consumers can import any
vitest/*subpath. It also removes bundled browser providers frompeerDependencies(users don't need to install them separately) and adds custom export paths for vite-plus-specific usage patterns.Key decisions in this function:
"browser"condition placed BEFORE"node"in exports to handle Nuxt edge/cloudflare--conditions browser(see issuevp testfails with ws CJS named export error whennitro.presetiscloudflare_module#831)@vitest/browser-playwright,@vitest/browser-webdriverio,@vitest/browser-previewremoved from peerDependencies (now bundled)22.
validateExternalDeps()— Build Integrity Check (line 2495)What: Scans all
dist/**/*.{js,mjs,cjs}files with oxc-parser, extracts every external import specifier, and verifies each is declared independenciesorpeerDependencies.Why: After all the copying, bundling, and rewriting, an undeclared external dependency would silently fail at runtime in consumer projects. This validation catches any dependency that was missed — whether from a vitest upgrade introducing a new dep, or a rewriting bug leaving a bare specifier intact.
TODO: Patches That Could Be Contributed Back to Upstream Vitest
The patches above fall into two categories: vite-plus-specific (namespace rewriting, bundling mechanics) and genuine upstream improvements that benefit all vitest users. Below is the analysis.
Clearly Upstreamable
TODO 1 Exclude native bindings from browser optimizer (
patchVitestBrowserPackage)What: Add
lightningcss,@tailwindcss/oxide, andtailwindcssto the browseroptimizeDeps.excludelist in@vitest/browser.Why upstream needs this: Upstream vitest's exclude list (in
@vitest/browser/dist/index.js) does NOT include these packages. When users run vitest browser mode in a project using Tailwind CSS v4 or Lightning CSS, Vite's dependency optimizer tries to pre-bundle these native-binding packages, which crashes because native.nodebinaries can't be optimized. This affects all vitest browser mode users with these dependencies, not just vite-plus.Evidence: Upstream exclude list only has:
vitest,vitest/browser,@vitest/*,std-env,tinybench,tinyspy,tinyrainbow,pathe,msw,msw/browser. No native binding packages.Contribution form: PR to
packages/browser/src/node/plugin.tsin vitest-dev/vitest adding these to the exclude array.TODO 2: Make mocker
hoistMockssource check extensible (patchMockerHoistedModule)What: The
hoistMocksfunction in@vitest/mockerhardcodeshoistedModule = "vitest"and checksif (hoistedModule === source). This means any wrapper package that re-exports vitest'sviobject (not just vite-plus, but any custom test harness) will fail with "There are some problems in resolving the mocks API."Why upstream needs this: The hardcoded check prevents the vitest ecosystem from building wrapper packages. The
hoistMocksoptions already accepthoistedModuleas a parameter, but the default is hardcoded and there's no mechanism for wrapper packages to register alternative source names. A more flexible approach (e.g., accepting an array of valid source names, or a callback) would enable the ecosystem.Evidence:
@vitest/mocker/dist/chunk-hoistMocks.jsline 368:hoistedModule = "vitest"with strict equality on line 384.Contribution form: PR to
@vitest/mockerto accepthoistedModuleasstring | string[], or add ahoistedModulesarray option. This is a non-breaking change since the existing string option still works.TODO 3: Auto-inline packages that use
expect.extend()(patchServerDepsInline)What: Automatically add
@testing-library/jest-dom,@storybook/test, andjest-extendedtoserver.deps.inlinewhen they're installed.Why upstream needs this: The module identity problem with
expect.extend()affects any vitest user using npm/pnpm overrides (not just vite-plus). When these libraries callrequire('vitest').expect.extend(matchers), the override mechanism can create a separate module instance, so matchers register on a differentchaiinstance than the test runner uses. This manifests as "expect(...).toBeInTheDocument is not a function" — a common vitest issue. Auto-inlining ensures these libraries share the same module instance.Evidence: The issue is well-documented in vite-plus issue #897, but the root cause (module identity under overrides) is a general vitest concern.
Contribution form: PR to vitest's
ModuleRunnerTransformplugin (configResolvedhandler) to auto-detect and inline knownexpect.extend()packages. Include atest.server.deps.autoInlineconfig option to control this behavior.TODO 4: Export condition ordering for
--conditions browser(mergePackageJson)What: The
"browser"export condition must come before"node"in vitest's package.json exports.Why upstream needs this: When frameworks like Nuxt set edge/cloudflare presets, vitest passes
--conditions browserto worker processes. Node.js resolvers check conditions in order — if"node"comes before"browser", Node.js matches"node"first, loads the Node.js entry which importsws, but with--conditions browseractive,wsresolves to its browser stub (ws/browser.js) that doesn't exportWebSocketServer, causing aSyntaxError. This affects all vitest users with Nuxt edge/cloudflare presets.Evidence: Documented in vite-plus issue #831. Upstream vitest's package.json doesn't have separate browser/node entries for the main export (it uses a single
"import"field), so upstream may not hit this today — but if vitest ever adds conditional exports (which is a growing pattern), the ordering matters.Contribution form: If/when vitest adds conditional exports, ensure
"browser"precedes"node". Could also be contributed as documentation or a test case.Potentially Upstreamable (Design Improvements)
TODO 5: Browser-safe module-runner stub (
createModuleRunnerStub)What: Provide an official browser-safe stub for
vite/module-runnerthat exports placeholder classes instead of Node.js-only code.Why upstream could benefit: The real
vite/module-runnercontainsprocess.platform,Buffer, andfsimports that crash browsers. Any vitest browser mode code that transitively importsvite/module-runnerwill hang. Currently upstream vitest avoids this through careful import structure, but it's fragile — any refactoring that accidentally pulls in module-runner will break browser mode silently.Contribution form: PR to vite itself to provide a
"browser"export condition forvite/module-runnerthat exports safe stubs, similar to howwsprovidesws/browser.js.TODO 6: Browser provider locators should not import
server(patchBrowserProviderLocators)What: The locator files in
@vitest/browser-playwright,@vitest/browser-webdriverio, and@vitest/browser-previewimport{ page, server }fromvitest/browser. In browser context,serveris unnecessary — the config is available viawindow.__vitest_worker__.config.Why upstream could benefit: Importing
serverfrom the main browser index pulls in Node.js server code into the browser module graph. While upstream vitest avoids issues through its module resolution, this coupling is architecturally fragile. If the locator files usedwindow.__vitest_worker__.config.browser.locatorsinstead ofserver.config.browser.locators, it would be cleaner and more resilient.Evidence:
@vitest/browser-playwright/dist/locators.jsline 2:import { page, server } from 'vitest/browser';Contribution form: PR to
@vitest/browser-*locator implementations to usewindow.__vitest_worker__.configinstead of theserverimport for configuration access.NOT Upstreamable (Vite-Plus-Specific)
These patches exist solely because of vite-plus's bundling strategy (embedding vitest inside a different package namespace) and have no upstream equivalent:
bundleVitest()vite→@voidzero-dev/vite-plus-core(namespace change)brandVitest()copyVitestPackages()convertTabsToSpaces()collectLeafDependencies()bundleLeafDeps()rewriteVitestImports()patchVendorPaths()patchVitestCoreResolver()patchVitestPkgRootPaths()patchVitestBrowserPackage()(vendor-aliases)createBrowserCompatShim()createNodeEntry()patchModuleAugmentations()patchChaiTypeReference()/// <reference types="chai" />lost during bundlingcreatePluginExports()mergePackageJson()validateExternalDeps()Key Design Decisions
Browser/Node.js separation is the primary architectural constraint. The entire hybrid strategy exists because Rolldown's chunk-splitting would mix browser and Node.js code, causing runtime crashes in browser test mode.
Import rewriting is pervasive. Every import to
vite,@vitest/*, and leaf dependencies is rewritten to point to the correct location within thedist/tree. This is handled byrewriteVitestImports()usingoxc-parserfor accurate AST-based transformations.vendor-aliasesplugin is injected at runtime. Since imports are rewritten to relative paths withindist/, a Vite plugin (vendor-aliases) is injected into@vitest/browserto resolve@vitest/*andvitest/*imports correctly when Vite processes them during browser tests.Build-time validation catches drift.
validateExternalDeps()scans all bundled files and verifies every external import is declared independenciesorpeerDependencies, preventing silent runtime failures when upgrading Vitest.