Add app version management, update checks, and periodic SW updates
- Introduced version.json and appVersionAssetPlugin for build-time version tracking. - Enhanced settings page with update check/status UI. - Refactored bootstrap to handle SW updates and initiate periodic checks.
This commit is contained in:
@@ -93,6 +93,7 @@ For installability and service worker support:
|
|||||||
|
|
||||||
- serve `manifest.webmanifest` with an appropriate web manifest content type
|
- serve `manifest.webmanifest` with an appropriate web manifest content type
|
||||||
- make sure `service-worker.js` is reachable from the deployed site root
|
- make sure `service-worker.js` is reachable from the deployed site root
|
||||||
|
- make sure `version.json` is reachable from the deployed site root for app update checks
|
||||||
- avoid aggressive caching on `index.html` during upgrades so new builds are picked up reliably
|
- avoid aggressive caching on `index.html` during upgrades so new builds are picked up reliably
|
||||||
|
|
||||||
### Smoke test after deployment
|
### Smoke test after deployment
|
||||||
@@ -113,6 +114,7 @@ public/
|
|||||||
manifest.webmanifest
|
manifest.webmanifest
|
||||||
offline.html
|
offline.html
|
||||||
service-worker.js
|
service-worker.js
|
||||||
|
version.json
|
||||||
src/
|
src/
|
||||||
api/
|
api/
|
||||||
app/
|
app/
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "lonc-web",
|
"name": "lonc-web",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "lonc-web",
|
"name": "lonc-web",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zxing/browser": "^0.1.5",
|
"@zxing/browser": "^0.1.5",
|
||||||
"alpinejs": "^3.14.9",
|
"alpinejs": "^3.14.9",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lonc-web",
|
"name": "lonc-web",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ const APP_SHELL = ['/', '/index.html', '/manifest.webmanifest', '/offline.html',
|
|||||||
|
|
||||||
self.addEventListener('install', (event) => {
|
self.addEventListener('install', (event) => {
|
||||||
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)));
|
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)));
|
||||||
self.skipWaiting();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('activate', (event) => {
|
self.addEventListener('activate', (event) => {
|
||||||
@@ -19,6 +18,12 @@ self.addEventListener('activate', (event) => {
|
|||||||
self.clients.claim();
|
self.clients.claim();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event?.data?.type === 'SKIP_WAITING') {
|
||||||
|
self.skipWaiting();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
self.addEventListener('fetch', (event) => {
|
self.addEventListener('fetch', (event) => {
|
||||||
if (event.request.method !== 'GET') {
|
if (event.request.method !== 'GET') {
|
||||||
return;
|
return;
|
||||||
@@ -40,6 +45,11 @@ self.addEventListener('fetch', (event) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requestUrl.pathname === '/version.json') {
|
||||||
|
event.respondWith(fetch(event.request, { cache: 'no-store' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const destination = event.request.destination;
|
const destination = event.request.destination;
|
||||||
if (
|
if (
|
||||||
destination === 'script' ||
|
destination === 'script' ||
|
||||||
|
|||||||
Vendored
+190
-4
@@ -9,24 +9,201 @@ import { appShell } from '../components/app-shell.js';
|
|||||||
import { navBar } from '../components/nav-bar.js';
|
import { navBar } from '../components/nav-bar.js';
|
||||||
import { registerFeatureData } from '../features/register.js';
|
import { registerFeatureData } from '../features/register.js';
|
||||||
|
|
||||||
|
const APP_UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
function createAppUpdateManager() {
|
||||||
|
let registration = null;
|
||||||
|
let waitingWorker = null;
|
||||||
|
|
||||||
|
async function fetchServerVersion() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/version.json?ts=${Date.now()}`, {
|
||||||
|
cache: 'no-store',
|
||||||
|
headers: {
|
||||||
|
'cache-control': 'no-cache',
|
||||||
|
pragma: 'no-cache',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Version endpoint failed with HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await response.json();
|
||||||
|
const serverVersion = String(payload?.version || '').trim();
|
||||||
|
const serverBuildTime = String(payload?.buildTime || '').trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
serverVersion: serverVersion || null,
|
||||||
|
serverBuildTime: serverBuildTime || null,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
serverVersion: null,
|
||||||
|
serverBuildTime: null,
|
||||||
|
error: error instanceof Error ? error.message : 'Unable to reach version endpoint.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncWaitingWorker() {
|
||||||
|
waitingWorker = registration?.waiting || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupRegistrationHooks() {
|
||||||
|
if (!registration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncWaitingWorker();
|
||||||
|
|
||||||
|
registration.addEventListener('updatefound', () => {
|
||||||
|
const installing = registration.installing;
|
||||||
|
if (!installing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
installing.addEventListener('statechange', () => {
|
||||||
|
if (installing.state === 'installed' && navigator.serviceWorker.controller) {
|
||||||
|
waitingWorker = registration.waiting || installing;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function installServiceWorker() {
|
async function installServiceWorker() {
|
||||||
if (!('serviceWorker' in navigator)) {
|
if (!('serviceWorker' in navigator)) {
|
||||||
return;
|
return { supported: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
await Promise.all(registrations.map((registration) => registration.unregister()));
|
await Promise.all(registrations.map((existingRegistration) => existingRegistration.unregister()));
|
||||||
|
return { supported: false, development: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
registration = await navigator.serviceWorker.register('/service-worker.js');
|
||||||
|
setupRegistrationHooks();
|
||||||
|
|
||||||
|
return {
|
||||||
|
supported: true,
|
||||||
|
registered: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkForAppUpdate() {
|
||||||
|
if (registration) {
|
||||||
|
await registration.update().catch(() => {});
|
||||||
|
syncWaitingWorker();
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = await fetchServerVersion();
|
||||||
|
const hasVersionMismatch = Boolean(server.serverVersion && server.serverVersion !== APP_VERSION);
|
||||||
|
|
||||||
|
return {
|
||||||
|
supported: 'serviceWorker' in navigator,
|
||||||
|
currentVersion: APP_VERSION,
|
||||||
|
serverVersion: server.serverVersion,
|
||||||
|
serverBuildTime: server.serverBuildTime,
|
||||||
|
waitingWorker: Boolean(waitingWorker),
|
||||||
|
updateAvailable: Boolean(waitingWorker) || hasVersionMismatch,
|
||||||
|
hasVersionMismatch,
|
||||||
|
serverError: server.error || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForControllerChange(previousController, timeoutMs = 4000) {
|
||||||
|
if (!('serviceWorker' in navigator)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await navigator.serviceWorker.register('/service-worker.js');
|
if (navigator.serviceWorker.controller && navigator.serviceWorker.controller !== previousController) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
let isDone = false;
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
if (isDone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDone = true;
|
||||||
|
navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onControllerChange = () => {
|
||||||
|
finish();
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeout = setTimeout(finish, timeoutMs);
|
||||||
|
|
||||||
|
navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearAllServiceWorkerCaches() {
|
||||||
|
if (!('caches' in window)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = await caches.keys();
|
||||||
|
await Promise.all(keys.map((key) => caches.delete(key)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyAppUpdate() {
|
||||||
|
const previousController = navigator.serviceWorker?.controller || null;
|
||||||
|
|
||||||
|
if (registration) {
|
||||||
|
await registration.update().catch(() => {});
|
||||||
|
syncWaitingWorker();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waitingWorker) {
|
||||||
|
waitingWorker.postMessage({ type: 'SKIP_WAITING' });
|
||||||
|
await waitForControllerChange(previousController);
|
||||||
|
}
|
||||||
|
|
||||||
|
await clearAllServiceWorkerCaches().catch(() => {});
|
||||||
|
|
||||||
|
const nextUrl = new URL(window.location.href);
|
||||||
|
nextUrl.searchParams.set('app_update', String(Date.now()));
|
||||||
|
window.location.replace(nextUrl.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPeriodicChecks() {
|
||||||
|
if (!registration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setInterval(() => {
|
||||||
|
checkForAppUpdate().catch(() => {});
|
||||||
|
}, APP_UPDATE_CHECK_INTERVAL_MS);
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
checkForAppUpdate().catch(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
installServiceWorker,
|
||||||
|
checkForAppUpdate,
|
||||||
|
applyAppUpdate,
|
||||||
|
startPeriodicChecks,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bootstrapApp() {
|
export function bootstrapApp() {
|
||||||
const store = createAppStore();
|
const store = createAppStore();
|
||||||
Alpine.store('app', store);
|
Alpine.store('app', store);
|
||||||
|
|
||||||
|
const appUpdateManager = createAppUpdateManager();
|
||||||
|
|
||||||
registerFeatureData(Alpine, store);
|
registerFeatureData(Alpine, store);
|
||||||
|
|
||||||
const appRoot = document.querySelector('#app');
|
const appRoot = document.querySelector('#app');
|
||||||
@@ -104,6 +281,12 @@ export function bootstrapApp() {
|
|||||||
renderNav();
|
renderNav();
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
async checkForAppUpdate() {
|
||||||
|
return appUpdateManager.checkForAppUpdate();
|
||||||
|
},
|
||||||
|
async applyAppUpdate() {
|
||||||
|
return appUpdateManager.applyAppUpdate();
|
||||||
|
},
|
||||||
handleAuthFailure(error) {
|
handleAuthFailure(error) {
|
||||||
if (!store.session?.applicationKey || !store.session?.hasValidated || authFailureHandled) {
|
if (!store.session?.applicationKey || !store.session?.hasValidated || authFailureHandled) {
|
||||||
return;
|
return;
|
||||||
@@ -152,7 +335,10 @@ export function bootstrapApp() {
|
|||||||
|
|
||||||
renderNav();
|
renderNav();
|
||||||
|
|
||||||
installServiceWorker().catch(() => {
|
appUpdateManager
|
||||||
|
.installServiceWorker()
|
||||||
|
.then(() => appUpdateManager.startPeriodicChecks())
|
||||||
|
.catch(() => {
|
||||||
store.addAlert({
|
store.addAlert({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'PWA installation support could not be initialized.',
|
message: 'PWA installation support could not be initialized.',
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import { APP_VERSION } from '../../app/config.js';
|
||||||
|
|
||||||
export function renderSettingsPage() {
|
export function renderSettingsPage() {
|
||||||
return `
|
return `
|
||||||
<section class="container-xxl py-4 py-lg-5">
|
<section class="container-xxl py-4 py-lg-5">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<div class="col-12 col-lg-7">
|
<div class="col-12 col-lg-7">
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-body p-4" x-data="settingsPage()">
|
<div class="card-body p-4" x-data="settingsPage()" x-init="initUpdatePanel()">
|
||||||
<div class="d-flex justify-content-between align-items-start mb-4">
|
<div class="d-flex justify-content-between align-items-start mb-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow mb-2">Client Settings</p>
|
<p class="eyebrow mb-2">Client Settings</p>
|
||||||
@@ -40,6 +42,45 @@ export function renderSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary align-self-start" type="submit">Save settings</button>
|
<button class="btn btn-primary align-self-start" type="submit">Save settings</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<hr class="my-4" />
|
||||||
|
|
||||||
|
<div class="vstack gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="h5 mb-1">App update</h2>
|
||||||
|
<p class="text-body-secondary mb-0">
|
||||||
|
Check for the latest deployed build and force-refresh this installed app when needed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="row g-3 small">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<div class="border rounded-3 p-3 h-100 bg-body-tertiary">
|
||||||
|
<div class="text-uppercase text-body-secondary fw-semibold mb-1">Current version</div>
|
||||||
|
<div class="fw-semibold" x-text="update.currentVersion"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<div class="border rounded-3 p-3 h-100 bg-body-tertiary">
|
||||||
|
<div class="text-uppercase text-body-secondary fw-semibold mb-1">Server version</div>
|
||||||
|
<div class="fw-semibold" x-text="update.serverVersion || 'Unavailable'"></div>
|
||||||
|
<template x-if="update.serverBuildTime">
|
||||||
|
<div class="text-body-secondary mt-1" x-text="'Built: ' + formatBuildTime(update.serverBuildTime)"></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" @click="checkForUpdates()" :disabled="update.isChecking || update.isApplying">
|
||||||
|
<span x-show="!update.isChecking">Check for updates</span>
|
||||||
|
<span x-show="update.isChecking">Checking...</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" type="button" @click="applyUpdate()" :disabled="update.isApplying">
|
||||||
|
<span x-show="!update.isApplying">Update app</span>
|
||||||
|
<span x-show="update.isApplying">Updating...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="small" :class="updateStatusClass()" x-text="update.statusText"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,6 +109,15 @@ export function settingsPageData(store) {
|
|||||||
baseUrl: store.config.baseUrl || '',
|
baseUrl: store.config.baseUrl || '',
|
||||||
database: store.config.database || '',
|
database: store.config.database || '',
|
||||||
},
|
},
|
||||||
|
update: {
|
||||||
|
currentVersion: APP_VERSION,
|
||||||
|
serverVersion: null,
|
||||||
|
serverBuildTime: null,
|
||||||
|
statusText: 'Ready to check for updates.',
|
||||||
|
statusType: 'secondary',
|
||||||
|
isChecking: false,
|
||||||
|
isApplying: false,
|
||||||
|
},
|
||||||
get userLogin() {
|
get userLogin() {
|
||||||
return store.session?.userLogin || '';
|
return store.session?.userLogin || '';
|
||||||
},
|
},
|
||||||
@@ -83,5 +133,83 @@ export function settingsPageData(store) {
|
|||||||
|
|
||||||
store.addAlert({ type: 'success', message: 'Settings saved locally.' });
|
store.addAlert({ type: 'success', message: 'Settings saved locally.' });
|
||||||
},
|
},
|
||||||
|
formatBuildTime(value) {
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleString();
|
||||||
|
},
|
||||||
|
updateStatusClass() {
|
||||||
|
switch (this.update.statusType) {
|
||||||
|
case 'success':
|
||||||
|
return 'text-success';
|
||||||
|
case 'warning':
|
||||||
|
return 'text-warning';
|
||||||
|
case 'danger':
|
||||||
|
return 'text-danger';
|
||||||
|
default:
|
||||||
|
return 'text-body-secondary';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async initUpdatePanel() {
|
||||||
|
await this.checkForUpdates();
|
||||||
|
},
|
||||||
|
async checkForUpdates() {
|
||||||
|
if (!window.__loncApp?.checkForAppUpdate) {
|
||||||
|
this.update.statusText = 'Service worker updates are not available in this browser.';
|
||||||
|
this.update.statusType = 'warning';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update.isChecking = true;
|
||||||
|
this.update.statusText = 'Checking for updates...';
|
||||||
|
this.update.statusType = 'secondary';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.__loncApp.checkForAppUpdate();
|
||||||
|
this.update.currentVersion = result.currentVersion || APP_VERSION;
|
||||||
|
this.update.serverVersion = result.serverVersion || null;
|
||||||
|
this.update.serverBuildTime = result.serverBuildTime || null;
|
||||||
|
|
||||||
|
if (result.updateAvailable) {
|
||||||
|
this.update.statusText = 'Update available. Use "Update app" to refresh this installed build.';
|
||||||
|
this.update.statusType = 'warning';
|
||||||
|
} else if (result.serverError) {
|
||||||
|
this.update.statusText = `No update signal from server (${result.serverError}).`;
|
||||||
|
this.update.statusType = 'secondary';
|
||||||
|
} else {
|
||||||
|
this.update.statusText = 'This app is up to date.';
|
||||||
|
this.update.statusType = 'success';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown update check error.';
|
||||||
|
this.update.statusText = `Update check failed: ${message}`;
|
||||||
|
this.update.statusType = 'danger';
|
||||||
|
} finally {
|
||||||
|
this.update.isChecking = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async applyUpdate() {
|
||||||
|
if (!window.__loncApp?.applyAppUpdate) {
|
||||||
|
this.update.statusText = 'Update action is not available in this browser.';
|
||||||
|
this.update.statusType = 'warning';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update.isApplying = true;
|
||||||
|
this.update.statusText = 'Applying update and refreshing app...';
|
||||||
|
this.update.statusType = 'warning';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.__loncApp.applyAppUpdate();
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown update error.';
|
||||||
|
this.update.statusText = `Update failed: ${message}`;
|
||||||
|
this.update.statusType = 'danger';
|
||||||
|
this.update.isApplying = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,32 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import packageJson from './package.json';
|
import packageJson from './package.json';
|
||||||
|
|
||||||
|
function appVersionAssetPlugin() {
|
||||||
|
return {
|
||||||
|
name: 'app-version-asset',
|
||||||
|
apply: 'build',
|
||||||
|
generateBundle() {
|
||||||
|
this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
fileName: 'version.json',
|
||||||
|
source: JSON.stringify(
|
||||||
|
{
|
||||||
|
version: packageJson.version,
|
||||||
|
buildTime: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
define: {
|
define: {
|
||||||
__APP_VERSION__: JSON.stringify(packageJson.version),
|
__APP_VERSION__: JSON.stringify(packageJson.version),
|
||||||
},
|
},
|
||||||
|
plugins: [appVersionAssetPlugin()],
|
||||||
server: {
|
server: {
|
||||||
port: 4173,
|
port: 4173,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user