import vue from "@vitejs/plugin-vue"; import { defineConfig, loadEnv, type Plugin } from "vite"; const monorepoRoot = new URL("../..", import.meta.url).pathname; type AppVariant = "dev" | "pro"; function appIdentityPlugin(mode: string): Plugin { const env = loadEnv(mode, monorepoRoot, ""); const envValue = (name: string) => process.env[name] || env[name]; const defaultVariant: AppVariant = mode === "development" ? "dev" : "pro"; const configuredVariant = envValue("APP_VARIANT"); const variant: AppVariant = configuredVariant === "dev" || configuredVariant === "pro" ? configuredVariant : defaultVariant; const appName = envValue("VITE_APP_NAME") || (variant === "dev" ? "有数记账 Dev" : "有数记账"); const shortName = envValue("VITE_APP_SHORT_NAME") || appName; const appId = envValue("VITE_APP_ID") || (variant === "dev" ? "/?app=cents-dev" : "/?app=cents"); const iconPrefix = variant === "dev" ? "icon-dev" : "icon"; const appleTouchIcon = variant === "dev" ? "/apple-touch-icon-dev.png" : "/apple-touch-icon.png"; const manifest = JSON.stringify( { name: appName, short_name: shortName, id: appId, description: "家庭共享账本", start_url: "/", display: "standalone", background_color: "#ffffff", theme_color: "#43c9bd", icons: [ { src: `/${iconPrefix}-192.png`, sizes: "192x192", type: "image/png", purpose: "any", }, { src: `/${iconPrefix}-512.png`, sizes: "512x512", type: "image/png", purpose: "any", }, { src: `/${iconPrefix}-maskable-512.png`, sizes: "512x512", type: "image/png", purpose: "maskable", }, ], }, null, 2, ); return { name: "cents-app-identity", configureServer(server) { server.middlewares.use((req, res, next) => { if (req.url?.split("?")[0] !== "/manifest.webmanifest") { next(); return; } res.setHeader("Content-Type", "application/manifest+json; charset=utf-8"); res.end(manifest); }); }, transformIndexHtml(html) { return html .replace(/.*<\/title>/, `<title>${appName}`) .replace('href="/icon-192.png"', `href="/${iconPrefix}-192.png"`) .replace('href="/apple-touch-icon.png"', `href="${appleTouchIcon}"`); }, generateBundle() { this.emitFile({ type: "asset", fileName: "manifest.webmanifest", source: manifest, }); }, }; } function offlineServiceWorkerPlugin(): Plugin { const publicAssets = [ "/apple-touch-icon.png", "/apple-touch-icon-dev.png", "/icon-192.png", "/icon-512.png", "/icon-maskable-512.png", "/icon-dev-192.png", "/icon-dev-512.png", "/icon-dev-maskable-512.png", ]; return { name: "cents-offline-service-worker", generateBundle(_options, bundle) { const bundledAssets = Object.keys(bundle) .filter((fileName) => fileName !== "sw.js") .map((fileName) => `/${fileName}`); const precache = [...new Set(["/", ...bundledAssets, ...publicAssets])]; const source = `const CACHE_NAME = "cents-shell-v4"; const PRECACHE = ${JSON.stringify(precache, null, 2)}; function fetchWithTimeout(request, timeoutMs) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); return fetch(request, { signal: controller.signal }).finally(() => clearTimeout(timer)); } self.addEventListener("install", (event) => { event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE)).then(() => self.skipWaiting())); }); self.addEventListener("activate", (event) => { event.waitUntil( caches.keys().then((keys) => Promise.all( keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)), )).then(() => self.clients.claim()), ); }); self.addEventListener("fetch", (event) => { if (event.request.method !== "GET") return; const url = new URL(event.request.url); if (url.origin !== self.location.origin || url.pathname.startsWith("/api/")) return; if (event.request.mode === "navigate") { event.respondWith( fetchWithTimeout(event.request, 1500).then((response) => { if (!response.ok) throw new Error("navigation unavailable"); const copy = response.clone(); void caches.open(CACHE_NAME).then((cache) => cache.put("/", copy)); return response; }).catch(async () => (await caches.match("/")) ?? caches.match("/index.html")), ); return; } event.respondWith( caches.match(event.request).then((cached) => cached ?? fetch(event.request).then((response) => { if (response.ok) { const copy = response.clone(); void caches.open(CACHE_NAME).then((cache) => cache.put(event.request, copy)); } return response; })), ); }); `; this.emitFile({ type: "asset", fileName: "sw.js", source }); }, }; } export default defineConfig(({ mode }) => ({ envDir: monorepoRoot, plugins: [vue(), appIdentityPlugin(mode), offlineServiceWorkerPlugin()], server: { allowedHosts: ["dev.cents.homemade.net.cn"], proxy: { "/api": { target: "http://127.0.0.1:3000", changeOrigin: false, }, }, }, }));