Vite, and sites
This guide shows you how to start every Vite config in a workspace from one base, so a dev server, a test and a build all resolve the workspace the same way, and how a site’s build ships compressed copies that a static host can serve.
@wtf/project-vite/config holds the base Vite config of a workspace, baseConfig(). Every Vite
config starts from it: an app’s vite.config, a Storybook’s viteFinal, and each Vitest config
(the configs of @wtf/project-vitest start from it themselves).
Export of ./config | What it is |
|---|---|
baseConfig | The base config, made of the two layers below: mergeConfig(baseConfig(), yours) |
compress | The plugin that writes brotli and gzip copies in a site’s build; baseConfig adds it unless compress: false |
conditionsOf | The resolve conditions of a run: @wtf/source first, when reading from source |
buildsSite | Whether a run is a site’s build: vite build, with no library (build.lib) and no server (build.ssr) |
CLIENT_CONDITIONS | Vite’s own conditions for browser modules, which come after @wtf/source |
COMPRESSIBLE | The extensions of the files a site’s build compresses: text, and WebAssembly |
THRESHOLD | The smallest file that gets compressed, 1024 bytes |
FROM_DIST | Whether the run reads the packages from their build (TEST_FROM=dist) |
What the base sets
Every command shares this, whether a dev server, a test or a build:
resolve.conditions:@wtf/source, then Vite’s own (module,browser,development|production). A workspace package resolves to its TypeScript through the@wtf/sourcecondition of its export map (SOURCE_CONDITIONof@wtf/project-resolve/condition), with nothing built first. WithTEST_FROM=dist, astest:distandtest:browser:distrun, there is no@wtf/source, and a package resolves the way a consumer resolves it: to its build.
@wtf/project-vitest’s configs add only what a test needs: an alias from each workspace package to
its source, and, in a run from the build, the plugin that loads a package’s own modules from its
dist.
Compress a site
A site’s vite build adds one thing that no dev server, test, library build or server build gets.
Beside each text file it writes, it writes a brotli copy (quality 11) and a gzip copy (level 9),
<file>.br and <file>.gz, using vite-plugin-compression2. A file gets copies only if its
extension is in COMPRESSIBLE, it is at least THRESHOLD bytes, and the copy is smaller. Files
in public/ get them too.
A static host such as GitLab Pages compresses nothing as it serves. It serves a file’s .br or
.gz in its place, to a browser that accepts it, only when that copy is there. Without them, a
Storybook or an app with megabytes of script crosses the network uncompressed.
Tune it with baseConfig({ compress: { threshold, extensions, encodings } }), or turn it off with
baseConfig({ compress: false }), for a site no static host serves.
Compress what Vite did not write
A site’s folder can hold files Vite did not build: Storybook’s manager, which Storybook copies in
as it ships, a folder its staticDirs serves, and whatever a pipeline copies in after the build.
Vite writes no copies of those; compress them in the job that publishes the folder, after the
build, and keep the copies Vite already wrote.
Use it in an app
An app’s vite.config starts from the base and adds its own settings:
import { defineConfig, mergeConfig, type PluginOption } from 'vite'
import { baseConfig } from '@wtf/project-vite/config'
// The app's own plugins, as `react()` of @vitejs/plugin-react.declare const react: () => PluginOption
export default defineConfig(mergeConfig(baseConfig(), { plugins: [react()] }))mergeConfig appends lists, so the app’s plugins come after the base’s, and any conditions it
gives come after @wtf/source.
Use it in Storybook
A Storybook’s viteFinal takes the config Storybook made, merges the base into it, then merges its
own settings:
import { mergeConfig, type InlineConfig } from 'vite'
import { baseConfig } from '@wtf/project-vite/config'
// `StorybookConfig` of the Storybook's framework, as @storybook/react-vite's.type StorybookConfig = { stories: string[] viteFinal: (vite: InlineConfig) => InlineConfig}
const config: StorybookConfig = { stories: ['../stories/**/*.stories.tsx'], viteFinal: (vite) => mergeConfig(mergeConfig(vite, baseConfig()), { plugins: [] })}
export default configThe base’s conditions replace any resolve.conditions the Storybook set for @wtf/source: don’t
repeat them.
Use it with Vitest
Each config of @wtf/project-vitest (rootConfig, packageConfig, integrationConfig,
browserConfig, browserRootConfig and storybookConfig) starts from the base, without the
site-build plugin. A Vitest config built on one of them needs nothing more. A Vitest config for
another suite, such as vitest.host.config.ts, starts from one of them, or from the base.
Which Vite, and which Vitest
@wtf/project-vite owns the Vite of a workspace. It depends on vite at ^8.3.5, and so on
rolldown, Vite’s bundler, at the version that Vite takes. @wtf/project-vitest depends on
@wtf/project-vite, so a repo gets its Vite through the presets. The config imports only Vite’s
types, plus vite-plugin-compression2, the one library that writes the copies.
What a repo’s manifests name:
vite: only in a manifest whose own code imports it, such as a vite.config, a Storybook’s.storybook/main.tsor a test that callsbuild(the root’s manifest, for a config at the root). Name it by@wtf/project-vite’s range,^8.3.5: never a pin, and never a range that allows an older Vite. pnpm links into a folder’snode_modulesonly what its manifest names, so a file that importsvitefinds it only through a manifest beside it or above it that names it. A repo whose only Vite configs are@wtf/project-vitest’s names noviteat all: Vitest takes it as a peer, and pnpm installs it (throughautoInstallPeers, its default) at the one version@wtf/project-viteresolves.rolldown: never. And no override ofviteorrolldownin pnpm-workspace.yaml.vitestand@vitest/coverage-v8at the root, plus@vitest/browser-playwrightandplaywrightin a repo with browser tests: each by one exact version, the one@wtf/project-vitesttakes,5.0.5, since the toolchain is pinned. They are peers of@wtf/project-vitest, never its dependencies. That way thevitestbin (pnpm links only what the root names), the tests’import { it } from 'vitest', and the coverage provider all come from the very Vitest that runs.
A repo that links these packages from a local folder, before they are published, uses the Vite and
rolldown of that folder’s own install. The two match only at one version, so move each repo along
with it: pnpm update -r vite rolldown vitest "@vitest/*".
