Files
2026-05-04 15:10:18 +04:00

174 lines
6.6 KiB
TypeScript

import { defineConfig, loadEnv, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'
/**
* Dev-only middleware that hands back a "killswitch" service worker at /sw.js.
*
* Problem it solves: the packaged `openclaw_client start` server registers a
* real PWA service worker that precaches the built bundle. Later, when you
* stop that service and run `npm run dev` on the same origin, the browser
* still has the old SW registered and keeps serving stale /assets/*.js
* chunks from its precache — dev edits (and redux-logger) never appear
* until you hard-reload or manually unregister.
*
* On every navigation the browser refetches /sw.js to check for updates. By
* responding with a different script here, we force the browser to install
* this new SW, which immediately unregisters itself, wipes all caches, and
* navigates every open tab back to its current URL — so the next load hits
* Vite cleanly with no further user action.
*
* Scoped to dev only (`apply: 'serve'`); the plugin is inert during `build`,
* so the real Workbox-generated /sw.js ships in production.
*/
function serviceWorkerKillswitch(): Plugin {
const SW_SOURCE = `/* Auto-generated by vite.config.ts: replaces any stale PWA service
* worker left over from a previous \`openclaw_client start\` run. On
* activation it wipes all caches, unregisters itself, and force-navigates
* every open tab so the next load hits Vite cleanly. */
self.addEventListener('install', () => { self.skipWaiting(); });
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
try {
const keys = await caches.keys();
await Promise.all(keys.map((k) => caches.delete(k)));
} catch (_e) { /* best effort */ }
try { await self.registration.unregister(); } catch (_e) { /* best effort */ }
const clients = await self.clients.matchAll({ type: 'window' });
for (const client of clients) {
try { client.navigate(client.url); } catch (_e) { /* ignore */ }
}
})());
});
`;
return {
name: 'openclaw:sw-killswitch',
apply: 'serve',
configureServer(server) {
server.middlewares.use('/sw.js', (_req, res) => {
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
res.setHeader('Cache-Control', 'no-store');
res.setHeader('Service-Worker-Allowed', '/');
res.end(SW_SOURCE);
});
},
};
}
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
const port =
Number(process.env.CLIENT_PORT) ||
Number(env.CLIENT_PORT) ||
Number(env.VITE_CLIENT_PORT) ||
18800
return {
plugins: [
react(),
serviceWorkerKillswitch(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: [
'logo_128.png',
'icons/apple-touch-icon.png',
'icons/icon-192.png',
'icons/icon-512.png',
'icons/icon-512-maskable.png',
],
manifest: {
name: 'OpenClaw Client',
short_name: 'OpenClaw',
description: 'Chat interface for OpenClaw AI agents',
theme_color: '#0b0b0b',
background_color: '#0b0b0b',
display: 'standalone',
orientation: 'any',
start_url: '/',
scope: '/',
icons: [
{
src: '/icons/icon-192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any',
},
{
src: '/icons/icon-512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any',
},
{
src: '/icons/icon-512-maskable.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
],
},
workbox: {
// workbox-build 7.x + terser has a known hang during SW minification
// (https://github.com/GoogleChrome/workbox/issues/3245). The SW is
// already small (<20KB), so skipping minification is a safe tradeoff.
mode: 'development',
// Precache static build output (JS, CSS, fonts, images) but
// **not** index.html. The shell is fetched fresh on every
// load so:
// 1. `window.__OPENCLAW_CONFIG__` (injected by serve.mjs
// from `req.headers.host`) always matches the page's
// hostname — critical when accessing the same install
// via localhost, LAN IP, and Tailscale.
// 2. After an `openclaw_client update`, the new
// hashed-asset references in index.html are picked up
// immediately instead of clients pinning to the old
// precached shell until the SW happens to update.
globPatterns: ['**/*.{js,css,svg,png,ico,woff2}'],
globIgnores: ['**/index.html', 'index.html'],
// The main app bundle currently weighs ~3.4MB unminified due to MUI + markdown.
// 6MB headroom avoids nuisance build failures when it grows slightly.
maximumFileSizeToCacheInBytes: 6 * 1024 * 1024,
// No `navigateFallback` — every SPA route is fetched from
// the network so updated index.html ships immediately. The
// tradeoff: the very first navigation after going offline
// returns whatever the browser cached on the previous load.
navigateFallback: null,
navigateFallbackDenylist: [/^\/api/, /^\/ws/],
runtimeCaching: [
{
// API is on a different origin (port 18802) but be explicit
// in case anyone proxies everything through one host.
urlPattern: ({ url }) => url.pathname.startsWith('/api/'),
handler: 'NetworkOnly',
},
{
urlPattern: ({ url }) => url.pathname.startsWith('/ws'),
handler: 'NetworkOnly',
},
],
cleanupOutdatedCaches: true,
},
devOptions: {
// Keep the SW off during `npm run dev` — otherwise its
// navigateFallback intercepts Vite's ESM requests (/main.tsx,
// /@react-refresh, /@vite/client, /manifest.webmanifest) and
// returns index.html, breaking HMR with MIME-type errors.
// Test install/offline via `npm run build && npm run preview`.
enabled: false,
type: 'module',
navigateFallback: 'index.html',
},
}),
],
server: {
port,
host: '0.0.0.0',
},
preview: {
port,
host: '0.0.0.0',
},
}
})