build-preset
v0.7.0
Published
Shared build conventions for browser games: Vite 8 and Vitest configs with headed, muted browser testing, a Capacitor config, a tsup library build, and TypeScript and Biome base configs.
Maintainers
Readme
build-preset
Shared build conventions for browser games. One dependency gives a project its Vite 8 config, its Vitest unit and real-browser test fragments, a Capacitor config, a tsup library build, and the TypeScript and Biome base configs, so a dozen game repositories stop drifting apart.
- Vite 8 game config with env-switched
base, Rolldown code-splitting groups for heavy vendors (three.js, Rapier, Phaser, Tone.js) or your own vendor chunks, HMR watch-ignore, and an optional@alias. - Vitest 4 or 5 fragments: a jsdom or Node unit config and a headed, muted real-Chromium browser config that builds the Playwright provider itself.
- Capacitor config with a documented merge, so overriding
server.hostnamekeepsserver.androidScheme. - tsup library build whose banner is derived from the package name.
tsconfig.base.jsonandbiome.base.jsonto extend.- No runtime dependencies. Every entry point ships ESM and CommonJS with matching types.
Install
pnpm add -D build-preset vite vitest @vitest/browser-playwright playwright game-harnessgame-harness provides the browser test config in the quick start below.
tsup is an optional peer: install it only if you use build-preset/tsup.
Quick start
// vite.config.ts
import { defineGamePreset } from "build-preset/vite";
import path from "node:path";
export default defineGamePreset({
appName: "my-game",
srcDir: path.resolve(import.meta.dirname, "src"),
heavyDeps: { three: true, rapier: true },
});// vitest.browser.config.ts: browser tests come from game-harness, the owner of browser QA
import { defineBrowserTestConfig } from "game-harness/vitest";
import { defineConfig } from "vitest/config";
const browserTest = defineBrowserTestConfig({
include: ["tests/browser/**/*.browser.test.ts"],
});
export default defineConfig({
optimizeDeps: { include: browserTest.__optimizeDepsInclude ?? [] },
test: browserTest,
});// tsconfig.json
{ "extends": "build-preset/tsconfig.base.json" }// biome.json
{ "extends": ["build-preset/biome.base.json"] }Vendor chunks by when they load
heavyDeps toggles fixed groups. A game that splits vendor code by when it loads names its own
chunks. In declaration order, the first chunk matching a module takes it; chunks outrank
heavyDeps:
defineGamePreset({
appName: "example-game",
chunks: {
three: ["three"], // the title preloads these three...
r3f: ["@react-three/fiber"],
drei: ["@react-three/drei"],
rapier: ["@react-three/rapier", "@dimforge/rapier3d-compat"], // ...the room loads these
post: ["@react-three/postprocessing", "postprocessing", "n8ao"],
tone: ["tone"],
},
});A string is an npm package name; a RegExp is tested against the module id. See
the Vite page for the precedence rules.
API overview
| Import | Exports |
| --- | --- |
| build-preset | Everything below except /tsup (a thin barrel; prefer the subpaths) |
| build-preset/vite | defineGamePreset, staticRouteExport, StaticRouteExportOptions, HeavyDepsOptions, VendorChunks, ChunkPattern |
| build-preset/vitest | defineUnitTest, defineBrowserTest, defaultBrowserLaunchArgs |
| build-preset/capacitor | defineCapacitorPreset, mergeCapacitorConfig, androidVersionGradleSnippet |
| build-preset/tsup | libraryBuild |
| build-preset/tsconfig.base.json, build-preset/biome.base.json | Shared configuration to extends |
The build-preset init-android command prints the CI-parameterized versionName and
versionCode block to paste into android/app/build.gradle.
See the API reference for every option and the architecture notes for why the presets behave as they do.
The browser test contract
defineBrowserTest and defaultBrowserLaunchArgs are deprecated: browser QA has one owner, and it
is game-harness (defineBrowserTestConfig from game-harness/vitest). They stay for existing
consumers and are removed in the next major.
defineBrowserTest is headed-only. It constructs the Vitest Playwright provider itself and puts
--mute-audio in provider.options.launchOptions.args, so a silent run does not depend on a
consumer remembering to copy a launch-argument array. It throws on a headless option and on any
hidden --headless Chromium argument. Muting the audio does not change a player's saved audio
preference: the consumer still asserts a non-persistent runtime mute route in its own tests.
Compatibility
| | Supported |
| --- | --- |
| Node.js | 22, 24 and 26 (CI verifies each on Linux; Windows on 26) |
| Vite | ^8.2.1 |
| Vitest | ^4.1.10 or ^5.0.0 |
| @vitest/browser-playwright | ^4.1.10 or ^5.0.0, matching Vitest |
| Playwright | >=1.62.1 <2 |
| tsup (optional) | ^8.5.0 |
The Vitest 4 and Vitest 5 lines are both exercised by the packed-consumer smoke in CI.
Development
mise install # Node 26 and pnpm 12, or `corepack enable`
pnpm install
pnpm verify # Biome, markdownlint, tsc, tests with coverage, build, publint, attw, packed-consumer smoke
pnpm docs:build # the Sourcey documentation siteSee CONTRIBUTING.md. Releases are automated by Release Please and published to npm with provenance from GitHub Actions.
Links
- Documentation: https://jonbogaty.com/build-preset/
- npm: https://www.npmjs.com/package/build-preset
- Changelog: CHANGELOG.md
- Decisions: docs/decisions.md
- Security policy: SECURITY.md
