import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { defineConfig, type Plugin } from 'vite'; import { svelte } from '@sveltejs/vite-plugin-svelte'; /** * injectBootVersion stamps the app version into index.html's boot-capability guard, replacing its * __BOOT_VERSION__ placeholder with the same VITE_APP_VERSION build-arg that feeds __APP_VERSION__. * The guard runs before the bundle, so it cannot read the bundle's version; this lets its on-demand * diagnostic report name the client version anyway. Falls back to "dev" for a local build. */ function injectBootVersion(): Plugin { const version = process.env.VITE_APP_VERSION || 'dev'; return { name: 'inject-boot-version', transformIndexHtml(html) { return html.replace(/__BOOT_VERSION__/g, version); }, }; } /** * emitPolyfills writes the prebuilt core-js bundle to `dist/polyfills.js`. The index.html gate * script loads it via document.write only on an old engine that lacks the es2020+ runtime APIs the * app uses (globalThis, structuredClone, Array.at, …) — e.g. the Chromium 66 Android System WebView * behind the Telegram/VK in-app browser — before the deferred module runs. A modern engine never * requests it, so it adds nothing to that payload and stays out of the module graph the bundle-size * gate measures. */ function emitPolyfills(): Plugin { return { name: 'emit-polyfills', generateBundle() { const src = readFileSync(resolve(import.meta.dirname, 'node_modules/core-js-bundle/minified.js'), 'utf8'); this.emitFile({ type: 'asset', fileName: 'polyfills.js', source: src }); }, }; } // The edge Connect service is scrabble.edge.v1.Gateway; the gateway serves it over // h2c on :8081 by default. In dev we proxy the RPC path so the browser (which can // not speak h2c directly) talks to the dev server on the same origin. In `mock` // mode the app runs entirely against an in-memory fake transport — no gateway, // no backend, no Postgres — which is what `pnpm start` launches. const RPC_PREFIX = '/scrabble.edge.v1.Gateway'; export default defineConfig(({ mode }) => ({ // Relative asset base so the one build serves under any path — the gateway maps the // Telegram Mini App to /telegram/ (the hash router is path-agnostic). base: './', define: { // App version shown on the About screen, injected at build time from `git describe` // via a Docker build-arg. Falls back to "dev" for a plain local/mock build, // so a missing build-arg never breaks the build. __APP_VERSION__: JSON.stringify(process.env.VITE_APP_VERSION || 'dev'), }, // emitPolyfills ships dist/polyfills.js (loaded only by old engines; see its docstring + the // index.html boot guard). injectBootVersion stamps the app version into that guard's diagnostic. plugins: [svelte(), emitPolyfills(), injectBootVersion()], server: { port: 5173, proxy: mode === 'mock' ? undefined : { [RPC_PREFIX]: { target: process.env.GATEWAY_URL || 'http://localhost:8081', changeOrigin: true, }, }, }, build: { // Down-level to es2019 so an old Android System WebView (seen: Telegram/VK in-app WebView on // Chromium 66, Android 9) can parse the bundle — esbuild lowers es2020+ syntax (?., ??, private // fields, static blocks, …) that Chrome 66 rejects with "Unexpected token ?", which left the SPA // on a white screen. esbuild lowers syntax only, not runtime APIs — the es2020+ globals the // bundle/deps call at runtime (globalThis, structuredClone, Array.at, …) are covered separately // by the conditional core-js loader (emitPolyfills + the index.html gate), loaded only on an // engine that actually lacks them. target: 'es2019', // Emit sourcemaps everywhere except the production build. A shipped `.map` // carries full `sourcesContent` — the entire TypeScript/Svelte source — and the // gateway/landing images serve `dist/` verbatim, so production maps would expose // the whole client source at the edge. Dev and the `mock` e2e build // (`vite build --mode mock`) keep maps for debugging; the production `vite build` // (the Docker image build) drops them. sourcemap: mode !== 'production', // Two entries: the game SPA (index.html, served at /app/ + /telegram/) and the // public landing page (landing.html, served at /). Assets are shared in dist/assets/, and // the relative base lets one build serve under any path. rollupOptions: { input: { main: resolve(import.meta.dirname, 'index.html'), landing: resolve(import.meta.dirname, 'landing.html'), }, }, }, }));