import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; import { VitePWA } from "vite-plugin-pwa"; function resolveManualChunk(id: string) { if (id.includes("node_modules")) { if (["react", "react-dom", "react-router-dom"].some((pkg) => id.includes(`/node_modules/${pkg}/`))) { return "vendor-react"; } if (id.includes("/node_modules/@tanstack/react-query/")) { return "vendor-query"; } if (id.includes("/node_modules/@zxing/browser/")) { return "vendor-scan"; } if ( id.includes("/node_modules/@radix-ui/") || id.includes("/node_modules/class-variance-authority/") || id.includes("/node_modules/clsx/") || id.includes("/node_modules/tailwind-merge/") || id.includes("/node_modules/sonner/") ) { return "vendor-ui"; } return undefined; } if ( id.includes("/src/api/http.ts") || id.includes("/src/api/types.ts") || id.includes("/src/api/errorMessages.ts") || id.includes("/src/api/auth.ts") || id.includes("/src/auth/") || id.includes("/src/components/common/") || id.includes("/src/hooks/") || id.includes("/src/lib/") || id.includes("/src/pwa.ts") ) { return "app-core"; } if (id.includes("/src/api/masterData.ts") || id.includes("/src/components/ai/")) { return "app-domain-shared"; } return undefined; } function normalizePublicBuildSha(value: string | undefined, fallback: string) { const normalized = String(value ?? "").trim().toLowerCase(); return /^[0-9a-f]{7,64}$/.test(normalized) ? normalized.slice(0, 12) : fallback; } export default defineConfig(({ mode }) => { const env = loadEnv(mode, ".", "VITE_"); const publicBuildSha = normalizePublicBuildSha( env.VITE_BUILD_SHA, mode === "production" ? "unknown" : "development", ); return { define: { "import.meta.env.VITE_BUILD_SHA": JSON.stringify(publicBuildSha), }, plugins: [ react(), VitePWA({ disable: false, registerType: "autoUpdate", injectRegister: false, // pwa.ts owns native registration and update reloads. includeAssets: ["pwa-192x192.png", "pwa-512x512.png", "pwa-maskable-512x512.png"], manifest: { name: "hyb-oa 移动工作台", short_name: "hyb-oa", description: "海音达工作台移动端入口,支持离线 PDF 发票上传与移动审批。", start_url: "/", display: "standalone", background_color: "#ffffff", theme_color: "#2563eb", icons: [ { src: "pwa-192x192.png", sizes: "192x192", type: "image/png", }, { src: "pwa-512x512.png", sizes: "512x512", type: "image/png", }, { src: "pwa-maskable-512x512.png", sizes: "512x512", type: "image/png", purpose: "maskable", }, ], }, workbox: { cleanupOutdatedCaches: true, clientsClaim: true, navigateFallback: "/index.html", navigateFallbackDenylist: [/^\/api\//], globPatterns: ["**/*.{js,css,html,ico,png,svg,webmanifest}"], skipWaiting: true, runtimeCaching: [ { urlPattern: ({ request, url }) => ["script", "style", "font", "image"].includes(request.destination) || url.pathname.endsWith(".webmanifest"), handler: "StaleWhileRevalidate", options: { cacheName: "app-static-runtime", cacheableResponse: { statuses: [0, 200], }, expiration: { maxEntries: 120, maxAgeSeconds: 7 * 24 * 60 * 60, }, }, }, { urlPattern: ({ request, url }) => { if (request.method !== "GET") { return false; } return ( url.pathname === "/api/me" || url.pathname.startsWith("/api/workflow/tasks") || url.pathname.startsWith("/api/workflow/approvers") || url.pathname.startsWith("/api/notifications") ); }, // Fall back to cache only when the network fails (offline). A network // timeout here served pre-approval task state to slow-but-online clients. handler: "NetworkFirst", options: { cacheName: "app-api-runtime", cacheableResponse: { statuses: [200], }, expiration: { maxEntries: 80, maxAgeSeconds: 30 * 60, }, }, }, ], }, devOptions: { enabled: false, }, }), ], build: { rollupOptions: { output: { manualChunks(id) { return resolveManualChunk(id); }, }, }, }, server: { proxy: { "/api": { target: "http://127.0.0.1:3000", changeOrigin: false, }, }, }, }; });