Join the Newsletter!

Exclusive content & updates. No spam.

Skip to content
Alexander Opalic
Alexander Opalic

TIL - Detecting fresh deployments in a Vue 3 SPA with vite + version.json

Learn how to detect fresh deployments in a Vue 3 SPA with vite + version.json

Today I discovered a very simple way for my Vue 3 single-page app to tell users when I’ve released a new update.

The idea

  1. Build-time: Each time I run vite build, a small Vite plugin creates a version.json file. It also sets up some information that’s available when the app is built.
  2. Run-time: The app regularly checks the /version.json file.
  3. If the version or tag in that file doesn’t match the information the app was built with, we show a message like “New version available – refresh!”

Injects constants

Accesses

Vite Build Process

version-json Plugin

Environment Variables:
• import.meta.env.APP_VERSION
• import.meta.env.APP_TAG

Application Code

Compile-Time Access

The Vite plugin ( plugins/version-json.ts )

export default function function versionJson(): PluginversionJson(): Plugin {
  // …snip…
  const 
const versionDetails: {
    name: any;
    version: any;
    tag: any;
    commit: any;
    commitTime: any;
    created: string;
}
versionDetails
= {
name: anyname: pkg.name, version: anyversion: pkg.version, // 2.0.0 tag: anytag: gitTag(), // v2.0.0-2-gabcdef commit: anycommit: gitCommitHash(), // abcdef… commitTime: anycommitTime: gitCommitTimestamp(), created: stringcreated: new
var Date: DateConstructor
new () => Date (+4 overloads)
Date
().Date.toISOString(): string
Returns a date as a string value in ISO format.
toISOString
(),
} // 👉 1. Serve /version.json in dev configureServer(server) { server.middlewares.use((req: anyreq, res: anyres, next: anynext) => { if (req: anyreq.url === '/version.json') { res: anyres.setHeader('Content-Type', 'application/json') res: anyres.end(var JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
JSON
.JSON.stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
@paramvalue A JavaScript value, usually an object or array, to be converted.@paramreplacer An array of strings and numbers that acts as an approved list for selecting the object properties that will be stringified.@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
stringify
(
const versionDetails: {
    name: any;
    version: any;
    tag: any;
    commit: any;
    commitTime: any;
    created: string;
}
versionDetails
, null, 2))
return } next: anynext() }) } // 👉 2. Emit dist/version.json in production closeBundle() { writeFileSync(resolve(outDir, 'version.json'), var JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
JSON
.JSON.stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
@paramvalue A JavaScript value, usually an object or array, to be converted.@paramreplacer An array of strings and numbers that acts as an approved list for selecting the object properties that will be stringified.@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
stringify
({ ...
const versionDetails: {
    name: any;
    version: any;
    tag: any;
    commit: any;
    commitTime: any;
    created: string;
}
versionDetails
, created: stringcreated: new
var Date: DateConstructor
new () => Date (+4 overloads)
Date
().Date.toISOString(): string
Returns a date as a string value in ISO format.
toISOString
() }, null, 2))
} }

The plugin also makes import.meta.env.APP_VERSION and APP_TAG available, so the app always knows which version it’s running.


The watcher component ( ShowNewVersion.vue )


<script setup lang="ts">
const const props: DefineProps<LooseRequired<__VLS_Props>, never>props = const defineProps: <__VLS_Props>() => DefineProps<LooseRequired<__VLS_Props>, never> (+2 overloads)
Vue `<script setup>` compiler macro for declaring component props. The expected argument is the same as the component `props` option. Example runtime declaration: ```js // using Array syntax const props = defineProps(['foo', 'bar']) // using Object syntax const props = defineProps({ foo: String, bar: { type: Number, required: true } }) ``` Equivalent type-based declaration: ```ts // will be compiled into equivalent runtime declarations const props = defineProps<{ foo?: string bar: number }>() ```
@see{@link https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits}This is only usable inside `<script setup>`, is compiled away in the output and should **not** be actually called at runtime.
defineProps
<{ initialVersion: string | nullinitialVersion: string|null; initialTag: string | nullinitialTag: string|null }>()
const { const isNewVersion: anyisNewVersion, const details: anydetails } =
function useCheck(): {
    isNewVersion: any;
    details: any;
}
useCheck
()
function
function useCheck(): {
    isNewVersion: any;
    details: any;
}
useCheck
() {
const const isNewVersion: anyisNewVersion = ref(false) const const details: anydetails = ref<string|null>(null) async function function (local function) poll(): Promise<void>poll() { if (!const props: DefineProps<LooseRequired<__VLS_Props>, never>props.initialVersion: string | nullinitialVersion) return const const res: Responseres = await function fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response> (+1 overload)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)
fetch
('/version.json')
const const v: anyv = await const res: Responseres.Body.json(): Promise<any>
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json)
json
()
if (const v: anyv.version !== const props: DefineProps<LooseRequired<__VLS_Props>, never>props.initialVersion: stringinitialVersion || const v: anyv.tag !== const props: DefineProps<LooseRequired<__VLS_Props>, never>props.initialTag: string | nullinitialTag) { const isNewVersion: anyisNewVersion.value = true const details: anydetails.value = `${const v: anyv.version} (${const v: anyv.tag})` } } onMounted(() => function (local function) poll(): Promise<void>poll(), function setInterval<[]>(callback: () => void, ms?: number): NodeJS.Timeout (+2 overloads)
Schedules repeated execution of `callback` every `delay` milliseconds. When `delay` is larger than `2147483647` or less than `1`, the `delay` will be set to `1`. Non-integer delays are truncated to an integer. If `callback` is not a function, a `TypeError` will be thrown. This method has a custom variant for promises that is available using `timersPromises.setInterval()`.
@sincev0.0.1@paramcallback The function to call when the timer elapses.@paramdelay The number of milliseconds to wait before calling the `callback`.@paramargs Optional arguments to pass when the `callback` is called.@returnfor use with {@link clearInterval}
setInterval
(function (local function) poll(): Promise<void>poll, 30_000))
return { isNewVersion: anyisNewVersion, details: anydetails } } </script> <template> <div: HTMLAttributes & ReservedPropsdiv v-if="const isNewVersion: anyisNewVersion" HTMLAttributes.class?: anyclass="alert"> A new version is available → refresh! {{ const details: anydetails }} </div: HTMLAttributes & ReservedPropsdiv> </template>

How I use it

I run this command:

npm run build        # emits dist/version.json

When people use the app, their browser checks the /version.json file every 30 seconds. As soon as the file’s content changes, the yellow banner appears. They can then do a manual refresh (⌘R) to load the new version.

Takeaways

  • No server changes needed – it’s just one static file.
  • It works the same way when I’m developing locally (the file is served on the fly) and in the live app (the file is created once during the build).
  • You get information at build-time and a check at run-time, all in one.
  • You can find the full code I showed above in this repository → vueNewVersion.

For more details, you can look at the Deep Wiki documentation.

That’s it – one plugin, one tiny component, and an easy way to manage cache-busting!

#vue #vite