vite-plugin-app-version
v0.3.0
Published
Vite plugin to generate an app version file (version.json) and optional virtual module with git/tag/build metadata.
Maintainers
Readme
vite-plugin-app-version
Generate an app
version.jsonplus an optionalvirtual:app-versionmodule with git/tag/build metadata — for build-info display, cache-busting, and runtime "new version available" checks. Zero dependencies, ESM + CJS.
Features
- 🏷️ Emits
version.jsonwith git tag/commit,package.jsonversion, build time, and mode. - 🧩 Optional
virtual:app-versionmodule:import version, { checkVersion, onCheck, getChangelog } from 'virtual:app-version'. - 🧭 Pluggable
versionStrategy—auto(default),git-tag,package-json, or your own function. - 📜 Optional
changelog.json— generated from git commits (conventional-commit aware), aCHANGELOG.md, an explicit list, or a custom function. - 🧪 Dev preview with
no-store+ weakETag(304), served under your Vitebase. - 🧷 Typed — ship-with
client.d.ts(zero-config) or opt-in precise generated types viaemitDtsTo. - 🔀
extraFieldsare fully type-inferred at the call site. - 🟢 Zero runtime dependencies — and it never writes into your
src/unless you ask it to.
Installation
npm i -D vite-plugin-app-version
# or: pnpm add -D vite-plugin-app-version
# or: yarn add -D vite-plugin-app-versionRequires Node ≥ 18 and Vite ≥ 4.
Usage
// vite.config.ts
import { defineConfig } from 'vite';
import { generateVersion } from 'vite-plugin-app-version';
export default defineConfig({
plugins: [
generateVersion({
extraFields: {
env: process.env.NODE_ENV,
apiUrl: 'https://api.example.com',
release: 42,
},
}),
],
});| Phase | Behavior |
|-------|----------|
| dev (vite) | version.json served at its route under your Vite base with no-store + weak ETag. |
| build (vite build) | Emitted as a build asset to the root of your build outDir. |
| outputDir | Also written to disk (dev & build), skipped when unchanged. |
| changelog | When enabled, a changelog.json is emitted/served the same way, alongside version.json. |
Example version.json
{
"pkgVersion": "9.9.9",
"version": "v2.5.0",
"commitShort": "a10d744",
"buildTime": "2026-06-07T15:01:37.037Z",
"mode": "production",
"env": "production",
"apiUrl": "https://api.example.com",
"release": 42
}The virtual:app-version module
import version, { checkVersion, onCheck, getChangelog } from 'virtual:app-version';
console.log('Build:', version.version, version.buildTime);
// React to deploys: compares the deployed version.json against this build.
const off = onCheck(({ updated, latest }) => {
if (updated) console.log('🔄 New version available:', latest?.version);
});
setInterval(checkVersion, 60_000);
// off(); // unsubscribe
// Fetch the deployed changelog.json (newest release first; [] if disabled/unreachable).
const releases = await getChangelog();| Export | Signature | Description |
|--------|-----------|-------------|
| version (default) | AppVersion | This build's inlined version metadata. |
| checkVersion | () => Promise<{ updated, latest }> | Fetches the deployed version.json and reports whether it differs from this build. |
| onCheck | (cb) => () => void | Subscribes to every checkVersion() result; returns an unsubscribe function. |
| getChangelog | () => Promise<ChangelogEntry[]> | Fetches the deployed changelog.json. Returns [] when the changelog is disabled or unreachable. |
Types
Add the shipped ambient types (the standard Vite pattern) — either a triple-slash reference in any .d.ts/entry file:
/// <reference types="vite-plugin-app-version/client" />or via tsconfig.json:
{ "compilerOptions": { "types": ["vite-plugin-app-version/client"] } }This types the standard fields and permits custom ones (loose index signature).
Precise types for extraFields (opt-in)
For exact typing of your custom fields, generate a declaration with emitDtsTo:
generateVersion({
emitDtsTo: 'src/app-version.d.ts', // written only when its content changes
extraFields: { release: 42, env: 'prod' },
});produces:
declare module 'virtual:app-version' {
export interface AppVersion {
pkgVersion: string | null;
version: string;
commitShort: string | null;
buildTime: string;
mode: string;
release: number; // ← exact
env: string; // ← exact
}
// ...checkVersion / onCheck / getChangelog / default export
}Unlike v0.2, the plugin does not write any file into your source tree by default. Generation happens only when you set
emitDtsTo, and only when the content actually changes.
Options
generateVersion<Extra>(options?: AppVersionOptions<Extra>): Plugin| Option | Type | Default | Description |
|--------|------|---------|-------------|
| filename | string | "version.json" | Output filename. |
| publicFields | AppVersionField[] | ["pkgVersion","version","commitShort","buildTime","mode"] | Which standard fields to expose. |
| exposeVirtual | boolean | true | Enable the virtual:app-version module. |
| extraFields | Record<string, unknown> | {} | Custom fields merged into the output (and inferred into types). |
| outputDir | string | — | Also mirror version.json (and changelog.json) to this directory on disk. |
| emitDtsTo | string | — | Opt-in: write a precise .d.ts for the virtual module to this path. |
| versionStrategy | VersionStrategy | "auto" | How the version string is resolved. See Version strategy. |
| changelog | boolean \| ChangelogOptions | false | Emit a changelog.json and expose getChangelog(). See Changelog. |
Standard fields (AppVersionInfo)
interface AppVersionInfo {
version: string; // git exact tag → git describe → short commit → pkgVersion → timestamp
commitShort: string | null;
pkgVersion: string | null;
buildTime: string; // ISO timestamp
mode: string; // Vite mode
}Git is detected via a statically-imported
execSync, so it works in both the ESM and CJS builds. Outside a git work tree,versionfalls back topkgVersion, then a timestamp.
Version strategy
versionStrategy controls how the human-readable version string is resolved. Any strategy that
yields nothing (null/empty) gracefully degrades to the auto chain, so version is always
non-empty.
| Strategy | Resolves to |
|----------|-------------|
| "auto" (default) | git describe (exact tag → nearest tag → short commit) → package.json version → build timestamp. |
| "git-tag" | The tag on HEAD → nearest tag → short commit. |
| "package-json" | The version field from package.json. |
| (ctx) => string \| null | Full control. Receives a VersionContext; return null to fall back to auto. |
generateVersion({
versionStrategy: (ctx) => `${ctx.pkgVersion}+${ctx.commitShort}`,
});interface VersionContext {
pkgVersion: string | null; // package.json version
gitDescribe: string | null; // git describe (exact tag → nearest → short commit)
gitTag: string | null; // tag on HEAD, when HEAD sits exactly on a tag
commitShort: string | null; // short git commit hash
mode: string; // Vite mode
root: string; // resolved project root
}Changelog
Opt in with changelog to emit a changelog.json (served/emitted alongside version.json)
and expose getChangelog() on the virtual module. Pass true for the default git source, or an
object to choose the source and tune output.
generateVersion({ changelog: true }); // git commits, grouped by tag
generateVersion({ changelog: { strategy: 'file' } }); // parse CHANGELOG.md
generateVersion({ changelog: { limit: 10 } }); // keep the 10 newest releases| Strategy | Source |
|----------|--------|
| "git" (default) | Commits grouped by tag, parsed as conventional commits (feat, fix(scope):, feat!:…). Commits after the newest tag form an "Unreleased" entry. |
| "file" | A Keep a Changelog style CHANGELOG.md (path overrides the location). |
| "config" | The explicit entries array you pass in. |
| (ctx) => ChangelogEntry[] | Full control; sync or async. |
ChangelogOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| strategy | "git" \| "file" \| "config" \| function | "git" | Where entries come from. |
| filename | string | "changelog.json" | Asset filename written/served. |
| path | string | "CHANGELOG.md" | "file" strategy: markdown path (relative to root). |
| entries | ChangelogEntry[] | — | "config" strategy: the explicit entry list. |
| limit | number | — | Cap the number of releases (newest kept). |
Example changelog.json
[
{
"version": "Unreleased",
"date": null,
"changes": [
{ "type": "feat", "scope": "api", "message": "add changelog support", "hash": "a10d744" }
]
},
{
"version": "v2.5.0",
"date": "2026-06-07T15:01:37.000Z",
"changes": [
{ "type": "fix", "scope": null, "message": "handle missing tags", "hash": "9f3c2b1" }
]
}
]interface ChangelogEntry {
version: string; // tag label, or "Unreleased"
date: string | null; // ISO date, or null when unknown
changes: ChangelogChange[];
}
interface ChangelogChange {
type: string | null; // conventional-commit type / Keep-a-Changelog heading
scope?: string | null; // conventional-commit scope, when present
message: string;
hash?: string | null; // short commit hash, when sourced from git
}License
MIT © dev.zarghami
