vitejs/vite
github.com/vitejs/vite ↗ · 2718 files · 5.6 MB
Vite ⚡ Next Generation Frontend Tooling - 💡 Instant Server Start - ⚡️ Lightning Fast HMR - 🛠️ Rich Features - 📦 Optimized Build - 🔩 Universal Plugin Interface - 🔑 Fully Typed APIs Vite (French word for "quick", pronounced /viːt/, like "veet") is a build t
Overview
This is the Vite monorepo (`@vitejs/vite-monorepo`) at v8.3.0 — a pnpm workspace shipping three published packages: `vite` (the native-ESM dev server + Rolldown-powered bundler), `create-vite` (the scaffolding CLI), and `@vitejs/plugin-legacy` (SystemJS/Babel downlevel output for old browsers). The `vite` package is the bulk of it: a `cac`-driven CLI over four commands (dev/build/optimize/preview), a `connect` middleware stack that transforms modules on demand and pushes HMR over an authenticated WebSocket, and a Rollup-compatible plugin container that now runs on Rolldown/Oxc rather than Rollup+esbuild. What's distinctive at this commit is the Environment API — dev and build are both fanned out across named environments (`client`, `ssr`, custom) each with its own module graph, plugin container and optimizer — plus an experimental `bundledDev` mode that swaps the per-module transform pipeline for a lazy Rolldown dev bundle served from memory. Correctness is enforced almost entirely by a 83-playground / 109-spec Playwright+Vitest integration suite that runs every fixture in serve, bundled-dev, and build modes.
System map
Three publishable packages under `packages/`, a 83-directory `playground/` that doubles as the integration test corpus, and a VitePress `docs/` site deployed to Netlify. The `vite` package's runtime splits four ways: `src/node` (server, build, config, plugins — Node only), `src/client` (browser HMR client and error overlay), `src/module-runner` (SSR/runtime module evaluation), and `src/shared` (code compiled into both sides). All bundling — of Vite itself, of user configs, of dep pre-bundles, and of production output — goes through Rolldown; Rollup remains only as a type source and devDependency. There is no database or persistent store; the only on-disk state is the dep-optimizer cache under `cacheDir` and build output under `outDir`.
- The published CLI entry is `packages/vite/bin/vite.js`, a thin launcher that enables source maps, installs an `unhandledRejection` trap, parses `-d/--debug`, `-f/--filter` and `--profile` out of `process.argv` *before* importing `../dist/node/cli.js`, and calls `module.enableCompileCache()` with a 10s `flushCompileCache` timer.
- `packages/vite/src/node/cli.ts` registers exactly four `cac` commands: the default `[root]` (aliased `serve` and `dev`), `build [root]`, `optimize [root]` (marked deprecated in its own description), and `preview [root]`; each dynamically `import()`s its implementation module so the dev path never loads `build.ts`.
- The package's public API surface is `packages/vite/src/node/index.ts`, which re-exports `createServer`, `build`, `createBuilder`, `preview`, `optimizeDeps`, `defineConfig`/`resolveConfig`/`loadConfigFromFile`, `runnerImport`, `fetchModule`, `createServerHotChannel`, `transformWithOxc`, `preprocessCSS`, and `parseAst`/`parseAstAsync` aliased from Rolldown.
- `package.json` `exports` publishes four entry points — `.` → `dist/node/index.js`, `./module-runner` → `dist/node/module-runner.js`, `./internal` → `dist/node/internal.js`, and raw `./dist/client/*` — with `./types/internal/*` explicitly nulled to keep internal types unreachable.
- Vite builds itself with Rolldown, not Rollup: `rolldown.config.ts` emits separate browser bundles for `src/client/env.ts` → `dist/client/env.mjs`, `src/client/clientEntry.ts` → `dist/client/client.mjs`, and `src/client/bundledDevClient.ts` → `dist/client/bundledDevClient.mjs`, plus a Node ESM bundle with `node/[name].js` entries and `node/chunks/` shared chunks.
- The dev server is a `connect()` app, not a framework: `_createServer` builds `middlewares = connect()`, wraps it in `resolveHttpServer`, attaches `createWebSocketServer`, and creates a `chokidar` watcher over root + `config.configFileDependencies` + env files + `publicDir`.
- Runtime state is owned per-environment, not globally: `_createServer` iterates `config.environments` calling `environmentOptions.dev.createEnvironment(name, config, { ws })` and `environment.init({ watcher, previousInstance })`, so each of `client`/`ssr`/custom gets its own `DevEnvironment` with its own `moduleGraph` and `pluginContainer`.
- The legacy flat `server.moduleGraph` / `server.pluginContainer` / `server.transformRequest` APIs still exist but are getters that call `warnFutureDeprecation(config, 'removeServerModuleGraph')` etc. and delegate to the client environment via a `ModuleGraph` facade in `mixedModuleGraph.ts`.
- Production builds go through `createBuilder` → `builder.buildApp()` → per-environment `buildEnvironment()`, which calls `resolveRolldownOptions(environment, chunkMetadataMap)` then `rolldown(...)` and `bundle.write|generate`; `--watch` instead returns a Rolldown `watch()` watcher wired to BUNDLE_START/BUNDLE_END/ERROR events.
- The pnpm workspace spans `packages/*`, `playground/**`, nested `packages/**/__tests__/**`, and `docs`, with `overrides: { vite: 'workspace:*' }` so every playground resolves the local build, and patched deps for `sirv@3.0.2`, `chokidar@3.6.0` and `dotenv-expand@13.0.0`.
- Build/test commands are all at the root: `pnpm build` (`pnpm -r --filter='./packages/*' run build`), `pnpm test` = `test-unit && test-serve && test-build`, plus `test-serve-bundled` which sets `VITE_TEST_BUNDLED_DEV=1`, and `ci-publish` (`tsx scripts/publishCI.ts`).
- Docs are a separate VitePress app (`@vitejs/monorepo-docs`) with a 592-line `docs/.vitepress/config.ts`, deployed by Netlify via `command = "pnpm ci-docs"` publishing `docs/.vitepress/dist`, gated by `scripts/docs-check.sh` as the ignore predicate.
Feature catalog
Vite's user-facing surface is four CLI commands, a programmatic JS API, a Rollup-superset plugin hook system, and a browser-side HMR runtime with an error overlay. Around that sit optional subsystems: dependency pre-bundling, CSS preprocessing and modules, worker/WASM/asset handling, `import.meta.glob`, SSR module running, and the legacy-browser plugin. `create-vite` is an independent interactive scaffolder with eleven framework families. The most recently added features are the multi-environment builder (`--app` / `builder: {}`), console forwarding from browser to terminal, and `--experimentalBundle` bundled dev.
- `vite dev` accepts `--host`, `--port`, `--open [path]`, `--cors`, `--strictPort`, `--force` (bust the optimizer cache) and `--experimentalBundle`, then prints URLs and installs CLI shortcuts, adding a `p` shortcut to toggle the CPU profiler when launched under `--profile`.
- `vite build` exposes `--target`, `--outDir`, `--assetsDir`, `--assetsInlineLimit`, `--ssr [entry]`, `--sourcemap [inline|hidden]`, `--minify [oxc|terser|esbuild]` (default `oxc`), `--manifest`, `--ssrManifest`, `--emptyOutDir`, `-w/--watch`, and `--app` which is sugar for `builder: {}`.
- Interactive terminal shortcuts during dev are `r` restart server, `u` show server url, `o` open in browser, `c` clear console, `q` quit; preview mode registers only `o` and `q`.
- The dev server exposes built-in HTTP endpoints beyond module serving: `/__open-in-editor` (via `launch-editor-middleware`), a `viteHMRPingMiddleware` answering `accept: text/x-vite-ping` with 204, and `/@fs/` raw filesystem serving guarded by the allow-list.
- A dev proxy is available via `server.proxy`, backed by `http-proxy-3`, supporting `rewrite`, `configure`, webpack-dev-server-style `bypass`, WebSocket proxying, and `rewriteWsOrigin` to rewrite the Origin header on WS upgrades.
- The browser client ships an error overlay implemented as the custom element `vite-error-overlay` (`class ErrorOverlay extends HTMLElement`), gated by the injected `__HMR_ENABLE_OVERLAY__` define and creatable/clearable from `handleMessage`.
- Dependency pre-bundling is a full subsystem: `scanImports` computes entries and runs a Rolldown scanner, `runOptimizeDeps` writes a `_metadata.json` cache under `getDepsCacheDir(environment)`, and `loadCachedDepOptimizationMetadata` / `cleanupDepsCacheStaleDirs` manage reuse and GC across runs.
- The built-in plugin pipeline provides asset handling, CSS (`cssPlugin`/`cssPostPlugin`/`cssAnalysisPlugin`), HTML (`htmlInlineProxyPlugin`/`buildHtmlPlugin`), web workers (`webWorkerPlugin`, `workerImportMetaUrlPlugin`), WASM (`wasmHelperPlugin`), `define`, `import.meta.glob` (`importGlobPlugin`), dynamic-import variables, and module-preload polyfill.
- Browser console forwarding to the terminal is opt-in via `server.forwardConsole.enabled`, which conditionally inserts `forwardConsolePlugin({ environments: ['client'] })` into the pipeline and is picked up client-side through the `__SERVER_FORWARD_CONSOLE__` define.
- SSR is served by `fetchModule(environment, url, importer)`, which short-circuits `data:` URLs and Node builtins to `{externalize, type:'builtin'}`, external URLs to `type:'network'`, and otherwise resolves bare specifiers through `tryNodeResolve` with `externalConditions`.
- `vite/module-runner` is a separately published runtime: `class ModuleRunner` with `evaluatedModules`, a pluggable `transport` that `invoke('fetchModule', ...)`s the server, optional HMR via `createHMRHandlerForRunner`, and `cachedRequest`/`directRequest` for circular-dependency-aware evaluation.
- `create-vite` v9.2.1 is an interactive prompt-driven scaffolder over a `FRAMEWORKS` table covering vanilla, vue, react, preact, lit, svelte, solid and qwik, each with TS/JS variants plus links to external starters (Nuxt, Vike, Official Vue Starter), materialised from the sibling `template-*` directories.
Data flow
Three input boundaries feed the system: the config file (loaded by bundling, runner-import, or native import), `.env` files filtered by prefix, and HTTP/WS requests from the browser. Config flows into a single frozen `ResolvedConfig` that fans out into per-environment configs; each `DevEnvironment` then owns a `EnvironmentModuleGraph` as the authoritative in-memory state. A request's data path is URL → plugin container `resolveId`→`load`→`transform` → optional `ssrTransform` → cached `TransformResult` (code + map + etag + deps) stored on the module node and written back out through `send()`. The reverse direction — file change → HMR payload → WebSocket → browser — is the only push channel. In build mode the same plugin container is driven by Rolldown instead of by HTTP requests, and the terminal output boundary is `outDir` plus optional `manifest.json`/`ssr-manifest.json`.
- Config entry: `resolveConfig` merges inline config over the file config from `loadConfigFromFile`, then re-derives `mode`, filters plugins by `apply`, runs `config` hooks via `runConfigHook`, and force-injects `client` (and usually `ssr`) into `config.environments` before per-environment resolution.
- `loadConfigFromFile` picks one of three loaders selected by `--configLoader`: `bundleAndLoadConfigFile` (default — Rolldown-bundles the config then evaluates it), `runnerImportConfigFile` (uses Vite's own module runner), or `nativeImportConfigFile` (raw `import()` of a `file://` URL with a `?t=` cache-buster, or `fresh-import`).
- Env vars enter through `loadEnv(mode, envDir, prefixes = 'VITE_')`, which reads `.env`, `.env.local`, `.env.<mode>`, `.env.<mode>.local` (accepting FIFOs so 1Password-style pipes work), parses with `node:util` `parseEnv`, then expands with `dotenv-expand` against a *copy* of `process.env` so expansion doesn't mutate the global.
- `transformRequest(environment, url, options)` dedupes concurrent work through `environment._pendingRequests` keyed by timestamp-stripped URL, and records a `monotonicDateNow()` timestamp up front so a transform that races an invalidation is discarded rather than cached stale.
- Inside `loadAndTransform` the order is `pluginContainer.load(id)` → fall back to reading from disk → `pluginContainer.transform(code, id, ...)` → `ssrTransform(code, normalizedMap, url, originalCode, ...)` when the environment needs module-runner output.
- The transform result carries `{ code, map, ssr?, etag?, deps?, dynamicDeps? }`; the etag is what `cachedTransformMiddleware` matches against `if-none-match` via `moduleGraph.getModuleByEtag` to short-circuit with a 304.
- `importAnalysisPlugin` is the rewrite stage that converts every import specifier to a server-resolvable URL via its internal `normalizeUrl`, records importer/imported edges on the module graph, and computes `isSelfAccepting` from `import.meta.hot.accept` calls — the data the HMR propagation algorithm later consumes.
- HMR output is one of two payload shapes computed in `updateModules`: a `{type:'full-reload', triggeredBy, path}` message, or a `{type:'update', updates:[...]}` array whose entries carry `path`, `acceptedPath`, `explicitImportRequired`, `isWithinCircularImport` and `firstInvalidatedBy`.
- Client-side the payload arrives over a `normalizeModuleRunnerTransport`-wrapped WebSocket opened to `${protocol}://${socketHost}?token=${wsToken}` with subprotocol `vite-hmr`, and is dispatched by `handleMessage` into `hmrClient.queueUpdate(update)`.
- Static assets bypass the transform pipeline entirely: `servePublicMiddleware` is registered *before* `transformMiddleware` specifically so `publicDir` files are served as-is, and `serveRawFsMiddleware` handles `/@fs/`-prefixed absolute paths by slicing `FS_PREFIX` off the pathname.
- In bundled-dev mode the data path inverts: Rolldown produces output files that `BundledDev` stores as lazily-evaluated entries in `MemoryFiles` (including `BUNDLED_DEV_CLIENT_FILENAME`), and `memoryFilesMiddleware` serves from that map instead of from the module graph.
- Build-mode data flow is fanned out by environment: `createBuilder` re-runs `resolveConfigToBuild` once per environment name (unless `configBuilder.sharedConfigBuild`) with a `patchPlugins` pass that swaps in shared plugin instances for those marked `sharedDuringBuild`.
Behavior traces
Four flows are worth tracing: dev-server startup, a module request, a file-change HMR round trip, and a production build. Startup is notable for deferring `buildStart` and optimizer init until the HTTP `listen` call is intercepted. The module request path is a long but linear middleware chain. HMR distinguishes config/env changes (full server restart) from module changes (graph propagation). The build path is a per-environment loop over Rolldown.
- Dev startup: `bin/vite.js` → `dist/node/cli.js` → the default cac action → `createServer()` → `_createServer` → `disableCache()` → `resolveConfig(inlineConfig,'serve')` → `resolveHttpServer` → `createWebSocketServer` → chokidar watcher → per-environment `createEnvironment`/`init` → middleware registration → `server.listen()`.
- Startup defers expensive work by monkey-patching the HTTP server: `httpServer.listen` is replaced with a wrapper that awaits `initServer(true)` first and re-emits any error as an `'error'` event, so `buildStart` and `environment.listen(server)` run exactly once before the socket binds.
- In middleware mode there is no HTTP server at all — `httpServer` is `null`, `resolveHttpServer` is skipped, and `initServer(false)` is awaited inline at the end of `_createServer`.
- The middleware chain is registered in a fixed order: `timeMiddleware` (only when `process.env.DEBUG`) → `rejectInvalidRequestMiddleware` → `corsMiddleware` → `hostValidationMiddleware` → user `configureServer` hooks → `cachedTransformMiddleware` → `proxyMiddleware` → `baseMiddleware` → `/__open-in-editor` → HMR ping → `servePublicMiddleware` → `transformMiddleware` → `serveRawFsMiddleware` → `serveStaticMiddleware` → `htmlFallbackMiddleware` → post hooks → `indexHtmlMiddleware` → `notFoundMiddleware` → `errorMiddleware`.
- `configureServer` hooks run in two phases around the internal middlewares: the hook's synchronous body runs before `cachedTransformMiddleware` is added, while any function it *returns* is pushed to `postHooks` and invoked after the static/HTML-fallback middlewares but before `indexHtmlMiddleware`, which is what lets user middleware pre-empt `index.html`.
- A module request runs `transformMiddleware` → `environment.transformRequest(url)` → `transformRequest` (pending-request dedupe) → `doTransform` (`pluginContainer.resolveId`) → `loadAndTransform` (`pluginContainer.load`, then `pluginContainer.transform`) → optional `ssrTransform` → `send()` with the etag.
- `EnvironmentPluginContainer.resolveId` lazily triggers `this.buildStart()` and awaits `_buildStartPromise` on the first resolution, so for non-client environments the `buildStart` hook fires on first request rather than at server boot.
- File change → `watcher.on('change')` → `onFileChange` → `handleHMRUpdate(type, file, server)`, which first checks whether the file is `config.configFile`, a member of `config.configFileDependencies`, or an env file for the current mode — any of those triggers `restartServerWithUrls(server)` instead of an HMR update.
- `handleHMRUpdate` snapshots `server.environments` and defines `isStale()` comparing identity, because a `hotUpdate`/`handleHotUpdate` plugin hook may restart the server mid-transaction and invalidate the whole HMR pass.
- Edits to Vite's own client directory can't be hot-updated: if the changed file starts with `normalizedClientDir`, every environment immediately gets `hot.send({ type:'full-reload', path:'*' })` and the function returns.
- `updateModules` runs `propagateUpdate(mod, traversedModules, boundaries)` per changed module, invalidates via `moduleGraph.invalidateModule`, and escalates to full reload on any dead end, on an empty `modules` array, on `circular import invalidate`, or when a client-environment `.html` file changes and none of its modules are of type `js`.
- Production build: `cli.ts` build action → `createBuilder(inlineConfig, null)` → `builder.buildApp()` → sorted `buildApp` plugin hooks with `configBuilder.buildApp(builder)` interleaved before `order:'post'` hooks → fallback loop calling `builder.build(environment)` for any environment still `!isBuilt` → `buildEnvironment` → `rolldown()` → `bundle.write()` → `builder.runDevTools()`.
Testing signals
Testing is Vitest-only, split into two configs. `vitest.config.ts` collects 68 unit specs from `**/__tests__/**/*.spec.[tj]s` inside `packages/`, running with `isolate: false` and a 20s timeout. `vitest.config.e2e.ts` collects 109 integration specs from `playground/**`, driving a real Playwright Chromium browser against real Vite servers. The integration suite is the primary correctness net and runs three times in CI — serve, bundled-dev, and build — across Node 20/22/24/26 on Ubuntu plus macOS and Windows. The weakest spot is that unit coverage of the largest files is thin relative to their size: `css.ts` (3685 lines) and `hmr.ts` (1193 lines) have no dedicated unit spec, and are covered only indirectly through playground fixtures.
- Unit tests use `vitest.config.ts` with `include: ['**/__tests__/**/*.spec.[tj]s']`, explicitly excluding `./playground/**` and `./playground-temp/**`, `testTimeout: 20000`, `isolate: false`, and an alias mapping `vite/module-runner` to the TypeScript source rather than `dist`.
- There are 68 unit spec files under `packages/**/__tests__/`, concentrated in `packages/vite/src/node/__tests__/` (config, build, dev, env, html, http, resolve, scan, utils, oxc, packages, shortcuts, external, environment, filterRegex, assetSource) plus `plugins/`, `optimizer/` and `utils/` subdirectories.
- Integration tests use `vitest.config.e2e.ts` with `include: ['./playground/**/*.spec.[tj]s']`, `globalSetup: playground/vitestGlobalSetup.ts`, `setupFiles: playground/vitestSetup.ts`, `reporters: 'dot'`, and a timeout of 50s on CI / 30s locally (Infinity under `PWDEBUG`).
- `vitestGlobalSetup.ts` launches a shared `chromium.launchServer()` (headed when `VITE_DEBUG_SERVE` is set, `--no-sandbox` on CI), shares the endpoint via `project.provide('wsEndpoint', ...)`, then copies only the playground directories actually referenced by the selected test files into `playground-temp/`.
- `vitestSetup.ts` exposes the per-test harness — `page`, `viteTestUrl`, `browserLogs`, `browserErrors`, `serverLogs`, and mode flags `isBuild`/`isServe`/`isBundledDev`/`isBundled` — aliased in tests as `~utils`.
- The same 109 playground specs are executed under three different modes selected by env vars: default serve, `VITE_TEST_BUNDLED_DEV=1` (`test-serve-bundled`), and `VITE_TEST_BUILD=1` (`test-build`), with `NODE_ENV` set to `production` only in the build run.
- Known gaps in bundled-dev mode are tracked as an explicit 22-entry `bundledDevExclude` array in `vitest.config.e2e.ts` — including `fs-serve`, `csp`, `hmr-ssr`, `optimize-deps`, `tailwind` and all four `worker` variants — with the convention that partial failures use `test.skipIf(isBundledDev)` instead of listing the file.
- CI runs the full matrix Node 20/22/24/26 on ubuntu-latest plus macos-latest@24 and windows-latest@24.15.0 (pinned to work around nodejs/node#63638), with `fail-fast: false` and a 20-minute timeout per job.
- The CI `test` job runs all four suites sequentially — `test-unit`, `test-serve`, `test-serve-bundled`, `test-build` — after a `pnpm run build`, and a separate `lint` job runs eslint, `pnpm format && git diff --exit-code`, `pnpm typecheck`, and `pnpm test-docs`.
- CI skips the whole test matrix when only docs, `.github/**` (excluding `ci.yml`), `packages/create-vite/template**`, or `**.md` changed, via a `changed` job using `tj-actions/changed-files` with `only_changed`.
- Flakiness is managed with environment knobs rather than retries in test code: `VITEST_SEGFAULT_RETRY: 3` and `NODE_OPTIONS: --max-old-space-size=6144` are set globally in the workflow env block.
- A scheduled Semgrep scan (`--config auto`, weekly on Tuesdays) uploads SARIF to GitHub code scanning, but explicitly excludes `playground` and `__tests__` and disables the `detect-non-literal-regexp` rule — so dynamic regex construction in source is not flagged.
Risk map
Vite's security model rests on four independent guards — the `server.fs` allow/deny list, WebSocket token authentication, host validation, and CORS origin restriction — and a bypass in any one of them exposes the developer's filesystem or dev server to a hostile web page. The code shows scar tissue from past CVEs: Windows 8.3 short-name detection, NTFS ADS colon rejection, `#`-in-URL rejection, and trailing-slash normalisation are all defensive patches with explanatory comments. Beyond security, the main structural risks are the very large monolithic modules (`css.ts` at 3685 lines, `config.ts` at 2961), the two-implementation split between classic dev and `bundledDev`, and a large surface of deprecated-but-live compatibility shims.
- `isFileLoadingAllowed` is the single chokepoint for filesystem access and carries three separate bypass mitigations: Windows 8.3 short-name paths (`PROGRA~1`, matched by `windowsShortNameSegmentRE`) are rejected outright, any `:` outside a drive letter is rejected as a possible NTFS alternate data stream, and trailing slashes are stripped before the deny check because `fs.readFile('/foo.png/')` resolves to `/foo.png`.
- `server.fs.deny` defaults to only six patterns — `.env`, `.env.*`, `*.{crt,pem,key,p12,pfx,cer,der}`, `.npmrc`, `.yarnrc.yml`, `**/.git/**` — so any other secret-bearing file inside an allowed root (e.g. `credentials.json`, `.aws/`, `id_rsa` without extension) is servable when `fs.strict` passes.
- `config.safeModulePaths` is an escape hatch checked *before* the `fs.allow` list in `isFileLoadingAllowed`, and it is populated at runtime by `DevEnvironment._registerInputsAsSafeModules()` — meaning build inputs silently widen the serving allow-list.
- `rejectInvalidRequestMiddleware` exists specifically because Node accepts `#` in request targets while the `server.fs.deny` checks assume it is absent — a 400 is returned for any URL containing `#`, and this middleware is the first in the chain.
- WebSocket auth uses a per-process 72-bit token compared with `crypto.timingSafeEqual`, but the token travels as a URL query parameter (`?token=...`), which the comment acknowledges may land in server/middleware logs.
- `config.legacy.skipWebSocketTokenCheck` is a live opt-out that disables token validation entirely inside `shouldHandle`, re-opening cross-site WebSocket hijacking for anyone who sets it.
- Host validation is skipped whenever `server.https` is configured (`if (allowedHosts !== true && !serverConfig.https)`), on the reasoning that HTTPS is not DNS-rebinding-vulnerable — so a misissued or self-signed HTTPS dev setup loses that guard.
- The WS upgrade path allows *any* non-browser client without a token: `shouldHandle` returns `true` when `req.headers.origin` is absent, with an inline comment justifying it for backward compat — meaning any local process that can open a socket can join the HMR channel.
- Shell-out surface exists in the dev server itself: `server/index.ts` imports `execSync` from `node:child_process`, and `openBrowser.ts` spawns a browser process honouring the `BROWSER`/`BROWSER_ARGS` environment variables which `loadEnv` will populate from a project's `.env` file.
- Config loading executes arbitrary project code by design through three paths (`bundleAndLoadConfigFile`, `runnerImportConfigFile`, `nativeImportConfigFile`), and `bundleConfigFile`/`loadConfigFromBundledFile` evaluate the bundled result — so opening an untrusted repo and running `vite` is code execution.
- Blast-radius hotspots by size: `plugins/css.ts` (3685 lines), `config.ts` (2961), `utils.ts` (2022), `build.ts` (1986), `plugins/html.ts` (1753), `server/index.ts` (1496), `server/pluginContainer.ts` (1332) — none of which has a same-named unit spec except `config.spec.ts`, `build.spec.ts` and `html.spec.ts`.
- The dev server now has two divergent implementations gated on one boolean: `config.experimental.bundledDev` swaps the entire serving strategy, skips `cachedTransformMiddleware`, skips `buildStart` at init, and short-circuits `handleHMRUpdate` with a `// TODO: support handleHotUpdate / hotUpdate` early return — so plugin HMR hooks silently do nothing in that mode.
Extension points
Vite's primary extension mechanism is the plugin object — a superset of Rolldown/Rollup's, adding `config`, `configEnvironment`, `configureServer`, `configurePreviewServer`, `transformIndexHtml`, `hotUpdate`, `buildApp`, and the environment-scoping fields `applyToEnvironment`/`sharedDuringBuild`. The ordered array returned by `resolvePlugins` is the canonical insertion-point table: user `pre`, `normal` and `post` plugins are spliced at three fixed positions between internal plugins. Beyond plugins, the extension surface includes the environment factory (`dev.createEnvironment`/`build.createEnvironment`), the hot-channel/transport abstraction, the CLI shortcut table, and the `create-vite` template directory convention.
- `resolvePlugins(config, prePlugins, normalPlugins, postPlugins)` is the ordering contract: user `pre` plugins land after the alias plugins, `normal` plugins after asset/CSS/worker handling but before `definePlugin`, and `post` plugins after the glob/dynamic-import plugins — with `clientInjectionsPlugin`, `cssAnalysisPlugin` and `importAnalysisPlugin` always last.
- `getSortedPluginsByHook(hookName, plugins)` implements the per-hook `order: 'pre' | 'post'` sorting with three running indices, and `createPluginHookUtils` memoises the result per hook name into `getSortedPlugins`/`getSortedPluginHooks` on the resolved config.
- `applyToEnvironment(environment)` lets a plugin opt in/out per environment or return a *replacement* plugin — as the built-in alias plugin does, swapping itself for Rolldown's native `nativeAliasPlugin` when `environment.config.isBundled` and no custom resolver is present.
- `perEnvironmentPlugin(name, applyToEnvironment)` is the public helper for building environment-scoped plugins, exported from the package root alongside `perEnvironmentState`.
- `buildApp` is the multi-environment build orchestration hook: `builder.buildApp()` walks `config.getSortedPlugins('buildApp')` and interleaves `configBuilder.buildApp(builder)` just before the first `order: 'post'` hook, letting frameworks control environment build order and interdependencies.
- `hotUpdate` is the environment-aware replacement for `handleHotUpdate`: `getSortedHotUpdatePlugins(environment)` selects the plugins and each hook receives a `HotUpdateOptions` with `{type, file, timestamp, read, server, modules}` and may return a filtered module list.
- Environments are created through configurable factories — `environmentOptions.dev.createEnvironment(name, config, { ws })` at server start and `config.build.createEnvironment(name, config)` in `createBuilder.setupEnvironment` — with `defaultCreateDevEnvironment`/`defaultCreateClientDevEnvironment` as the built-in defaults.
- The HMR transport is pluggable on both ends: server-side via `normalizeHotChannel` and `createServerHotChannel()` (exported publicly), client/runtime-side via `normalizeModuleRunnerTransport` and `createWebSocketModuleRunnerTransport` in `src/shared/moduleRunnerTransport.ts`.
- `bindCLIShortcuts({ print, customShortcuts })` accepts an array of `CLIShortcut<Server>` objects (`{ key, description, action }`) merged with the built-in table — the mechanism the CLI itself uses to add the `p` profiler toggle.
- Plugin hook filtering is an extension point with its own registry: `getCachedFilterForPlugin(plugin, hookName)` builds and WeakMap-caches `createIdFilter` / `createFilterForTransform` matchers from a hook's `filter: { id, code, moduleType }` object.
- `createIdResolver(config, options)` is exported as the supported way for plugins to reuse Vite's resolver, and `config.createResolver(options)` wraps it with per-environment `PartialEnvironment` instances for client and ssr.
- `create-vite` extends purely by directory convention — each entry in the `FRAMEWORKS` variants table maps to a sibling `template-<name>` directory (`template-react-ts`, `template-svelte`, `template-qwik-ts`, …), so adding a template means adding a directory plus a table entry.
Contributor handoff
This is a high-traffic OSS monorepo with strict conventions: pnpm only (enforced by `only-allow` in `preinstall`), oxfmt for formatting, eslint for `packages/*/{src,types}/**/*.ts`, and a `simple-git-hooks` pre-commit running lint-staged. Almost nothing can be verified without first running `pnpm build`, since the playground tests execute the built `dist/node/cli.js`. Read the CLI, server bootstrap, config resolver, and plugin pipeline before touching anything; treat the security guards and the deprecation shims as append-only unless the change is deliberate. The most common trap is editing `packages/vite/src/node` and testing without rebuilding, or adding a runtime dependency to the wrong section of `package.json`.
- Read first, in order: `packages/vite/src/node/cli.ts` (command surface), `packages/vite/src/node/server/index.ts` (`_createServer` + middleware order), `packages/vite/src/node/config.ts` (`resolveConfig`, `loadConfigFromFile`), `packages/vite/src/node/plugins/index.ts` (`resolvePlugins` ordering) and `packages/vite/src/node/build.ts` (`createBuilder`/`buildEnvironment`).
- Setup is `pnpm i` then `pnpm run build` at the repo root; while developing Vite itself, run `pnpm run dev` inside `packages/vite` for a watching Rolldown rebuild (`premove dist && pnpm build-bundle -w`).
- pnpm is mandatory — root `preinstall` runs `npx only-allow pnpm` and `packageManager` pins `pnpm@12.4.2`; npm/yarn installs will be rejected.
- Verification commands: `pnpm test-unit` (fast, no browser), `pnpm test-serve [match]` / `pnpm test-build [match]` for targeted playground runs, `pnpm lint`, `pnpm format` (oxfmt), and `pnpm typecheck` (root `tsc -p scripts` plus every package's own typecheck).
- Playground test matching is per-substring and only works on `test-serve`/`test-build`, not on `pnpm test` — e.g. `pnpm run test-serve assets` runs both `playground/assets` and `playground/assets-sanitize`.
- Trap: playground tests run against a real `vite` binary resolved through `viteBinPath` in `vitestSetup.ts`, so source edits in `packages/vite/src` are invisible to integration tests until `pnpm build` (or a running `pnpm dev` watcher) regenerates `dist`.
- Trap: the e2e global setup copies playgrounds into `playground-temp/` and only copies the directories matched by the selected spec files — a new fixture directory that no spec references will not be copied, and `VITE_PRESERVE_BUILD_ARTIFACTS=1` (via `pnpm debug-build`) is needed to inspect the output.
- Dependency placement is a reviewed decision: `packages/vite/package.json` carries a literal `"//": "READ CONTRIBUTING.md to understand what to put under deps vs. devDeps!"` marker, and CONTRIBUTING has sections on `require()` usage, adding dependencies, and ensuring type support.
- Formatting and linting are enforced on commit: `simple-git-hooks` pre-commit runs `lint-staged --concurrent false`, which applies `oxfmt` to all files and eslint `--fix` to `packages/*/{src,types}/**/*.ts`, `packages/**/*.d.ts` and `playground/**/__tests__/**/*.ts`.
- Safe edit boundary: anything under `playground/` is fixture + spec code, not shipped code — only `packages/vite`, `packages/create-vite` and `packages/plugin-legacy` are published, and CI skips tests entirely for `packages/create-vite/template**` changes.
- Do not relax the filesystem guards casually: `isFileLoadingAllowed`, `looksLikeWindowsShortNamePath`, the NTFS-colon check and `rejectInvalidRequestMiddleware` each carry comments identifying a specific bypass class they exist to block.
- When adding a server-facing API, mirror the environment convention: put state on `DevEnvironment` (which owns `moduleGraph`, `pluginContainer`, `_pendingRequests`, `transformRequest`, `warmupRequest`, `waitForRequestsIdle`) and expose any flat `server.*` alias only through `warnFutureDeprecation`.
Languages
| Language | Files | Share of source |
|---|---|---|
| JavaScript | 979 | 60% |
| TypeScript | 591 | 36% |
| JSON | 386 | docs/config |
| CSS | 205 | docs/config |
| HTML | 156 | docs/config |
| Markdown | 84 | docs/config |
| Sass | 30 | 2% |
| Vue | 27 | 2% |
Start reading here
Probable source entrypoint: packages/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
packages/ (797 files)
Repository structure
| Top-level path | Files |
|---|---|
playground/ | 1756 |
packages/ | 797 |
docs/ | 116 |
.github/ | 24 |
scripts/ | 8 |
patches/ | 3 |
Docs found: CONTRIBUTING.mdREADME.mddocs/acknowledgements.mddocs/blog.mddocs/blog/announcing-vite2.mddocs/blog/announcing-vite3.mddocs/blog/announcing-vite4-3.mddocs/blog/announcing-vite4.mddocs/blog/announcing-vite5-1.mddocs/blog/announcing-vite5.md
Package
package.json — @vitejs/vite-monorepo, 32 declared dependencies.
@e18e/eslint-plugin@eslint/js@type-challenges/utils@types/babel__core@types/babel__preset-env@types/convert-source-map@types/cross-spawn@types/etag@types/less@types/node@types/picomatch@types/stylus
CI & config entrypoints
README.md— Repository readmeplayground/minify/vite.config.js— Vite configplayground/base-conflict/vite.config.ts— Vite configplayground/css-sourcemap/vite.config.js— Vite configplayground/dynamic-import/vite.config.js— Vite configplayground/nested-deps/vite.config.js— Vite configplayground/alias/vite.config.js— Vite configplayground/optimize-deps-no-discovery/vite.config.js— Vite configplayground/backend-integration/vite.config.js— Vite configplayground/hmr-root/vite.config.ts— Vite config
Structural health signals
Average score 7/10 across 96 analyzed source files; 36 hotspots surfaced.
packages/vite/src/node/plugins/importAnalysis.ts— 1159 lines, score 4 (Large file, Deep nesting)packages/vite/src/node/plugins/worker.ts— 871 lines, score 5.2 (Large file, Deep nesting)playground/fs-serve/__tests__/commonTests.ts— 638 lines, score 5.2 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
1544bb1b7ac92026-09-22 — docs(config): correct the server.sourcemapIgnoreList default (#23462)39330f489a0a2026-09-22 — fix(optimizer): don't skip imports whose binding starts with type (#23540)bd97903e326c2026-09-22 — docs(config): document preserveEntrySignatures defaults (#23469)15569ce343b52026-09-22 — docs: fix typo builtin -> built-in in plugins guide (#23525)9abd99bfdd312026-09-21 — refactor: remove duplicate configurations (#23532)