Files
scrabble-game/ui/vite.config.ts
T
Ilia Denisov 4a0c296bbb
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
fix(ui): conditionally load core-js polyfills for old Android WebViews
The es2019 down-level fixed the parse error, but the Chromium 66 in-app WebView
(Android 9) then threw "Uncaught ReferenceError: globalThis is not defined" — esbuild
lowers syntax, not the es2020+ runtime globals the bundle and its deps reference
(globalThis, structuredClone, Array.at, Object.hasOwn, Promise.allSettled, …), so the
module still failed to evaluate and the SPA stayed a white screen.

Load core-js only where it is actually missing. A permanent ES5 gate in index.html
feature-detects the engine and, on an old one, document.writes a parser-blocking
<script src=polyfills.js> before the deferred module runs; a modern engine matches
none of the checks and downloads nothing. document.write is used deliberately — it is
the only way to inject a script guaranteed to run before a deferred module — with a
static literal argument (no interpolation, no injection surface). vite.config
emitPolyfills writes dist/polyfills.js from the prebuilt core-js-bundle and keeps it
out of the module graph, so the bundle-size budget is unaffected (app entry still
108.8 / 110 KB gzip).

pnpm-workspace denies core-js-bundle's install script (the package ships a prebuilt
minified.js we only read at build time; its script is a funding banner). svelte-check
and the mock e2e (Chromium + WebKit) are green and the size gate still passes. The
on-device re-test on the Chromium 66 emulator is pending.
2026-07-04 17:03:49 +02:00

103 lines
5.1 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { defineConfig, type Plugin } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
/**
* stripBootDiag removes the temporary on-device boot-diagnostic block from index.html — the span
* between the `<!-- BOOT-DIAG:START -->` and `<!-- BOOT-DIAG:END -->` markers. That block is a
* full-screen ES5 overlay meant only for the production build shipped to the test contour, so it
* is dropped from every non-production build: the `mock` e2e (whose first taps the overlay would
* intercept) and the dev server. It is wired only when mode !== 'production'. Remove this plugin
* together with the index.html block once the Android WebView diagnosis is finished — it must
* never reach master / production.
*/
function stripBootDiag(): Plugin {
return {
name: 'strip-boot-diag',
transformIndexHtml(html) {
return html.replace(/\s*<!-- BOOT-DIAG:START[\s\S]*?BOOT-DIAG:END -->/g, '');
},
};
}
/**
* 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 always ships dist/polyfills.js (loaded only by old engines; see its docstring
// and the index.html gate). The boot-diagnostic overlay ships only in the production build (the
// test contour) — it is stripped from the dev server and the mock e2e build, where a full-screen
// overlay would intercept Playwright's taps. See stripBootDiag / the index.html BOOT-DIAG block.
plugins: [svelte(), emitPolyfills(), ...(mode !== 'production' ? [stripBootDiag()] : [])],
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'),
},
},
},
}));