mirror of
https://github.com/tiennm99/loto.git
synced 2026-10-13 02:14:42 +00:00
build(web): upgrade to SvelteKit 3 and adapter-static 4, move the PWA to vite-plugin-pwa
This commit is contained in:
1 parent
28825b78e4
commit
124d65f800
22 files changed
+310
-217
No files matched your search
@@ -71,6 +71,15 @@ hand if any is wanted in the closed test. See
|
||||
|
||||
### PWA install verification (manual, post-deploy)
|
||||
|
||||
**Priority after the SvelteKit 3 upgrade (2026-10-12).** Before the
|
||||
upgrade, the Pages build's `sw.js` precached `/` and bound navigations to
|
||||
`/`. Both point at the origin root, which returns 404 on Pages, so the
|
||||
service worker most likely never installed there. The new build (plain
|
||||
`vite-plugin-pwa`, replacing `@vite-pwa/sveltekit`, which breaks under
|
||||
Kit 3) uses `/loto/`. On Pages, confirm that the service worker installs,
|
||||
that the app works offline, and that the update banner appears after the
|
||||
next deploy.
|
||||
|
||||
Needs production deploy on GitHub Pages + physical Android Chrome +
|
||||
iOS Safari. No code; verification only.
|
||||
|
||||
@@ -153,14 +162,6 @@ iOS Safari. No code; verification only.
|
||||
so every draw re-runs all of them. If profiling shows jank, the cheap
|
||||
fix is a `MasterCell(num, order, isLast)` composable so only the two
|
||||
changed cells recompose per draw.
|
||||
- **`web/package.json` overrides** (re-checked 2026-10-12).
|
||||
`cookie`: still needed — Kit 2.70.x still declares `cookie ^0.6.0`,
|
||||
below the 0.7.0 fix. Kit 3.0.1 (now `latest`) declares `^2.0.1`, so
|
||||
the override goes away with a Kit 3 migration (a major upgrade, not
|
||||
done). `serialize-javascript`: dropped 2026-10-12 — redundant — the consumer
|
||||
(`@rollup/plugin-terser`, via `workbox-build`) declares `^7.0.3`, which
|
||||
already resolves to 7.1.x; the `^7.0.5` override only guards a lockfile
|
||||
that pins 7.0.3/7.0.4.
|
||||
- **Voice list growth.** If we add voices > 2 (esp. > 10), revisit
|
||||
the precache strategy — currently we precache only the default
|
||||
voice. The 7d runtime cache covers the rest, but cold-start cost
|
||||
|
||||
@@ -56,6 +56,8 @@ npm run dev # dev server at http://localhost:5173
|
||||
| `npm run preview` | Preview production build locally |
|
||||
| `npm test` | Run unit tests |
|
||||
| `npm run lint` | ESLint check |
|
||||
| `npm run check` | svelte-check type check (JSDoc) |
|
||||
| `npm run verify:pwa` | Check the service worker in `build/` (after a build, same `BUILD_PROFILE`) |
|
||||
|
||||
---
|
||||
|
||||
@@ -97,6 +99,37 @@ scripts/
|
||||
```
|
||||
|
||||
Game state lives entirely in Svelte 5 rune stores; no server round-trips.
|
||||
|
||||
### Build config
|
||||
|
||||
- **No `svelte.config.js`.** SvelteKit 3 takes its options (adapter-static,
|
||||
`paths.base` from `base-path.js`) as arguments to the `sveltekit()` plugin
|
||||
in `vite.config.js`.
|
||||
- **`#lib/*` imports.** `src/lib` is reached through the `imports` field in
|
||||
`package.json` (`import { settings } from "#lib/settings-store.svelte.js"`),
|
||||
always with the file extension. `jsconfig.json` extends `$app/tsconfig`.
|
||||
- **Base-path-safe URLs.** Static files go through `asset()` from
|
||||
`$app/paths` (no leading `/`: `asset("audio/…")`), so they resolve under
|
||||
both `""` and `/loto`.
|
||||
|
||||
### PWA
|
||||
|
||||
The service worker comes from
|
||||
[vite-plugin-pwa](https://vite-pwa-org.netlify.app/) (`generateSW`,
|
||||
`registerType: "prompt"`); the manifest is the hand-written
|
||||
`static/manifest.webmanifest`. `+layout.svelte` registers it through
|
||||
`virtual:pwa-register` and wires `onNeedRefresh` to the update banner in
|
||||
`src/lib/update-prompt.svelte.js`. It precaches the app shell plus the default
|
||||
voice; other voices are cached on first play.
|
||||
|
||||
`@vite-pwa/sveltekit` is not used: it generates `sw.js` when Vite's SSR build
|
||||
closes, which assumed SvelteKit 2's build order. SvelteKit 3 builds SSR first,
|
||||
then the client, then prerenders, all inside its `buildApp` hook, so
|
||||
`vite.config.js` drops vite-plugin-pwa's own build step and calls its
|
||||
`generateSW()` from a `buildApp` hook that runs after SvelteKit's and before
|
||||
the adapter copies `.svelte-kit/output` into `build/`.
|
||||
`npm run verify:pwa` checks the result after a build (run it with the same
|
||||
`BUILD_PROFILE`).
|
||||
Audio manifest is generated once at build prep time — the app reads a JSON manifest
|
||||
on load and plays the appropriate clip for each drawn number.
|
||||
|
||||
|
||||
+12
-7
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* SvelteKit `paths.base` resolution, shared between `svelte.config.js`
|
||||
* (page routing / prerendering) and `vite.config.js` (PWA precache
|
||||
* manifest). Both consumers MUST agree on the base or the service
|
||||
* worker's `additionalManifestEntries` end up origin-rooted while the
|
||||
* app itself serves under a sub-path (e.g. GitHub Pages `/loto`),
|
||||
* SvelteKit `paths.base` resolution, shared between the `sveltekit()`
|
||||
* plugin options (page routing / prerendering) and the PWA precache
|
||||
* manifest, both in `vite.config.js`. Both consumers MUST agree on the
|
||||
* base or the service worker's `additionalManifestEntries` end up
|
||||
* origin-rooted while the app itself serves under a sub-path (e.g. GitHub Pages `/loto`),
|
||||
* causing every precached URL to 404 and the SW install to fail.
|
||||
*
|
||||
* Resolution order:
|
||||
@@ -16,9 +16,14 @@
|
||||
*/
|
||||
import { loadEnv } from "vite";
|
||||
|
||||
/** @returns {string} */
|
||||
/**
|
||||
* SvelteKit validates the shape (leading `/`, no trailing `/`) when it loads
|
||||
* its config, so a malformed NEXT_BASE_PATH fails the build there.
|
||||
* @returns {"" | `/${string}`}
|
||||
*/
|
||||
export function resolveBase() {
|
||||
if (process.env.NEXT_BASE_PATH != null) return process.env.NEXT_BASE_PATH;
|
||||
if (process.env.NEXT_BASE_PATH != null)
|
||||
return /** @type {"" | `/${string}`} */ (process.env.NEXT_BASE_PATH);
|
||||
if (process.env.VITE_DEV_PROFILE === "codeserver") {
|
||||
// .env.local lives outside process.env at config-eval time; loadEnv reads it.
|
||||
const env = loadEnv(
|
||||
|
||||
@@ -127,19 +127,19 @@ function randomANumberInRow(weights) {
|
||||
|
||||
## Internal Navigation
|
||||
|
||||
- Use `import { base } from '$app/paths'` for internal links/asset URLs to preserve basePath across deployments.
|
||||
- Example: `<img src="{base}/icon.svg">` resolves on both root (`""`) and subpath (`/loto`) builds.
|
||||
- Use `asset()` (static files) and `resolve()` (routes) from `$app/paths` to preserve basePath across deployments. Their arguments don't start with `/` (route IDs passed to `resolve()` are the exception).
|
||||
- Example: `<img src={asset("icons/icon-192.png")}>` resolves on both root (`""`) and subpath (`/loto`) builds.
|
||||
|
||||
## Import Organization
|
||||
|
||||
1. SvelteKit imports (`$app/paths`, `$lib/...`)
|
||||
1. SvelteKit and app-library imports (`$app/paths`, `#lib/...`, always with the file extension)
|
||||
2. Third-party imports
|
||||
3. Local component/utility imports
|
||||
|
||||
```js
|
||||
import { base } from "$app/paths";
|
||||
import { generateGrid, isRowComplete } from "$lib/game-logic.js";
|
||||
import PlayerBoard from "$lib/PlayerBoard.svelte";
|
||||
import { asset } from "$app/paths";
|
||||
import { generateGrid, isRowComplete } from "#lib/game-logic.js";
|
||||
import PlayerBoard from "#lib/PlayerBoard.svelte";
|
||||
```
|
||||
|
||||
## Component Patterns
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
| `src/lib/active-tab.svelte.js` | Single-active-tab coordinator via `BroadcastChannel`. New tab broadcasts `claim`; old tab sets `activeTab.inactive = true`. `+layout.svelte` mounts `watchActiveTab()` on boot and renders a fullscreen overlay banner ("Phiên Lô tô đang chạy ở tab khác. Nhấn để tiếp tục tại đây.") when inactive — nhấn calls `claimActiveTab()` which broadcasts back, inactivating the other tab in turn. Soft coordination only (cooperating tabs); no-op in browsers without `BroadcastChannel` (legacy iOS Safari ≤15.4). Prevents double auto-call intervals, double localStorage writers, and overlapping audio across tabs. |
|
||||
| `src/lib/player-auto-cross.js` | Pure `applyMasterCalls({grid, crossed, called, lastHandledIndex, manualUnticks, mode})`. Cursor-by-index dedup (vs the retired `at`-timestamp model), so any caller can pass `lastHandledIndex: 0` to replay master's full history (used by player regen + "Xoá đánh dấu" in both mode). Manual unticks suppress re-cross on replay. |
|
||||
| `src/lib/vietnamese-number.js` | `numberToVietnamese(n)` — pure utility mapping 0..90 to spoken Vietnamese, with tonal exceptions (15 → "mười lăm", 21 → "hai mươi mốt", 25 → "hai mươi lăm"). Out-of-range falls back to `String(n)`. |
|
||||
| `src/lib/voice.js` | Bundled-MP3 playback. Exports `playNumber(n)`, `playWaiting(n)` (sequences cho + N), `playBingo()`, `cancelPlayback()`. Lazy `<audio>` cache, cancel-then-play, token-based cancel ensures stale promises can't resume after a new event. Reads active voice from `settings.voice`; URLs go through `import { base } from "$app/paths"` for basePath safety. |
|
||||
| `src/lib/voice.js` | Bundled-MP3 playback. Exports `playNumber(n)`, `playWaiting(n)` (sequences cho + N), `playBingo()`, `cancelPlayback()`. Lazy `<audio>` cache, cancel-then-play, token-based cancel ensures stale promises can't resume after a new event. Reads active voice from `settings.voice`; URLs go through `asset()` from `$app/paths` for basePath safety. |
|
||||
| `src/lib/audio-manifest.js` | Re-exports `static/audio/manifest.json` as `VOICES` (array) + `VOICE_IDS` (Set) + `DEFAULT_VOICE`. Manifest is generated by `scripts/generate-audio.py`. |
|
||||
| `src/lib/settings-store.svelte.js` | Reactive global UI settings via Svelte 5 runes. Stores 9 keys: `theme` (enum: "auto" / "light" / "dark"), `mode` (enum: "player" / "master" / "both"; replaces legacy `masterMode`), `autoCallEnabled` (bool), `autoCallSpeed` (1–10), `emptyCellColor` (hex), `voiceEnabledMaster` (bool), `voiceEnabledPlayer` (bool), `voiceWaitingNumber` (bool), `voice` (string id matching audio manifest). Persisted to localStorage `loto_settings`. Pushes values to CSS vars and `<html class="dark">` on `:root`. Per-key validators preserve old data. One-shot migration: `masterMode: true` → `mode: "both"`. |
|
||||
|
||||
@@ -47,21 +47,20 @@
|
||||
### Configuration & PWA
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `svelte.config.js` | adapter-static (HTML export), dual basePath via BUILD_PROFILE env, SvelteKit PWA plugin config. |
|
||||
| `vite.config.js` | Tailwind + SvelteKit + PWA plugins. codeserver HMR config (port, allowedHosts, hmr). |
|
||||
| `package.json` | SvelteKit 2, Svelte 5 (runes), Tailwind 4, Vite, @vite-pwa/sveltekit. Scripts: dev, dev:codeserver, build, build:gh, lint, test, test:watch. |
|
||||
| `vite.config.js` | Tailwind + SvelteKit + PWA plugins. SvelteKit options (adapter-static, dual basePath via `base-path.js`) go to the `sveltekit()` plugin; there is no `svelte.config.js`. vite-plugin-pwa generates `sw.js` from a `buildApp` hook. codeserver HMR config (port, allowedHosts, hmr). |
|
||||
| `package.json` | SvelteKit 3, Svelte 5 (runes), Tailwind 4, Vite, vite-plugin-pwa. `imports` maps `#lib/*` to `src/lib/*`. Scripts: dev, dev:codeserver, build, build:gh, lint, check, verify:pwa, test, test:watch. |
|
||||
| `.github/workflows/ci.yml` | Repo-root pipeline for push/PR. `test` (`npm test`) gates a two-profile `build` matrix; the artifacts feed `deploy-pages` (canonical, `https://tiennm99.github.io/loto/`), `deploy-firebase`, `preview-firebase`, and `android-debug`. |
|
||||
| `.github/actions/setup-web/` | Composite action: Node 24 + `npm ci` in `web/`. Shared by `ci.yml` and `android-release.yml`. |
|
||||
| `eslint.config.mjs` | ESLint 9 flat config (@eslint/js + eslint-plugin-svelte). Declares Svelte 5 rune globals. |
|
||||
| `jsconfig.json` | Path alias `$lib`, no checkJs. |
|
||||
| `jsconfig.json` | Extends `$app/tsconfig`; checkJs on for `src/` and `vite.config.js`. |
|
||||
| `.gitignore` | Excludes node_modules, build, .env.local, etc. |
|
||||
| `.env.example` | codeserver profile vars (CODESERVER_HOST, CODESERVER_PORT). |
|
||||
| `static/manifest.webmanifest` | PWA manifest: name, short_name, icons (192/512px), theme colors, display: standalone, scope, start_url. |
|
||||
| `static/icons/{192,512}.png` | PWA icons (auto-generated by @vite-pwa/sveltekit from source). |
|
||||
| `static/icons/{192,512}.png` | PWA icons, rendered from `static/icons/source*.svg` (see README). |
|
||||
| `static/audio/{voiceId}/*.mp3` | Pre-generated Vietnamese voice clips (1–90 + cho + kinh per voice). Generated by `scripts/generate-audio.py`. SW caches at runtime (CacheFirst). |
|
||||
| `static/audio/manifest.json` | Voice list `{ id, edgeName, label, gender }[]`. |
|
||||
| `scripts/generate-audio.py` | One-shot Python script: generates 92 clips per voice, writes manifest. |
|
||||
| Service Worker (auto) | @vite-pwa/sveltekit generates `/pwa-manifest.json`, `/sw.js`. App shell precache ~353 KB. Auto-update mode with reload toast. |
|
||||
| Service Worker (auto) | vite-plugin-pwa generates `/sw.js` (the manifest is the hand-written `static/manifest.webmanifest`). Precaches the app shell and the default voice. Prompt mode with an update banner. |
|
||||
|
||||
## Key Data Structures
|
||||
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
| `npm run build` | `""` (root) | Local preview / generic static host |
|
||||
| `npm run build:gh` | `/loto` | GitHub Pages → `https://tiennm99.github.io/loto` (canonical) |
|
||||
|
||||
Implementation: `svelte.config.js` reads `BUILD_PROFILE` env. Default is empty
|
||||
Implementation: `base-path.js` reads `BUILD_PROFILE` env and `vite.config.js`
|
||||
passes the result to the `sveltekit()` plugin's `paths.base`. Default is empty
|
||||
basePath; `BUILD_PROFILE=gh npm run build` switches to `/loto`.
|
||||
|
||||
Internal links use `import { base } from '$app/paths'` so they survive
|
||||
either profile without code changes.
|
||||
Static-file URLs go through `asset()` and route links through `resolve()`
|
||||
from `$app/paths`, so they survive either profile without code changes.
|
||||
|
||||
## Production Deployment — GitHub Pages
|
||||
|
||||
@@ -98,7 +99,7 @@ Generates:
|
||||
- `.svelte-kit/` — Build cache (not needed for deployment)
|
||||
|
||||
### Export Settings
|
||||
- `adapter-static` in `svelte.config.js`
|
||||
- `adapter-static`, passed to the `sveltekit()` plugin in `vite.config.js`
|
||||
- No server-side rendering (SSR disabled via `ssr: false`)
|
||||
- All pages pre-rendered to HTML + JS bundles
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ That format is intentionally out of scope.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: SvelteKit 2 with Svelte 5 (runes mode)
|
||||
- **Framework**: SvelteKit 3 with Svelte 5 (runes mode)
|
||||
- **Runtime**: Svelte 5 runes ($state, $derived, $effect, $props)
|
||||
- **Styling**: Tailwind CSS 4 (utility-first, animations)
|
||||
- **Persistence**: localStorage (no backend)
|
||||
|
||||
@@ -174,7 +174,7 @@ Each draw creates a new object (even repeat numbers) to ensure reactive re-fire.
|
||||
|
||||
**Static Storage**: All game state in localStorage. No API calls. Fully functional offline.
|
||||
|
||||
**Service Worker**: @vite-pwa/sveltekit auto-generates `/sw.js` with:
|
||||
**Service Worker**: vite-plugin-pwa generates `/sw.js` (wired into the SvelteKit build in `vite.config.js`) with:
|
||||
- App shell precache (~353 KB): HTML, CSS, JS bundles for instant load
|
||||
- Audio runtime caching: Voice clips cached on-demand (CacheFirst strategy)
|
||||
- Navigation: NetworkFirst (try network, fallback to cache)
|
||||
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"extends": "$app/tsconfig",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true
|
||||
}
|
||||
},
|
||||
"include": ["src", "vite.config.js"]
|
||||
}
|
||||
Generated
+57
-106
@@ -10,19 +10,19 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10",
|
||||
"@fontsource/roboto-condensed": "^5.3.0",
|
||||
"@sveltejs/adapter-static": "^3",
|
||||
"@sveltejs/kit": "^2",
|
||||
"@sveltejs/adapter-static": "^4",
|
||||
"@sveltejs/kit": "^3",
|
||||
"@sveltejs/vite-plugin-svelte": "^7",
|
||||
"@tailwindcss/vite": "^4",
|
||||
"@vite-pwa/sveltekit": "^1.1.0",
|
||||
"eslint": "^10",
|
||||
"eslint-plugin-svelte": "^3",
|
||||
"globals": "^17",
|
||||
"happy-dom": "20.11.0",
|
||||
"svelte": "^5",
|
||||
"svelte": "^5.57.1",
|
||||
"svelte-check": "^4.7.6",
|
||||
"tailwindcss": "^4",
|
||||
"vite": "^8.1.5",
|
||||
"vite-plugin-pwa": "^1",
|
||||
"vitest": "^4.1.11"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
@@ -1879,9 +1879,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
|
||||
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2670,47 +2670,43 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/adapter-static": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz",
|
||||
"integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-4.0.0.tgz",
|
||||
"integrity": "sha512-aF1J23R1ebKqUxQ4/qLr/Lxu63NFmLAtg8UZl3HFLxaoA1D/nhTVGcD9lTqx65iUbjNR2iOL0TgOxSX82x6Paw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@sveltejs/kit": "^2.0.0"
|
||||
"@sveltejs/kit": "^3.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.70.2",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.2.tgz",
|
||||
"integrity": "sha512-RzRoRpuR2KXqc5yMO0akQHDZeT4AslOlznGITURsqHaVbtyYP4Wn3eE3gxj9JcDyNYO0crkxhdwFHc+2vkVm6w==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-3.0.1.tgz",
|
||||
"integrity": "sha512-8JMCCq7ZUJELi3hbhb0+c9Rs8qnbHW90vhOfM+t1FSF4fhIpG9/FOsH4IYgcITMhbdkSrrn+Kvd9G+q8A+UO6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.9",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"acorn": "^8.16.0",
|
||||
"cookie": "^0.6.0",
|
||||
"devalue": "^5.8.1",
|
||||
"@rolldown/pluginutils": "^1.0.1",
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"cookie": "^2.0.1",
|
||||
"devalue": "^5.9.4",
|
||||
"esm-env": "^1.2.2",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.30.5",
|
||||
"mrmime": "^2.0.0",
|
||||
"set-cookie-parser": "^3.0.0",
|
||||
"sirv": "^3.0.0"
|
||||
"magic-string": "^1.2.3",
|
||||
"mrmime": "^2.0.1",
|
||||
"sirv": "^3.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"svelte-kit": "svelte-kit.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.13"
|
||||
"node": ">=22.17"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0",
|
||||
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
||||
"typescript": "^5.3.3 || ^6.0.0",
|
||||
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0"
|
||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||
"svelte": "^5.57.1",
|
||||
"typescript": "^6.0.0",
|
||||
"vite": "^8.0.12"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
@@ -2721,6 +2717,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit/node_modules/magic-string": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz",
|
||||
"integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/load-config": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.3.tgz",
|
||||
@@ -3060,13 +3066,6 @@
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
@@ -3136,33 +3135,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vite-pwa/sveltekit": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vite-pwa/sveltekit/-/sveltekit-1.1.0.tgz",
|
||||
"integrity": "sha512-mMIf2tY+7Hg8jecpu/WY+Ki2ikoXy3hVmt3tOxi0K+lYYnKQrDYthuHireI0S+26Mg9BXzL7qQF1xeB5VYlYlg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"kolorist": "^1.8.0",
|
||||
"tinyglobby": "^0.2.9",
|
||||
"vite-plugin-pwa": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@sveltejs/kit": "^1.3.1 || ^2.0.1",
|
||||
"@vite-pwa/assets-generator": "^1.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vite-pwa/assets-generator": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
|
||||
@@ -3702,13 +3674,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz",
|
||||
"integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
"node": ">=22"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
@@ -4338,9 +4314,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.4.tgz",
|
||||
"integrity": "sha512-CHnJXZDfBeOAjLIGNy/0BEsmDu5+irHgUXRW9pGptxCbO3uyjJ98crilzFgiBqizW94UodjX3GAPLg/dhEnUfA==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.4.0.tgz",
|
||||
"integrity": "sha512-CQPcneEHw/czj32uwQC52nYnax8H4SXt25FCM+LIwGZ49cMIxpOj3bP8wmzcI+Z0HIy5KkClaPhRLfCySzwHPA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5586,16 +5562,6 @@
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/known-css-properties": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz",
|
||||
@@ -5603,13 +5569,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/kolorist": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
|
||||
"integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
@@ -6787,13 +6746,6 @@
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz",
|
||||
"integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
@@ -7183,24 +7135,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.56.9",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.9.tgz",
|
||||
"integrity": "sha512-VT8kSnlEg8069w7AiCcAk3Yf5xvMnrGTagVOmU/OpOLHaHnNqXhWZCH/4EVga/bT/HtWhvE6/fHrXLErx7OnJA==",
|
||||
"version": "5.57.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.57.2.tgz",
|
||||
"integrity": "sha512-1YiPCAXT0Ct6g1vOMmSLa0Nsc3QRMvtDF32ihgglGVrugIuIK38RyDiFNXVDhtQ81TJjSO57NdMw6DyYLqSGDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.10",
|
||||
"@types/estree": "^1.0.5",
|
||||
"@types/trusted-types": "^2.0.7",
|
||||
"acorn": "^8.12.1",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"@jridgewell/sourcemap-codec": "^1.6.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.13",
|
||||
"@types/estree": "^1.0.9",
|
||||
"acorn": "^8.18.0",
|
||||
"aria-query": "5.3.1",
|
||||
"axobject-query": "^4.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"devalue": "^5.8.1",
|
||||
"devalue": "^5.9.2",
|
||||
"esm-env": "^1.2.1",
|
||||
"esrap": "^2.2.12",
|
||||
"esrap": "^2.3.6",
|
||||
"is-reference": "^3.0.3",
|
||||
"locate-character": "^3.0.0",
|
||||
"magic-string": "^0.30.11",
|
||||
|
||||
+7
-7
@@ -3,6 +3,9 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#lib/*": "./src/lib/*"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"dev:codeserver": "VITE_DEV_PROFILE=codeserver vite dev --host 0.0.0.0",
|
||||
@@ -19,25 +22,22 @@
|
||||
"optionalDependencies": {
|
||||
"lightningcss-linux-x64-gnu": "1.33.0"
|
||||
},
|
||||
"overrides": {
|
||||
"cookie": "^0.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10",
|
||||
"@fontsource/roboto-condensed": "^5.3.0",
|
||||
"@sveltejs/adapter-static": "^3",
|
||||
"@sveltejs/kit": "^2",
|
||||
"@sveltejs/adapter-static": "^4",
|
||||
"@sveltejs/kit": "^3",
|
||||
"@sveltejs/vite-plugin-svelte": "^7",
|
||||
"@tailwindcss/vite": "^4",
|
||||
"@vite-pwa/sveltekit": "^1.1.0",
|
||||
"eslint": "^10",
|
||||
"eslint-plugin-svelte": "^3",
|
||||
"globals": "^17",
|
||||
"happy-dom": "20.11.0",
|
||||
"svelte": "^5",
|
||||
"svelte": "^5.57.1",
|
||||
"svelte-check": "^4.7.6",
|
||||
"tailwindcss": "^4",
|
||||
"vite": "^8.1.5",
|
||||
"vite-plugin-pwa": "^1",
|
||||
"vitest": "^4.1.11"
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,8 @@
|
||||
/**
|
||||
* Build-time regression guard for the PWA layer.
|
||||
*
|
||||
* `adapter-static` renders page HTML during `adapt()`, which runs AFTER the
|
||||
* PWA plugin's `closeBundle` — so `@vite-pwa/sveltekit` never gets to inject
|
||||
* its own registration tag into the built HTML, and a plain grep on
|
||||
* SvelteKit renders page HTML itself, so vite-plugin-pwa never injects its
|
||||
* own registration tag into the built HTML, and a plain grep on
|
||||
* `build/index.html` proves nothing either way. `+layout.svelte` registers
|
||||
* the service worker itself via a dynamically-imported `virtual:pwa-register`
|
||||
* chunk instead; this script asserts that wiring survived the build rather
|
||||
@@ -13,7 +12,10 @@
|
||||
* Also asserts the audio precache manifest in `build/sw.js` is prefixed
|
||||
* with the SAME base path SvelteKit itself resolved for this build — a
|
||||
* mismatch here 404s every precached clip and fails the SW `install` event
|
||||
* on that origin (this exact bug shipped once already).
|
||||
* on that origin (this exact bug shipped once already). The same goes for
|
||||
* the prerendered index page, which is also the navigation fallback: it is
|
||||
* precached at the base URL only if `sw.js` was generated after SvelteKit
|
||||
* prerendered (see `svelteKitPWA` in vite.config.js).
|
||||
*
|
||||
* Usage: run AFTER `npm run build` / `npm run build:gh` (see package.json's
|
||||
* `verify:pwa` script), with the same `BUILD_PROFILE`/`NEXT_BASE_PATH` env
|
||||
@@ -90,12 +92,13 @@ if (!registerKey) {
|
||||
fail("No client chunk dynamically imports virtual:pwa-register.");
|
||||
}
|
||||
}
|
||||
if (!jsFiles.some((f) => readFileSync(f, "utf8").includes("sw.js"))) {
|
||||
fail(`No built JS under ${appDir} references "sw.js" — nothing registers it.`);
|
||||
const base = resolveBase();
|
||||
const swUrl = `${base}/sw.js`;
|
||||
if (!jsFiles.some((f) => readFileSync(f, "utf8").includes(swUrl))) {
|
||||
fail(`No built JS under ${appDir} references "${swUrl}" — nothing registers it.`);
|
||||
}
|
||||
|
||||
// --- H1: precached audio entries carry the resolved base path -----------
|
||||
const base = resolveBase();
|
||||
// --- precached audio entries carry the resolved base path ---------------
|
||||
const audioUrls = [...swSrc.matchAll(/"([^"]*\/audio\/[^"]*\.mp3)"/g)].map(
|
||||
(m) => m[1],
|
||||
);
|
||||
@@ -107,12 +110,24 @@ if (audioUrls.length === 0) {
|
||||
fail(
|
||||
`${badUrls.length}/${audioUrls.length} precached audio URL(s) don't ` +
|
||||
`start with "${base}/audio/" (resolved base "${base}") — e.g. "${badUrls[0]}". ` +
|
||||
"additionalManifestEntries in vite.config.js and svelte.config.js's " +
|
||||
"paths.base have drifted apart again.",
|
||||
"additionalManifestEntries and the sveltekit() paths.base in " +
|
||||
"vite.config.js have drifted apart again.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- the app shell is precached at the base URL and serves navigations ---
|
||||
const indexUrl = `${base}/`;
|
||||
if (!swSrc.includes(`{url:"${indexUrl}",revision:`)) {
|
||||
fail(
|
||||
`${swPath} doesn't precache the index page at "${indexUrl}" — the service ` +
|
||||
"worker was generated before prerendering, or with the wrong base.",
|
||||
);
|
||||
}
|
||||
if (!swSrc.includes(`createHandlerBoundToURL("${indexUrl}")`)) {
|
||||
fail(`${swPath} doesn't route navigations to "${indexUrl}".`);
|
||||
}
|
||||
|
||||
if (!ok) process.exit(1);
|
||||
console.log(
|
||||
`✓ PWA build check passed — base "${base}", ${jsFiles.length} client JS files, ` +
|
||||
|
||||
@@ -32,17 +32,17 @@
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import AutoCountdown from "$lib/AutoCountdown.svelte";
|
||||
import AutoCountdown from "#lib/AutoCountdown.svelte";
|
||||
import {
|
||||
drawNext,
|
||||
masterState,
|
||||
saveMaster,
|
||||
startNewGame,
|
||||
} from "$lib/master-store.svelte.js";
|
||||
import MasterEmptyState from "$lib/MasterEmptyState.svelte";
|
||||
import { settings } from "$lib/settings-store.svelte.js";
|
||||
import { cancelPlayback, playNumber, unlockAudio } from "$lib/voice.js";
|
||||
import { setWakeLock } from "$lib/wake-lock.js";
|
||||
} from "#lib/master-store.svelte.js";
|
||||
import MasterEmptyState from "#lib/MasterEmptyState.svelte";
|
||||
import { settings } from "#lib/settings-store.svelte.js";
|
||||
import { cancelPlayback, playNumber, unlockAudio } from "#lib/voice.js";
|
||||
import { setWakeLock } from "#lib/wake-lock.js";
|
||||
|
||||
let heroEl = $state(/** @type {HTMLDivElement | null} */ (null));
|
||||
let autoRunning = $state(false);
|
||||
|
||||
@@ -11,18 +11,18 @@
|
||||
saveCrossedState,
|
||||
saveGrid,
|
||||
saveManualUnticks,
|
||||
} from "$lib/game-logic.js";
|
||||
import { focusTrap } from "$lib/focus-trap.js";
|
||||
import { masterState } from "$lib/master-store.svelte.js";
|
||||
import { pushOverlay } from "$lib/overlay-history.js";
|
||||
import { applyMasterCalls } from "$lib/player-auto-cross.js";
|
||||
import { settings } from "$lib/settings-store.svelte.js";
|
||||
} from "#lib/game-logic.js";
|
||||
import { focusTrap } from "#lib/focus-trap.js";
|
||||
import { masterState } from "#lib/master-store.svelte.js";
|
||||
import { pushOverlay } from "#lib/overlay-history.js";
|
||||
import { applyMasterCalls } from "#lib/player-auto-cross.js";
|
||||
import { settings } from "#lib/settings-store.svelte.js";
|
||||
import {
|
||||
cancelPlayback,
|
||||
playBingo,
|
||||
playWaiting,
|
||||
unlockAudio,
|
||||
} from "$lib/voice.js";
|
||||
} from "#lib/voice.js";
|
||||
|
||||
const STORAGE_PREFIX = "loto";
|
||||
const PREVIEW_CELLS = [...Array(27).keys()];
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script>
|
||||
import { VOICES } from "$lib/audio-manifest.js";
|
||||
import { focusTrap } from "$lib/focus-trap.js";
|
||||
import { pushOverlay } from "$lib/overlay-history.js";
|
||||
import { VOICES } from "#lib/audio-manifest.js";
|
||||
import { focusTrap } from "#lib/focus-trap.js";
|
||||
import { pushOverlay } from "#lib/overlay-history.js";
|
||||
import {
|
||||
BOARD_TEXT_SCALES,
|
||||
resetSettings,
|
||||
saveSettings,
|
||||
settings,
|
||||
} from "$lib/settings-store.svelte.js";
|
||||
import { clearAudioCache } from "$lib/voice.js";
|
||||
} from "#lib/settings-store.svelte.js";
|
||||
import { clearAudioCache } from "#lib/voice.js";
|
||||
|
||||
let open = $state(false);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* @module lib/player-auto-cross
|
||||
*/
|
||||
|
||||
import { findUncrossedCell } from "$lib/game-logic.js";
|
||||
import { findUncrossedCell } from "#lib/game-logic.js";
|
||||
|
||||
/**
|
||||
* @param {object} args
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* @module lib/settings-store
|
||||
*/
|
||||
|
||||
import { DEFAULT_VOICE, VOICE_IDS } from "$lib/audio-manifest.js";
|
||||
import { DEFAULT_VOICE, VOICE_IDS } from "#lib/audio-manifest.js";
|
||||
|
||||
const STORAGE_KEY = "loto_settings";
|
||||
const HEX6 = /^#[0-9a-fA-F]{6}$/;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* and are picked by `settings.voice`.
|
||||
* @module lib/voice
|
||||
*/
|
||||
import { base } from "$app/paths";
|
||||
import { settings } from "$lib/settings-store.svelte.js";
|
||||
import { asset } from "$app/paths";
|
||||
import { settings } from "#lib/settings-store.svelte.js";
|
||||
|
||||
/**
|
||||
* Single reusable <audio> element, `.src` swapped per clip. iOS Safari
|
||||
@@ -76,7 +76,10 @@ export function unlockAudio() {
|
||||
function clipUrl(name) {
|
||||
// `settings.voice` is allowlist-validated and `name` is internal-only,
|
||||
// but encode anyway as defense-in-depth in case either ever escapes.
|
||||
return `${base}/audio/${encodeURIComponent(settings.voice)}/${encodeURIComponent(name)}.mp3`;
|
||||
// `asset()` is typed against the literal file list in /static, which a
|
||||
// runtime-composed path can't match, hence the cast.
|
||||
const file = `audio/${encodeURIComponent(settings.voice)}/${encodeURIComponent(name)}.mp3`;
|
||||
return asset(/** @type {Parameters<typeof asset>[0]} */ (file));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import "../app.css";
|
||||
import { loadMaster } from "$lib/master-store.svelte.js";
|
||||
import { loadSettings } from "$lib/settings-store.svelte.js";
|
||||
import { loadMaster } from "#lib/master-store.svelte.js";
|
||||
import { loadSettings } from "#lib/settings-store.svelte.js";
|
||||
import {
|
||||
activeTab,
|
||||
claimActiveTab,
|
||||
watchActiveTab,
|
||||
} from "$lib/active-tab.svelte.js";
|
||||
} from "#lib/active-tab.svelte.js";
|
||||
import {
|
||||
applyUpdate,
|
||||
dismissUpdate,
|
||||
setUpdateSW,
|
||||
showUpdatePrompt,
|
||||
updatePrompt,
|
||||
} from "$lib/update-prompt.svelte.js";
|
||||
} from "#lib/update-prompt.svelte.js";
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
return watchActiveTab();
|
||||
});
|
||||
|
||||
// `adapter-static` renders page HTML during `adapt()`, which runs AFTER
|
||||
// the PWA plugin's `closeBundle` — so `@vite-pwa/sveltekit` never gets a
|
||||
// chance to inject its own registration tag into the built HTML. We have
|
||||
// to call the plugin's registration entry point ourselves. Dynamic import
|
||||
// SvelteKit renders `src/app.html` itself, so vite-plugin-pwa never gets a
|
||||
// chance to inject its own registration tag into the built HTML (it is
|
||||
// configured with `injectRegister: false`). We have to call the plugin's
|
||||
// registration entry point ourselves. Dynamic import
|
||||
// (not a static one) so this module is never evaluated during SSR/
|
||||
// prerendering — `virtual:pwa-register` reaches into `navigator` and
|
||||
// `workbox-window`, both browser-only.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script>
|
||||
import { slide } from "svelte/transition";
|
||||
import MasterPanel from "$lib/MasterPanel.svelte";
|
||||
import PageFooter from "$lib/PageFooter.svelte";
|
||||
import PlayerBoard from "$lib/PlayerBoard.svelte";
|
||||
import SettingsButton from "$lib/SettingsButton.svelte";
|
||||
import { settings } from "$lib/settings-store.svelte.js";
|
||||
import MasterPanel from "#lib/MasterPanel.svelte";
|
||||
import PageFooter from "#lib/PageFooter.svelte";
|
||||
import PlayerBoard from "#lib/PlayerBoard.svelte";
|
||||
import SettingsButton from "#lib/SettingsButton.svelte";
|
||||
import { settings } from "#lib/settings-store.svelte.js";
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import adapter from "@sveltejs/adapter-static";
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
import { resolveBase } from "./base-path.js";
|
||||
|
||||
export default {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
pages: "build",
|
||||
assets: "build",
|
||||
fallback: undefined,
|
||||
precompress: false,
|
||||
strict: true,
|
||||
}),
|
||||
paths: { base: resolveBase() },
|
||||
},
|
||||
};
|
||||
+106
-4
@@ -1,8 +1,10 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import adapter from "@sveltejs/adapter-static";
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { SvelteKitPWA } from "@vite-pwa/sveltekit";
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
|
||||
import { resolveBase } from "./base-path.js";
|
||||
|
||||
@@ -33,6 +35,69 @@ const defaultVoicePrecacheEntries = clipNames.map((n) => ({
|
||||
revision: `audio-v1-${defaultVoiceId}-${n}`,
|
||||
}));
|
||||
|
||||
// SvelteKit writes its build to `.svelte-kit/output` (`client/` for
|
||||
// static files and JS, `prerendered/pages/` for the HTML), and the adapter
|
||||
// copies both into `build/` as its last step.
|
||||
const KIT_OUTPUT = ".svelte-kit/output";
|
||||
|
||||
/**
|
||||
* Maps a path under KIT_OUTPUT to the URL the deployed app serves it at.
|
||||
* Regular files become paths relative to `sw.js` (which sits at the app
|
||||
* root, so Workbox resolves them under the base); the prerendered index
|
||||
* page becomes the base URL itself, matching `navigateFallback`.
|
||||
* @param {string} file
|
||||
* @returns {string}
|
||||
*/
|
||||
function precacheUrl(file) {
|
||||
if (file.startsWith("client/")) return file.slice("client/".length);
|
||||
const page = file
|
||||
.replace(/^prerendered\/pages\//, "")
|
||||
.replace(/(^|\/)index\.html$/, "$1")
|
||||
.replace(/\.html$/, "");
|
||||
return page === "" ? `${base}/` : page;
|
||||
}
|
||||
|
||||
/**
|
||||
* vite-plugin-pwa wired into SvelteKit's build. The plugin's own build step
|
||||
* generates `sw.js` when each Vite environment closes, but SvelteKit 3 builds
|
||||
* SSR first, then the client, then prerenders, all inside its `buildApp`
|
||||
* hook, so at those moments the app shell isn't on disk yet. Instead this
|
||||
* drops that step and generates the service worker from a `buildApp` hook
|
||||
* that runs after SvelteKit's (it must come after `sveltekit()` in the
|
||||
* plugin list) and before the adapter's `post`-ordered one, which then
|
||||
* copies `sw.js` into `build/` along with everything else.
|
||||
*
|
||||
* Also dropped, because SvelteKit renders `src/app.html` itself and warns
|
||||
* about any plugin with a `transformIndexHtml` hook: `pwa-assets` (serves
|
||||
* the unused `pwaAssets` icon generator; icons live in /static) and
|
||||
* `dev-sw` (only acts when `devOptions.enabled` is true). Re-enabling
|
||||
* either feature means keeping its plugin.
|
||||
* @param {Partial<import("vite-plugin-pwa").VitePWAOptions>} options
|
||||
* @returns {import("vite").Plugin[]}
|
||||
*/
|
||||
function svelteKitPWA(options) {
|
||||
const plugins = VitePWA(options);
|
||||
const main = plugins.find((p) => p.name === "vite-plugin-pwa");
|
||||
const dropped = new Set([
|
||||
"vite-plugin-pwa:build",
|
||||
"vite-plugin-pwa:pwa-assets",
|
||||
"vite-plugin-pwa:dev-sw",
|
||||
]);
|
||||
return [
|
||||
...plugins.filter((p) => !dropped.has(p.name)),
|
||||
{
|
||||
name: "loto:pwa-generate-sw",
|
||||
apply: "build",
|
||||
async buildApp() {
|
||||
/** @type {import("vite-plugin-pwa").VitePluginPWAAPI | undefined} */
|
||||
const api = main?.api;
|
||||
if (!api) throw new Error("vite-plugin-pwa API not found");
|
||||
await api.generateSW();
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
// .env.local lives outside process.env at config-eval time; loadEnv reads it.
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
@@ -43,8 +108,23 @@ export default defineConfig(({ mode }) => {
|
||||
return {
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
sveltekit(),
|
||||
SvelteKitPWA({
|
||||
sveltekit({
|
||||
preprocess: vitePreprocess(),
|
||||
adapter: adapter({
|
||||
pages: "build",
|
||||
assets: "build",
|
||||
fallback: undefined,
|
||||
precompress: false,
|
||||
strict: true,
|
||||
}),
|
||||
paths: { base },
|
||||
// Kit 3 polls `_app/version.json` hourly by default. The PWA's
|
||||
// service worker already owns update detection (see the
|
||||
// `registerType: "prompt"` note below), so keep polling off as it
|
||||
// was under Kit 2.
|
||||
version: { pollInterval: 0 },
|
||||
}),
|
||||
svelteKitPWA({
|
||||
// "prompt" (not "autoUpdate"): a deploy must never force-reload a
|
||||
// live tab mid-round — that would drop in-memory state
|
||||
// (autoRunning, showCongrats, the auto-call countdown). The new SW
|
||||
@@ -58,8 +138,30 @@ export default defineConfig(({ mode }) => {
|
||||
// confusing.
|
||||
strategies: "generateSW",
|
||||
manifest: false,
|
||||
// `+layout.svelte` imports `virtual:pwa-register` itself, so the
|
||||
// plugin must not inject a registration script of its own.
|
||||
injectRegister: false,
|
||||
// Absolute SW URL and scope (`/loto/sw.js`, `/loto/`), so
|
||||
// registration never depends on the page URL's trailing slash.
|
||||
base: `${base}/`,
|
||||
outDir: `${KIT_OUTPUT}/client`,
|
||||
workbox: {
|
||||
globPatterns: ["**/*.{js,css,html,svg,png,woff2,webmanifest}"],
|
||||
globDirectory: KIT_OUTPUT,
|
||||
// manifest.webmanifest is left out: the browser fetches it
|
||||
// itself when installing, outside the service worker's cache.
|
||||
globPatterns: [
|
||||
"client/**/*.{js,css,html,svg,png,woff2}",
|
||||
"prerendered/pages/**/*.html",
|
||||
],
|
||||
manifestTransforms: [
|
||||
async (entries) => ({
|
||||
manifest: entries.map((e) => ({ ...e, url: precacheUrl(e.url) })),
|
||||
}),
|
||||
],
|
||||
// Content-hashed files need no revision. Tested against the glob
|
||||
// path, before `manifestTransforms` strips the `client/` prefix.
|
||||
dontCacheBustURLsMatching: /^client\/_app\/immutable\//,
|
||||
navigateFallback: `${base}/`,
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||
// App shell + DEFAULT voice clips → guaranteed offline. Other
|
||||
// voices fall through to the CacheFirst runtime rule below
|
||||
|
||||
Reference in new issue
Block a user