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.
version.json with git tag/commit, package.json version, build time, and mode.virtual:app-version module: import version, { checkVersion, onCheck, getChangelog } from 'virtual:app-version'.versionStrategy β auto (default), git-tag, package-json, or your own function.changelog.json β generated from git commits (conventional-commit aware), a CHANGELOG.md, an explicit list, or a custom function.no-store + weak ETag (304), served under your Vite base.client.d.ts (zero-config) or opt-in precise generated types via emitDtsTo.extraFields are fully type-inferred at the call site.src/ unless you ask it to.npm i -D vite-plugin-app-version
# or: pnpm add -D vite-plugin-app-version
# or: yarn add -D vite-plugin-app-version
Requires Node β₯ 18 and Vite β₯ 4.
// 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. |
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
}
virtual:app-version moduleimport 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. |
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).
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.
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. |
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.
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
}
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). |
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
}
MIT Β© dev.zarghami