explodorama / files / vite.config.ts
vite.config.ts
| 1 | import { defineConfig } from 'vitest/config' |
| 2 | import { qrcode } from 'vite-plugin-qrcode' |
| 3 | import { execSync } from 'node:child_process' |
| 4 | import { existsSync, readFileSync } from 'node:fs' |
| 5 | import fuzzyworld from 'fuzzyworld-tools/vite' |
| 6 | import { UNRELEASED, VERSION } from './src/changelog.ts' |
| 7 | |
| 8 | /** |
| 9 | * Relative asset paths by default, so the same build works wherever Pages puts it: |
| 10 | * at https://<user>.gitlab.io/<project>/ or at the root of a generated domain. Set BASE to |
| 11 | * override. `??` would not catch BASE="", so `||`. |
| 12 | */ |
| 13 | const base = process.env.BASE || './' |
| 14 | |
| 15 | /** A git command's output, or '' where there is no git (the menu then leaves the commit out). */ |
| 16 | function git(args: string): string { |
| 17 | try { |
| 18 | return execSync(`git ${args}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() |
| 19 | } catch { |
| 20 | return '' |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | // The commit the game was built from, shown in the menu with the version. Vite hands VITE_* |
| 25 | // variables to the game as import.meta.env. DIRTY: the build has uncommitted changes (in dev). |
| 26 | process.env.VITE_COMMIT = git('rev-parse --short HEAD') |
| 27 | process.env.VITE_COMMIT_DIRTY = git('status --porcelain') ? '1' : '' |
| 28 | |
| 29 | // Which copy is being built (src/channel.ts): `fw deploy --as explodorama-next` builds the next |
| 30 | // one, from `main`, for anyone who wants to try what isn't released. Anything else is the released copy. |
| 31 | const next = (process.env.FW_APP ?? '').endsWith('-next') |
| 32 | process.env.VITE_CHANNEL = next ? 'next' : 'stable' |
| 33 | // Where each copy lives, from package.json (`homepage`, `nextHomepage`): the menu shows and links them (src/channel.ts). |
| 34 | const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) as { homepage: string; nextHomepage: string } |
| 35 | // (Assigning undefined would make the text "undefined".) |
| 36 | if (pkg.homepage) process.env.VITE_HOME_URL = pkg.homepage |
| 37 | if (pkg.nextHomepage) process.env.VITE_NEXT_URL = pkg.nextHomepage |
| 38 | |
| 39 | export default defineConfig({ |
| 40 | base, |
| 41 | // `npm run dev` and `npm run preview` bind to every interface (--host) so a phone on the same |
| 42 | // Wi-Fi can open the game; this prints a scannable QR of that network URL alongside it. |
| 43 | plugins: [ |
| 44 | fuzzyworld(), |
| 45 | qrcode(), |
| 46 | // Dev only (Q103): `replays/<name>.json`, made by `npm run report-replay`, served as /__replay/<name>.json |
| 47 | // for the replay page (`?replay=<name>`). Nothing in a build. |
| 48 | { |
| 49 | name: 'report-replays', |
| 50 | configureServer(server) { |
| 51 | server.middlewares.use('/__replay', (req, res) => { |
| 52 | const name = (req.url ?? '').replace(/^\//, '').replace(/\.json$/, '') |
| 53 | const file = new URL(`./replays/${name}.json`, import.meta.url) |
| 54 | if (!/^[\w.-]+$/.test(name) || !existsSync(file)) { |
| 55 | res.statusCode = 404 |
| 56 | return res.end('no such replay') |
| 57 | } |
| 58 | res.setHeader('content-type', 'application/json') |
| 59 | res.end(readFileSync(file)) |
| 60 | }) |
| 61 | }, |
| 62 | }, |
| 63 | // The next copy tells the released one what is coming, for its "Try it early" teaser |
| 64 | // (src/ui/whatsnew.ts): next.json beside the game. |
| 65 | { |
| 66 | name: 'next-info', |
| 67 | generateBundle() { |
| 68 | if (!next) return |
| 69 | const info = { version: VERSION, commit: process.env.VITE_COMMIT, unreleased: UNRELEASED } |
| 70 | this.emitFile({ type: 'asset', fileName: 'next.json', source: JSON.stringify(info) }) |
| 71 | }, |
| 72 | }, |
| 73 | ], |
| 74 | // In `npm run dev`, the game's console output and uncaught errors, from any device (a phone on |
| 75 | // the Wi-Fi too), are printed in this terminal (user request, 2026-10-23: a crash on the phone |
| 76 | // showed nothing anywhere). Caught errors are logged with their stack by src/devlog.ts. |
| 77 | server: { forwardConsole: { unhandledErrors: true, logLevels: ['error', 'warn', 'info', 'log'] } }, |
| 78 | // Not `public/`: GitLab Pages requires its artifact to be a directory named `public`, and |
| 79 | // `mv dist public` moves dist *inside* an existing `public/` instead of renaming it. |
| 80 | publicDir: 'static', |
| 81 | // Sprites stay separate files rather than being inlined into the script: the art for enemies and |
| 82 | // pickups that aren't in the game yet then costs nothing until something draws it. |
| 83 | build: { target: 'es2022', assetsInlineLimit: 0 }, |
| 84 | // The decoy demo tests replay seeds 1-6 turn by turn: ~3 s here, over vitest's default 5 s on |
| 85 | // the slower CI runner (CI was red from 2026-11-01 for that reason alone). |
| 86 | test: { testTimeout: 30000 }, |
| 87 | }) |