import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; import { VitePWA } from 'vite-plugin-pwa'; import basicSsl from '@vitejs/plugin-basic-ssl'; import { statSync } from 'node:fs'; import { APP_NAME } from './src/config'; // Exact (decoded) sizes of the big data files: servers compress and stream them without a Content-Length, so the loading bar needs these. const size = (f: string) => { try { return statSync(f).size; } catch { return 0; } }; const DATA_BYTES = { snapshot: size('public/data/kk-snapshot.json'), transit: size('public/data/kk-transit.json') }; export default defineConfig({ base: './', define: { __DATA_BYTES__: JSON.stringify(DATA_BYTES) }, plugins: [ react(), // index.html uses %APP_NAME%: one name for the whole app (src/config.ts) { name: 'app-name', transformIndexHtml: (html: string) => html.replaceAll('%APP_NAME%', APP_NAME) }, // HTTPS=1 pnpm preview:https -> phones need a secure context for motion sensors and GPS ...(process.env.HTTPS ? [basicSsl()] : []), VitePWA({ registerType: 'autoUpdate', useCredentials: true, // manifest sends cookies, or the reverse-proxy login (Authentik) redirects it cross-origin and CORS blocks it includeAssets: ['uswds/sprite.svg', 'icons/*.png', 'icons/*.svg'], manifest: { name: APP_NAME, short_name: APP_NAME, description: 'Sprawdź dostępność miejsc i tras w Krakowie dla osób na wózkach i rodziców z wózkami dziecięcymi.', lang: 'pl', start_url: './', scope: './', display: 'standalone', background_color: '#ffffff', theme_color: '#1b1b1b', icons: [ { src: 'icons/icon-192.png', sizes: '192x192', type: 'image/png' }, { src: 'icons/icon-512.png', sizes: '512x512', type: 'image/png' }, { src: 'icons/icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, ], }, workbox: { // Offline-first: the whole app (code, fonts, icons, places, paths, timetable) is precached on the first visit. globPatterns: ['**/*.{js,mjs,css,html,svg,png,json,woff2}'], maximumFileSizeToCacheInBytes: 12 * 1024 * 1024, // the places snapshot is ~5.5 MB navigateFallback: 'index.html', navigateFallbackDenylist: [/^\/rt\//], // live feeds are never replaced by the app shell cleanupOutdatedCaches: true, clientsClaim: true, skipWaiting: true, runtimeCaching: [ { // street map: the offline pack (src/lib/offline.ts) fills this cache; tiles seen while browsing land here too urlPattern: /^https:\/\/tile\.openstreetmap\.org\/.*/, handler: 'CacheFirst', options: { cacheName: 'osm-tiles', expiration: { maxEntries: 3000, maxAgeSeconds: 60 * 60 * 24 * 180, purgeOnQuotaError: true }, cacheableResponse: { statuses: [0, 200] }, }, }, { urlPattern: /^https:\/\/server\.arcgisonline\.com\/.*/, handler: 'CacheFirst', options: { cacheName: 'sat-tiles', expiration: { maxEntries: 1500, maxAgeSeconds: 60 * 60 * 24 * 60, purgeOnQuotaError: true }, cacheableResponse: { statuses: [0, 200] }, }, }, { // photo assessment: the on-device AI runtime and model are large; once used online they work offline too urlPattern: ({ url }: { url: URL }) => url.pathname.endsWith('.wasm') || /(^|\.)(huggingface\.co|hf\.co)$/.test(url.hostname), handler: 'CacheFirst', options: { cacheName: 'ai-runtime', expiration: { maxEntries: 40, purgeOnQuotaError: true }, cacheableResponse: { statuses: [0, 200] }, }, }, ], }, }), ], // ZTP Kraków serves GTFS-Realtime without CORS headers: `rt/*` is proxied for `pnpm dev` and `pnpm preview`. // A static production host needs the same rule (e.g. nginx `location /rt/ { proxy_pass https://gtfs.ztp.krakow.pl/; }`). server: { proxy: { '/rt': { target: 'https://gtfs.ztp.krakow.pl', changeOrigin: true, rewrite: (p) => p.replace(/^\/rt/, '') } } }, preview: { proxy: { '/rt': { target: 'https://gtfs.ztp.krakow.pl', changeOrigin: true, rewrite: (p) => p.replace(/^\/rt/, '') } } }, css: { preprocessorOptions: { scss: { loadPaths: ['node_modules/@uswds/uswds/packages'], quietDeps: true, silenceDeprecations: ['import', 'mixed-decls', 'global-builtin', 'color-functions'], }, }, }, test: { environment: 'node', include: ['src/**/*.test.ts'] }, });