Architecture
staticRouteExport Vite plugin performs bounded file output only after a written HTML web build has closed; importing or creating it writes nothing.The preset factories are pure functions from options to plain config objects, so they are cheap
to test and easy to override. The opt-in staticRouteExport Vite plugin performs bounded file
output only after a written HTML web build has closed; importing or creating it writes nothing.
Module boundaries
vite ───────────> vite (mergeConfig, normalizePath)
> node:fs, node:path (opt-in static route output)
vitest ─────────> @vitest/browser-playwright
capacitor ──────> (leaf: no imports)
tsup ───────────> tsup (types only)
cli ────────────> capacitor
index ──────────> re-exports capacitor, vite and vitest (never tsup)Nothing imports a framework plugin. tsup is imported for types only, so its absence costs a
consumer nothing at runtime, and the barrel leaves it out so that a project that never builds a
library never needs its types either.
Contracts
Static route export uses the final resolved output directory and transformed index, validates portable declared directory paths, refuses linked directories and conflicting documents, and requires an asset base that survives nesting. The plugin does not configure a deployment server. Its integration tests execute Vite builds and verify output bytes and asset references separately from HTTP routing acceptance.
- Overrides merge last. Every factory takes an
overridesoption applied after all derived fields, so no project is boxed in by a preset default. - Headed and muted browser tests.
defineBrowserTestconstructs the Playwright provider itself and refusesheadlessin any spelling. The mute flag cannot be separated from the provider because there is no standalone argument array to pass around. - Absolute aliases only. A relative
srcDirthrows, because Vite would resolve it against the process working directory rather than the config file. - Rolldown groups, not
manualChunks. Vite 8 deprecatesmanualChunks. The factory emitscodeSplitting.groupsand reconciles same-named groups aftermergeConfig, becausemergeConfigconcatenates nested arrays and would otherwise keep both definitions. Caller-namedchunkscome first in the group list, thenheavyDepsgroups: Rolldown gives a module to the matching group with the smaller index, so list order is precedence. - Capacitor merges are explicit. Replacing a whole
serverobject by accident dropsandroidScheme, which breaks the app on Android, so the three platform sections andpluginsmerge one level deep and everything else replaces. - Types match the runtime. The CommonJS build emits its own
.d.ctsdeclarations; pointingrequireat an ESM.d.tsmakes TypeScript read the types as ESM while Node loads CommonJS.
Build and packaging
scripts/build.mjs runs the installed TypeScript twice, once per tsconfig.esm.json and
tsconfig.cjs.json, with no bundler. The CommonJS output is renamed to .cjs and .d.cts, its
specifiers are rewritten, and dist/cjs gets a { "type": "commonjs" } manifest. The CLI keeps its
shebang and is marked executable.
The package has no runtime dependencies. pnpm verify ends in a packed-consumer smoke that installs
the tarball into an empty project against the public registry only, loads every entry point under
ESM and CommonJS, type-checks a consumer file, builds a Vite app, and optionally launches headed
Chromium. CI runs that smoke against both supported Vitest majors.
Intentional limits
- No framework plugins, no Tailwind, no React setup.
- No release workflow YAML for Capacitor: signing and release policy belong to the application.
- No
headlessmode in the browser fragment.