vite-plugin-app-version

npm version license

Generate an app version.json plus an optional virtual:app-version module with git/tag/build metadata β€” for build-info display, cache-busting, and runtime β€œnew version available” checks. Zero dependencies, ESM + CJS.


Features


Installation

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.


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, version falls back to pkgVersion, 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