chore: monorepo scripts (dev, build, db, etc)

This commit is contained in:
ayo 2026-07-03 20:25:33 +02:00
parent 74551f2023
commit 7685a6dea7
8 changed files with 221 additions and 33 deletions

View file

@ -6,21 +6,43 @@ Guidance for AI coding agents working in this repository.
Classic Minesweeper as a vanilla web game — no framework, no TypeScript (JSDoc + `// @ts-check` only). Deployed at [mnswpr.com](https://mnswpr.com) (Netlify) and published to npm as `@ayo-run/mnswpr`. The game engine has **zero runtime dependencies**; only the website adds Firebase.
**`mnswpr` is the main test app.** It's the reference app for the monorepo and the default target for local runs — `.claude/launch.json` launches it (`dev` on :5173, `preview` on :4173), and it's what you should build/run/preview when verifying changes to the shared packages or tooling.
## Commands
Workspace-wide commands run from the root; per-app commands target the app by name with pnpm's `-F` filter (apps are named `<name>` — there are no mnswpr-specific root scripts).
```bash
pnpm i # install (pnpm is required — this is a pnpm workspace)
pnpm dev # run the website dev server (vite apps/mnswpr) — most common
pnpm build # build the website -> apps/mnswpr/dist
pnpm build:lib # build the publishable library -> packages/mnswpr/dist
pnpm lint # eslint . (JS + CSS); runs automatically on pre-commit
pnpm lint:fix # eslint --fix
pnpm build:preview # build the app and serve the production preview
pnpm test # run the Vitest suite once (jsdom)
pnpm test:watch # run Vitest in watch mode
pnpm lint # eslint . (JS + CSS); runs automatically on pre-commit
pnpm lint:fix # eslint --fix
pnpm build:lib # build the publishable library -> packages/mnswpr/dist
pnpm -F mnswpr run dev # Firestore emulator + auto-seed + dev server (emulators:exec) — most common; needs JDK 21+
pnpm -F mnswpr run dev:no-db # plain vite, no emulator (UI-only work / no JDK)
pnpm -F mnswpr run build # build the website -> apps/mnswpr/dist
pnpm -F mnswpr run build:preview # build the app and serve the production preview
```
Tests are co-located with the package they exercise (`packages/utils/test/`, `packages/mnswpr/test/`) and run under **Vitest** with a jsdom environment (root config in `vitest.config.js`). They cover the shared utils and drive the engine through real DOM events (mount `#app`, dispatch mouse events, assert on cell/grid attributes). For anything visual or input-timing related, also verify by running `pnpm dev` and playing.
### Infra (local CLI only — no web dashboards)
All infra runs through local CLIs. Each app **owns its infra scripts** in its own `package.json` under generic, tech-agnostic names (`deploy:db`, not `deploy:firestore`) — run them by targeting the app with pnpm's `-F` filter (no root wrapper scripts):
```bash
pnpm -F mnswpr run db:start # local DB emulator (mnswpr -> Firestore), standalone
pnpm -F mnswpr run db:seed # seed the running local emulator
pnpm -F mnswpr run db:stop # kill a stray/orphaned Firestore emulator holding :8080
pnpm -F mnswpr run deploy:db # deploy DB rules/indexes (-> firebase deploy --only firestore)
pnpm -F mnswpr run deploy:site # build + deploy hosting (-> netlify deploy --prod)
```
Each app defines the same generic script names backed by whatever stack it uses (e.g. Postgres, a different host), so `pnpm -F <name> run deploy:db` is uniform across apps. `deploy:site` requires a one-time `npx netlify-cli login && npx netlify-cli link` per app.
The Firestore emulator needs **Java** (it's a JVM program). `pnpm install` runs a root `postinstall` (`scripts/ensure-java.mjs`) that installs a user-local Temurin JRE 21 into `~/.local` without `sudo` when `java` is missing — idempotent, non-fatal, and auto-skipped on `CI` / `SKIP_JRE_SETUP` / unsupported platforms.
Tests are co-located with the package they exercise (`packages/utils/test/`, `packages/mnswpr/test/`) and run under **Vitest** with a jsdom environment (root config in `vitest.config.js`). They cover the shared utils and drive the engine through real DOM events (mount `#app`, dispatch mouse events, assert on cell/grid attributes). For anything visual or input-timing related, also verify by running `pnpm -F mnswpr run dev` and playing.
Node version: `.nvmrc` pins `lts/*`.
@ -28,7 +50,7 @@ Node version: `.nvmrc` pins `lts/*`.
This is the **Cozy Games** monorepo. Workspaces are declared in `pnpm-workspace.yaml` as `apps/*`, `packages/*`, and `sites/*`. `utils/` is now a real workspace package (`@cozy-games/utils`), imported by name — no more `../utils` relative paths.
- **`apps/mnswpr/`** — `@ayo-run/mnswpr`'s host, the mnswpr.com website. Consumes the engine and leaderboard via `workspace:*` (`import mnswpr from '@ayo-run/mnswpr/mnswpr.js'`) and wires them together in `apps/mnswpr/main.js`. Owns its Firebase config (`firebase.json`, `firestore.rules`, `.firebaserc`) and app-specific scripts (`apps/mnswpr/scripts/`). A future app (e.g. sudoku) gets its own `apps/<name>/`.
- **`apps/mnswpr/`** — package `mnswpr`, `@ayo-run/mnswpr`'s host, the mnswpr.com website. Consumes the engine and leaderboard via `workspace:*` (`import mnswpr from '@ayo-run/mnswpr/mnswpr.js'`) and wires them together in `apps/mnswpr/main.js`. Owns its Firebase config (`firebase.json`, `firestore.rules`, `.firebaserc`) and app-specific scripts (`apps/mnswpr/scripts/`). A future app (e.g. sudoku) gets its own `apps/<name>/` and its `package.json` `name` is just the app name (`<name>`, unscoped) so it's addressable directly by name (`pnpm -F <name> run <script>`).
- **`packages/mnswpr/`** — `@ayo-run/mnswpr`, the standalone, framework-free game engine published to npm. `packages/mnswpr/mnswpr.js` is the whole engine; `levels.js` defines the four difficulty presets. Depends only on `@cozy-games/utils`.
- **`packages/leaderboard/`** — `@cozy-games/leaderboard`, a backend-agnostic, time-windowed leaderboard (adapter-injected storage).
- **`packages/utils/`** — `@cozy-games/utils`, shared services with no dependencies, re-exported from `index.js`: `StorageService`, `TimerService` (`pretty()` time formatting used by both engine and leaderboard), `LoggerService`, `LoadingService`, and date-bucket helpers.

View file

@ -23,15 +23,44 @@ Each app owns its own backend config (e.g. mnswpr's Firestore rules live in
This is a [pnpm](https://pnpm.io) workspace (pnpm is required).
Workspace-wide commands run from the root:
```bash
pnpm i # install
pnpm dev # run the mnswpr app (Vite dev server)
pnpm test # run all package tests (vitest)
pnpm lint # eslint
pnpm build # build the mnswpr app -> apps/mnswpr/dist
pnpm build:lib # build the engine package -> packages/mnswpr/dist
```
### Per-app local development
Apps aren't run from the root — target the app by name with pnpm's `-F` filter. Apps are
named `<name>` (e.g. `mnswpr`), so every app runs the same way:
```bash
pnpm -F mnswpr run dev # start that app's Vite dev server
pnpm -F mnswpr run build # build just that app -> apps/mnswpr/dist
pnpm -F mnswpr run preview # preview its production build
```
## Infra (per-app, via local CLI)
Infra runs through local CLIs, never web dashboards. Each app owns its infra scripts
under generic names (`deploy:db`, `deploy:site`, `db:start`, `db:seed`) — run them by
targeting the app with pnpm's `-F` filter:
```bash
pnpm -F mnswpr run db:start # start the local DB emulator (Firestore) — needs Java, see app README
pnpm -F mnswpr run db:seed # seed the running emulator with dev data
pnpm -F mnswpr run db:stop # kill a stray emulator left holding :8080
pnpm -F mnswpr run deploy:db # deploy DB rules/indexes (firebase deploy --only firestore)
pnpm -F mnswpr run deploy:site # build + deploy hosting (netlify deploy --prod)
```
Apps are named `<name>`, so a future app uses the same command shape
(`pnpm -F <name> run deploy:db`), backed by whatever stack that app uses.
`deploy:site` needs a one-time `npx netlify-cli login && npx netlify-cli link` per app.
See [apps/mnswpr/README.md](apps/mnswpr/README.md) for the game itself, and each package's
README for library usage.

View file

@ -52,31 +52,65 @@ Technology Stack: HTML, JS, and CSS; [Google Firebase](https://firebase.google.c
To start development, you need [`node`](https://nodejs.org/en/download). I highly recommend [`pnpm`](https://pnpm.io/installation) to be used as well. Once you know you have this, you can do the following:
1. Install dependencies: `pnpm i`
2. Start the dev server: `pnpm run dev`
2. Start the dev server: `pnpm -F mnswpr run dev`
The rest of the everyday commands:
```bash
pnpm test # run the Vitest suite
pnpm lint # ESLint (JS + CSS)
pnpm lint:fix # ESLint with autofix
pnpm build # build the website
pnpm build:lib # build the publishable library
pnpm test # run the Vitest suite (workspace-wide)
pnpm lint # ESLint (JS + CSS)
pnpm lint:fix # ESLint with autofix
pnpm -F mnswpr run build # build the website
pnpm build:lib # build the publishable library
```
### Leaderboard (local Firestore emulator)
The leader board is backed by [Google Firestore](https://firebase.google.com). For local development the app talks to the **Firestore emulator** by default — fully local, no cloud, no deploy. The flag `VITE_FIRESTORE_EMULATOR=1` is already set in `app/.env.development`.
You need a **JDK 21+** installed (the emulator runs on Java; `firebase-tools` itself is fetched on demand via `npx`). Then, in two terminals:
`dev` is the whole loop in one command — it wraps Vite in `firebase emulators:exec`, so the Firestore emulator (on :8080, + UI) comes up, gets **seeded with sample scores automatically**, and the app dev server starts against it; everything shuts down when you stop it. (Each `dev` starts a fresh in-memory emulator, so the auto-seed writes exactly one clean set every time — no accumulation.) `firebase-tools` itself is fetched on demand via `npx`, so the only prerequisite is **Java** — the Firestore emulator is a Java program (`java -jar cloud-firestore-emulator-*.jar`), and firebase-tools does not bundle a JRE.
**Usually automatic.** `pnpm install` runs a root `postinstall` ([`scripts/ensure-java.mjs`](../../scripts/ensure-java.mjs)) that installs a user-local Temurin JRE 21 into `~/.local` (no `sudo`) when `java` isn't already on your PATH. It's idempotent and never fails the install, and it skips when `CI` or `SKIP_JRE_SETUP=1` is set, or on unsupported platforms.
If that skipped and you need Java (or prefer a system-wide install), do it manually — **install a JRE (Java 11+; 21 recommended):**
```bash
pnpm emulators # terminal 1 — start the Firestore emulator on :8080 (+ UI)
pnpm seed:emulator # terminal 2, once — fill it with sample scores
pnpm dev # terminal 2 — run the app against the emulator
# Debian / Ubuntu / Pop!_OS
sudo apt update && sudo apt install -y openjdk-21-jre-headless
# Fedora
sudo dnf install -y java-21-openjdk-headless
# macOS (Homebrew)
brew install openjdk@21
java -version # verify: should print "openjdk 21.x" (or 11+)
```
If the emulator isn't running, the board simply shows *"unavailable"* (a refused connection). To skip the emulator — for quick UI-only work, or if you don't have a JDK — set `VITE_FIRESTORE_EMULATOR=` (empty) in a local, gitignored `app/.env.local`; the app then uses the cloud `mw-test` namespace instead.
No `sudo`? Install a JRE into your home directory instead (no root needed):
```bash
curl -fsSL -o /tmp/jre21.tgz "https://api.adoptium.net/v3/binary/latest/21/ga/linux/x64/jre/hotspot/normal/eclipse"
mkdir -p ~/.local/lib && tar xzf /tmp/jre21.tgz -C ~/.local/lib
ln -sf ~/.local/lib/jdk-21*-jre/bin/java ~/.local/bin/java # ~/.local/bin is already on PATH
java -version
```
Without Java, `dev` and `db:start` fail with `Could not spawn 'java -version'`. Install it to a permanent location — a JRE unpacked under `/tmp` disappears when the OS cleans temp files. Then just:
```bash
pnpm -F mnswpr run dev # emulator (:8080 + UI) + auto-seed + app dev server — one command
```
That's the everyday loop. The other DB scripts are for when you want to run pieces separately:
```bash
pnpm -F mnswpr run db:start # emulator only (stays up across app restarts); pair with dev:no-db
pnpm -F mnswpr run db:seed # seed a separately-running emulator (what dev does for you)
pnpm -F mnswpr run db:stop # kill a stray/orphaned emulator holding :8080
```
To skip the emulator entirely — for quick UI-only work, or if you don't have a JDK — run `pnpm -F mnswpr run dev:no-db` (plain Vite) and set `VITE_FIRESTORE_EMULATOR=` (empty) in a local, gitignored `app/.env.local`; the app then uses the cloud `mw-test` namespace instead.
See [`docs/firebase-leaderboards.md`](./docs/firebase-leaderboards.md) for the full data model, security rules, environments, and deployment.

View file

@ -138,11 +138,11 @@ locally (so you validate them before deploying). The flag
Everyday dev loop:
```bash
pnpm emulators # terminal 1: Firestore emulator (+ Emulator UI) on :8080
pnpm seed:emulator # terminal 2, once: fill it with sample scores
pnpm dev # terminal 2: app runs against the local emulator
pnpm -F mnswpr run dev # emulator (+ UI) on :8080, auto-seeded with sample scores, + app dev server
```
`dev` seeds the fresh emulator for you (via `emulators:exec "node scripts/seed-dev-scores.js; vite"`). Use the standalone `db:start` / `db:seed` scripts only when running the emulator separately from the app.
Wiring: `app/main.js` passes `{ emulator: { host, port } }` to `FirebaseAdapter`,
which calls `connectFirestoreEmulator`. If the emulator isn't running the board
just shows "unavailable" (a refused connection) — start it, or opt out.

View file

@ -1,13 +1,21 @@
{
"name": "app",
"name": "mnswpr",
"version": "0.0.1",
"description": "the mnswpr.com web app",
"private": true,
"type": "module",
"main": "main.js",
"scripts": {
"dev": "npx -y firebase-tools emulators:exec --only firestore --ui \"node scripts/seed-dev-scores.js; vite\"",
"dev:no-db": "vite",
"build": "vite build",
"preview": "vite preview",
"build:preview": "npm run build && npm run preview"
"build:preview": "npm run build && npm run preview",
"deploy:db": "npx -y firebase-tools deploy --only firestore",
"deploy:site": "npm run build && npx -y netlify-cli deploy --prod --dir=dist",
"db:start": "npx -y firebase-tools emulators:start --only firestore",
"db:seed": "FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 node scripts/seed-dev-scores.js",
"db:stop": "pkill -f '[c]loud-firestore-emulator'; pkill -f '[f]irebase.* emulators:'; true"
},
"devDependencies": {
"@ayo-run/mnswpr": "workspace:*",

View file

@ -50,7 +50,7 @@ export default defineConfig([
}
},
{
files: ['**/scripts/**/*.js'],
files: ['**/scripts/**/*.{js,mjs,cjs}'],
languageOptions: {
globals: globals.node
}

View file

@ -12,16 +12,12 @@
"homepage": "https://mnswpr.com",
"scripts": {
"test": "vitest run",
"dev": "pnpm -F mnswpr dev",
"test:watch": "vitest",
"dev": "vite apps/mnswpr",
"start": "vite apps/mnswpr",
"emulators": "cd apps/mnswpr && npx -y firebase-tools emulators:start --only firestore",
"seed:emulator": "cd apps/mnswpr && FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 node scripts/seed-dev-scores.js",
"build": "vite build apps/mnswpr",
"build:lib": "vite build packages/mnswpr",
"publish:lib": "node scripts/publish-lib.js",
"release": "pnpm build:lib && pnpm -F @ayo-run/mnswpr run release && pnpm publish:lib",
"build:preview": "pnpm -F app run build:preview",
"postinstall": "node scripts/ensure-java.mjs",
"prepare": "husky",
"lint": "eslint .",
"lint:fix": "eslint . --fix"

99
scripts/ensure-java.mjs Normal file
View file

@ -0,0 +1,99 @@
/**
* Post-install convenience: make a Java runtime available for the Firebase
* Firestore emulator (used by `pnpm -F mnswpr dev` / `db:start`). The emulator is
* a Java program and firebase-tools does not bundle a JRE.
*
* Installs a Temurin JRE 21 into the user's home (~/.local) WITHOUT sudo and
* symlinks `java` into ~/.local/bin (already on most PATHs). Properties:
* - idempotent: does nothing if `java` is already on PATH
* - non-fatal: never fails `pnpm install` on any problem it warns, exits 0
* - opt-out: set SKIP_JRE_SETUP=1 (also auto-skips when CI is set)
* Only Linux/macOS on x64/arm64 are auto-handled; anything else prints manual
* instructions (see apps/mnswpr/README.md).
*/
import { spawnSync } from 'node:child_process'
import { existsSync, mkdirSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { homedir, tmpdir, platform, arch } from 'node:os'
import { join } from 'node:path'
const log = msg => console.log(`[ensure-java] ${msg}`)
const warn = msg => console.warn(`[ensure-java] ${msg}`)
// This is a convenience, never a blocker — always exit 0.
try {
await main()
} catch (err) {
warn(`skipped (${err?.message || err}).`)
warn('The Firestore emulator needs Java 11+ — install it manually, see apps/mnswpr/README.md.')
}
process.exit(0)
async function main() {
if (process.env.SKIP_JRE_SETUP) return log('SKIP_JRE_SETUP set — skipping.')
if (process.env.CI) return log('CI detected — skipping JRE setup.')
if (hasJava('java')) return log('Java already on PATH — nothing to do.')
const adoptOs = { linux: 'linux', darwin: 'mac' }[platform()]
const adoptArch = { x64: 'x64', arm64: 'aarch64' }[arch()]
if (!adoptOs || !adoptArch) {
warn(`no auto-install for ${platform()}/${arch()} — install a JRE 21 manually (apps/mnswpr/README.md).`)
return
}
if (!hasTool('tar')) {
warn('`tar` not found — cannot unpack the JRE. Install Java manually (apps/mnswpr/README.md).')
return
}
const prefix = process.env.JAVA_SETUP_PREFIX || join(homedir(), '.local')
const libDir = join(prefix, 'lib')
const binDir = join(prefix, 'bin')
mkdirSync(libDir, { recursive: true })
mkdirSync(binDir, { recursive: true })
const url = `https://api.adoptium.net/v3/binary/latest/21/ga/${adoptOs}/${adoptArch}/jre/hotspot/normal/eclipse`
log(`no Java found — downloading Temurin JRE 21 (${adoptOs}/${adoptArch})…`)
const res = await fetch(url, { redirect: 'follow' })
if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`)
const tgz = join(tmpdir(), `temurin-jre-21-${adoptOs}-${adoptArch}.tar.gz`)
writeFileSync(tgz, Buffer.from(await res.arrayBuffer()))
const before = new Set(readdirSync(libDir))
const untar = spawnSync('tar', ['xzf', tgz, '-C', libDir], { stdio: 'inherit' })
rmSync(tgz, { force: true })
if (untar.status !== 0) throw new Error('tar extraction failed')
// the top-level dir the tarball created (e.g. jdk-21.0.11+10-jre); on re-runs
// it already exists, so fall back to the newest matching dir.
const jreDir = readdirSync(libDir).find(d => !before.has(d) && /jdk.*jre/i.test(d))
|| readdirSync(libDir).filter(d => /jdk.*jre/i.test(d)).sort().pop()
if (!jreDir) throw new Error('could not locate the unpacked JRE directory')
// java is at bin/java (linux) or Contents/Home/bin/java (macOS)
const javaBin = [
join(libDir, jreDir, 'bin', 'java'),
join(libDir, jreDir, 'Contents', 'Home', 'bin', 'java')
].find(existsSync)
if (!javaBin) throw new Error('java binary not found in the unpacked JRE')
for (const tool of ['java', 'keytool']) {
const src = javaBin.replace(/java$/, tool)
const dst = join(binDir, tool)
if (existsSync(src)) { rmSync(dst, { force: true }); symlinkSync(src, dst) }
}
if (!hasJava(join(binDir, 'java'))) throw new Error('the installed java did not run')
log(`installed JRE 21 → ${join(libDir, jreDir)}`)
log(`linked java → ${join(binDir, 'java')}`)
if (!(process.env.PATH || '').split(':').includes(binDir)) {
warn(`${binDir} is not on your PATH — add it (e.g. in ~/.profile) so \`java\` is found.`)
}
}
function hasJava(bin) {
const r = spawnSync(bin, ['-version'], { stdio: 'ignore' })
return !r.error && r.status === 0
}
function hasTool(name) {
const r = spawnSync(name, ['--version'], { stdio: 'ignore' })
return !r.error
}