625fc3135a
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m9s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
The offer is the legal document a purchase accepts, needed before real-money intake goes live. Convert the source PDF to an editable ui/legal/offer_ru.md and render it to a standalone static dist/offer/index.html at build (a vite emit-offer plugin using marked); the landing container serves it at /offer/, with a bare /offer redirecting in. Add a small centered "Публичная оферта" footer link on the landing (ru/en) and a CI probe asserting /offer/ serves the page rather than silently falling through to the landing shell.
143 lines
6.5 KiB
TypeScript
143 lines
6.5 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';
|
|
import { VitePWA } from 'vite-plugin-pwa';
|
|
import { renderOfferHtml } from './src/lib/offer';
|
|
|
|
/**
|
|
* 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 });
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* emitOffer renders the public offer markdown (legal/offer_ru.md) to a standalone
|
|
* static page and emits it as dist/offer/index.html, which the landing container
|
|
* serves at /offer/ (deploy/landing/Caddyfile). The markdown is the owner-editable
|
|
* source of truth, so the page is regenerated from it on every build.
|
|
*/
|
|
function emitOffer(): Plugin {
|
|
return {
|
|
name: 'emit-offer',
|
|
generateBundle() {
|
|
const md = readFileSync(resolve(import.meta.dirname, 'legal/offer_ru.md'), 'utf8');
|
|
this.emitFile({ type: 'asset', fileName: 'offer/index.html', source: renderOfferHtml(md) });
|
|
},
|
|
};
|
|
}
|
|
|
|
// 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(),
|
|
emitOffer(),
|
|
injectBootVersion(),
|
|
// App-shell precache for the offline mode: a custom (injectManifest) service worker precaches
|
|
// index.html + the hashed assets so the installed web PWA cold-launches with no network. It
|
|
// supersedes the former install-only public/sw.js and outputs to dist/sw.js (same path, so the
|
|
// existing /app/ registration is unchanged). Registration is manual + web-only (injectRegister
|
|
// off; see pwa.svelte.ts) and the whole plugin is disabled in the mock build so Playwright is
|
|
// never perturbed. manifest:false — we already ship public/manifest.webmanifest.
|
|
VitePWA({
|
|
strategies: 'injectManifest',
|
|
srcDir: 'src',
|
|
filename: 'sw.ts',
|
|
injectRegister: false,
|
|
manifest: false,
|
|
disable: mode === 'mock',
|
|
injectManifest: {
|
|
// The app shell only: exclude the separate landing page and the conditional polyfill bundle
|
|
// (loaded document.write-only on an old engine; never part of the offline shell).
|
|
globPatterns: ['**/*.{js,css,html,svg,png,ico,woff,woff2}'],
|
|
globIgnores: ['**/landing*', '**/polyfills.js'],
|
|
},
|
|
devOptions: { enabled: false },
|
|
}),
|
|
],
|
|
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'),
|
|
},
|
|
},
|
|
},
|
|
}));
|