From 1ed617736eff8dcf8d45514bd217bc896cb8e988 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Wed, 12 Aug 2026 12:56:32 -0500 Subject: [PATCH] =?UTF-8?q?feat(template):=20a=20module=20that=20builds=20?= =?UTF-8?q?and=20loads=20=E2=80=94=20Phase=205=20slice=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kit's `template/`: a complete, minimal Runic Gateway module a reader copies, renames, and runs before reading a chapter. Slice 0 landed the workflow that runs it; this is the tree that workflow was written against, so the `template` job arms itself with no edit to the guard. Installed into a real core it adds one public page at `/examplegame/status`, a nav row pointing at it, one API route described in an OpenAPI fragment core merges, one table created by an idempotent schema fragment and dropped by a purge file, and both lifecycle hooks. That is deliberately less than a real module does; what it is complete about is the shape — every seam used once, with the reasoning next to it. Four decisions, settled with the org lead: 1. **Public tier only, plus the lifecycle hooks.** §2.11.1 d1's "one public route", plus enough to show the whole vertical seam once. Admin and player tiers become worked examples quoted from module-uo in chapter 2 rather than two thirds of a tree the reader deletes on day one. 2. **The release workflow ships as a file, in BOTH flavours** — `.gitea/` and `.github/`. Neither runs where it sits (a workflow is only read from a repository root) and each arms itself when the reader's copy is its own repo. Packaging is the part of a module that cannot be guessed at, and the kit's audience is outside this org, so assuming Gitea would have been assuming our own deployment. Core installs from a URL and does not care where the release lives — only that the host is on the operator's `MODULE_SOURCE_HOSTS`. 3. **A rename checklist that CI verifies**, not a rename script. `template/README.md` carries the table; `scripts/checkRenameSites.js` holds it against the tree in both directions — an unlisted file that still carries the placeholder fails, and so does a listed file that no longer does. The second half is the one usually left out and the more valuable: a row that has stopped matching reads as instructions to edit something that is not there. Same rule core's identifier check follows about its own exemptions. It has its own ten-test suite, run by CI as `node --test`, because a check that has never been shown to fail is a check nobody knows the state of. 4. **A neutral invented game.** One deviation from the literal answer, forced by decision 3: the id is `examplegame`, not `example`. The checklist check is a text search, and `example` occurs in ordinary English ("for example") all over prose that is not a rename site — a placeholder that cannot occur by accident is what makes the check answerable instead of a source of false alarms someone learns to ignore. **The pin moves to the 1.4.0 bump** (website `edge` 1b692bf), which is what `template/module.json` declares as `coreApi`. Slice 0 pinned its parent, before 1.4.0 existed, so `checkCoreApi.js` arms for the first time here — it asserts EQUALITY, and its failing on the next contract bump is the system working. Also in CI: the client tests now run AFTER the build (two of them read the built chunk and skip without one — run first, the job reports green while asking nothing about the artifact that ships), and `check:swagger` verifies the committed fragment is current. ## The finding: an UPDATE that changes nothing does not touch ON UPDATE CURRENT_TIMESTAMP Every suite passed, both guards passed, the chunk built, the module loaded into a real core and the page rendered correctly. Two hours later the same page said the world was offline, and it was wrong. `updated_at` was declared `ON UPDATE CURRENT_TIMESTAMP`, and MariaDB fires that only when an UPDATE actually CHANGES a value. The boot refresh writes the same numbers every thirty seconds — which is exactly what a quiet game looks like — so the timestamp froze at the first write, the row crossed the freshness window, and the model correctly reported a stale row as offline. Verified against the live database: two hours of refreshes, `updated_at` still the boot timestamp. No test in this repo could see it. The model takes its clock as an argument, and nothing in a suite runs the same UPDATE twice against a real database. It is only visible as a page that was right when you looked at it and wrong an hour later. The writer now sets `updated_at = CURRENT_TIMESTAMP` explicitly and the column drops the clause that was not doing what it looked like it was doing; both carry the reasoning. Re-verified end to end: the timestamp advances every interval and the API reports fresh. Falling out of the fix, the schema fragment gained the rule the reader hits next: **changing a table is an ALTER, never an edit to its CREATE** — `CREATE TABLE IF NOT EXISTS` does nothing when the table exists, so an edited column definition takes effect on a fresh install and on no existing one, which is the worst possible split because your development database is usually the fresh one. ## Verified - 29 server tests, 18 client tests, 10 kit-script tests; `check:imports`, `check:externals`, `check:swagger` and `checkCoreApi` all green, run in CI's own order from a clean `npm ci`. - Browser smoke (MODULE_API.md §7.7) against a real core built from the pinned ref: module `started`, published on `/api/v1/public/modules`, chunk served `no-cache` with the right MIME from the entry's directory while `module.json` and the server source 404, script tag injected after core's bundle, the page rendering inside core's own chrome, the nav row interleaved into the public header between Wiki and About, SPA navigation into it from another page, the module's path and schema and tag merged into `/api/docs.json`, and `[examplegame] registered against core API 1.4.0` in the console with no CSP report and no React error. Refs: MODULE_SYSTEM.md §2.11.1 (slice 1), MODULE_API.md §2.x, §3.x, §5.1, §7.7. Co-Authored-By: Claude --- .gitea/workflows/pr-checks.yml | 49 +- README.md | 7 +- ci/core-ref.json | 6 +- scripts/checkRenameSites.js | 146 ++ scripts/checkRenameSites.test.js | 107 + template/.gitea/workflows/release.yml | 270 +++ template/.github/workflows/release.yml | 241 +++ template/LICENSE.md | 674 +++++++ template/README.md | 145 ++ template/client/package-lock.json | 1792 +++++++++++++++++ template/client/package.json | 24 + template/client/scripts/checkExternals.js | 172 ++ template/client/src/api.js | 34 + template/client/src/core.js | 77 + template/client/src/entry.jsx | 79 + .../client/src/routes/public/WorldStatus.jsx | 65 + template/client/src/shim/jsx-runtime.js | 16 + template/client/src/shim/react-dom.js | 14 + template/client/src/shim/react-router-dom.js | 32 + template/client/src/shim/react.js | 50 + template/client/src/shim/rg.js | 29 + template/client/test/build.test.js | 154 ++ template/client/test/registration.test.js | 177 ++ template/client/vite.config.js | 137 ++ template/module.json | 14 + template/server/boot.js | 92 + template/server/core.js | 99 + template/server/db/purge.sql | 24 + template/server/db/schema.sql | 70 + template/server/index.js | 98 + .../model/worldStatus/worldStatus.db.js | 53 + .../model/worldStatus/worldStatus.model.js | 50 + template/server/package-lock.json | 1056 ++++++++++ template/server/package.json | 23 + .../server/router/public/world.controller.js | 27 + template/server/router/public/world.router.js | 50 + template/server/scripts/checkImports.js | 190 ++ template/server/scripts/swaggerFragment.js | 255 +++ template/server/swagger/doc.js | 56 + template/server/test/_fakes.js | 107 + template/server/test/checkImports.test.js | 149 ++ template/server/test/entry.test.js | 84 + template/server/test/schema.test.js | 116 ++ template/server/test/worldStatus.test.js | 58 + template/swagger-fragment.json | 133 ++ 45 files changed, 7289 insertions(+), 12 deletions(-) create mode 100644 scripts/checkRenameSites.js create mode 100644 scripts/checkRenameSites.test.js create mode 100644 template/.gitea/workflows/release.yml create mode 100644 template/.github/workflows/release.yml create mode 100644 template/LICENSE.md create mode 100644 template/README.md create mode 100644 template/client/package-lock.json create mode 100644 template/client/package.json create mode 100644 template/client/scripts/checkExternals.js create mode 100644 template/client/src/api.js create mode 100644 template/client/src/core.js create mode 100644 template/client/src/entry.jsx create mode 100644 template/client/src/routes/public/WorldStatus.jsx create mode 100644 template/client/src/shim/jsx-runtime.js create mode 100644 template/client/src/shim/react-dom.js create mode 100644 template/client/src/shim/react-router-dom.js create mode 100644 template/client/src/shim/react.js create mode 100644 template/client/src/shim/rg.js create mode 100644 template/client/test/build.test.js create mode 100644 template/client/test/registration.test.js create mode 100644 template/client/vite.config.js create mode 100644 template/module.json create mode 100644 template/server/boot.js create mode 100644 template/server/core.js create mode 100644 template/server/db/purge.sql create mode 100644 template/server/db/schema.sql create mode 100644 template/server/index.js create mode 100644 template/server/model/worldStatus/worldStatus.db.js create mode 100644 template/server/model/worldStatus/worldStatus.model.js create mode 100644 template/server/package-lock.json create mode 100644 template/server/package.json create mode 100644 template/server/router/public/world.controller.js create mode 100644 template/server/router/public/world.router.js create mode 100644 template/server/scripts/checkImports.js create mode 100644 template/server/scripts/swaggerFragment.js create mode 100644 template/server/swagger/doc.js create mode 100644 template/server/test/_fakes.js create mode 100644 template/server/test/checkImports.test.js create mode 100644 template/server/test/entry.test.js create mode 100644 template/server/test/schema.test.js create mode 100644 template/server/test/worldStatus.test.js create mode 100644 template/swagger-fragment.json diff --git a/.gitea/workflows/pr-checks.yml b/.gitea/workflows/pr-checks.yml index ef23d30..27d7b34 100644 --- a/.gitea/workflows/pr-checks.yml +++ b/.gitea/workflows/pr-checks.yml @@ -4,11 +4,19 @@ # # ── What each job is really asking ─────────────────────────────────────────── # -# • `links` — every relative link resolves, and no link pins a reader to a -# commit snapshot of a document that moves. Nothing is fetched: this project's -# Gitea is self-hosted, so an HTTP check would fail on a runner without -# credentials and teach everyone to ignore red. What breaks in practice is a -# relative path after a file moves, and that is answerable offline. +# • `prose` — the documentation, checked as far as documentation can be. Every +# relative link resolves, and no link pins a reader to a commit snapshot of a +# document that moves. Nothing is fetched: this project's Gitea is self-hosted, +# so an HTTP check would fail on a runner without credentials and teach +# everyone to ignore red. What breaks in practice is a relative path after a +# file moves, and that is answerable offline. +# +# It also holds `template/README.md`'s rename checklist against the template +# tree, in both directions — an unlisted file that still carries the +# placeholder, and a listed file that no longer does, are both failures. That +# checklist is the only instruction a reader has for the first thing they do +# with the template, and it is prose, so it rots the way prose does. The two +# checks in `scripts/` have their own unit tests, run in the same job. # # • `template` — the interesting one, and the anti-rot mechanism of the whole # repo (MODULE_SYSTEM.md §2.11.1 d2). It clones CORE at the ref pinned in @@ -60,7 +68,7 @@ env: NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT: 120000 jobs: - links: + prose: runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -70,11 +78,20 @@ jobs: with: node-version: 20 - # No dependencies on purpose — this has to run on a clone with nothing - # installed, which is also how a reader will run it. + # No dependencies on purpose — every step in this job has to run on a clone + # with nothing installed, which is also how a reader will run them. - name: Check every link in the book run: node scripts/checkLinks.js + - name: Check the rename checklist against the template + run: node scripts/checkRenameSites.js + + # The checks, checked. A check that has never been shown to fail is a check + # nobody knows the state of — and this one gates the instructions for the + # first thing a reader does. + - name: Test the checks themselves + run: node --test scripts/checkRenameSites.test.js + template: runs-on: ubuntu-latest timeout-minutes: 20 @@ -136,3 +153,19 @@ jobs: - name: Run the template's tests if: steps.guard.outputs.present == 'true' run: npm test --prefix template/server + + # After the build, and that ordering is the point: two of the client tests + # read the BUILT chunk and SKIP when there is none. Run before the build, + # this job would report green while asking nothing about the artifact that + # ships — which is exactly how the first real module's two artifact tests sat + # green and inert. + - name: Run the template's client tests + if: steps.guard.outputs.present == 'true' + run: npm test --prefix template/client + + # The committed OpenAPI fragment, regenerated and compared. Core merges that + # file verbatim into its own spec, so a stale one documents a URL surface the + # module does not serve — and nothing at runtime will ever say so. + - name: Check the template's OpenAPI fragment is current (MODULE_API.md §2.8) + if: steps.guard.outputs.present == 'true' + run: npm run check:swagger --prefix template/server diff --git a/README.md b/README.md index 1758fe7..4de4eaf 100644 --- a/README.md +++ b/README.md @@ -83,9 +83,10 @@ scripts/ the checks CI runs over both ``` CI clones core at a **pinned commit**, asserts the version the template declares -still matches that core's `MODULE_API_VERSION`, builds the template, and checks -every link in the book. So a change to the contract breaks this repo's build -loudly instead of leaving a chapter quietly wrong. +still matches that core's `MODULE_API_VERSION`, builds the template and runs its +guards, checks every link in the book, and holds the template's rename checklist +against the template's own tree. So a change to the contract breaks this repo's +build loudly instead of leaving a chapter quietly wrong. ## Licence diff --git a/ci/core-ref.json b/ci/core-ref.json index 718f0e3..a01c9cd 100644 --- a/ci/core-ref.json +++ b/ci/core-ref.json @@ -1,9 +1,13 @@ { "repo": "https://gitea.whitlocktech.com/RunicGateway/website.git", "branch": "edge", - "ref": "c3120ea3daee579ea9948e9e466065f25ee4e92f", + "ref": "1b692bf624404f9e4f924c231acfbfb7e9d0861a", "why": [ "The core this kit is written against, pinned to a commit rather than a branch.", + "This one is the MODULE_API_VERSION 1.4.0 bump, which is the version", + "template/module.json declares - slice 0 pinned its parent, before 1.4.0", + "existed, and the check below could not have passed against it.", + "", "Moving this pin is the moment someone re-reads the chapters: CI asserts the", "version template/module.json declares still equals this core's", "MODULE_API_VERSION, so a contract bump turns this repo red on purpose", diff --git a/scripts/checkRenameSites.js b/scripts/checkRenameSites.js new file mode 100644 index 0000000..9533e27 --- /dev/null +++ b/scripts/checkRenameSites.js @@ -0,0 +1,146 @@ +#!/usr/bin/env node +// The rename checklist in `template/README.md`, checked against the tree. +// +// A reader's first action is to copy `template/` and make it theirs, and the only +// thing telling them where the placeholder name is buried is that table. A +// checklist nobody verifies is wrong by the second edit to the template — someone +// adds a file, mentions the placeholder id in it, and every reader after that +// ships a module with a stray `examplegame` in its OpenAPI tags. +// +// So this asserts the table and the tree agree, in BOTH directions: +// +// • every file that still mentions the placeholder is listed, and +// • every listed file exists and still mentions it. +// +// The second half is the one that is easy to leave out and is the more valuable: +// an entry that has stopped matching is an entry that will be read as instructions +// to edit something that is not there. Same rule the identifier check in core's CI +// follows about its own exemptions — an exemption that no longer matches fails the +// build rather than being quietly tolerated. +// +// **Why the placeholder is `examplegame` and not `example`.** This is a whole-file +// text search, and `example` appears in ordinary English ("for example") all over +// prose that is not a rename site at all. A placeholder that cannot occur by +// accident is what makes a check like this answerable rather than a source of +// false alarms someone eventually learns to ignore. +// +// Usage: node scripts/checkRenameSites.js (from the repo root) + +const fs = require('fs') +const path = require('path') + +const ROOT = path.resolve(__dirname, '..') +const TEMPLATE = path.join(ROOT, 'template') +const CHECKLIST = path.join(TEMPLATE, 'README.md') + +// Anything a rename has to touch: the id (`examplegame`), the display name +// ("Example Game"), and the placeholder world ("Example World"). One pattern +// rather than three, because they are one decision. +const PLACEHOLDER = /example[ -]?(game|world)/i + +// Directories with nothing of ours in them. `dist` and `node_modules` are build +// output — a chunk full of the placeholder is not a rename site, it is the +// consequence of one. +const SKIP_DIRS = new Set(['.git', 'node_modules', 'dist']) + +// The checklist is the one file exempt from the scan: it is a table OF the +// placeholder and would trivially list itself. +const SELF = 'README.md' + +/** Every file under `template/`, template-relative, sorted. */ +function templateFiles(dir = TEMPLATE, out = []) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.isDirectory()) { + if (SKIP_DIRS.has(entry.name)) continue + templateFiles(path.join(dir, entry.name), out) + } else if (entry.isFile()) { + out.push(path.relative(TEMPLATE, path.join(dir, entry.name)).split(path.sep).join('/')) + } + } + return out.sort() +} + +/** + * The paths the checklist names, read from between its two markers. + * + * Delimited by explicit HTML comments rather than by looking for a heading or for + * every backticked path in the document: the README quotes plenty of paths in + * prose and in its tree diagram, and none of those are checklist entries. An + * explicit marker also means the table can be reformatted freely. + */ +function checklistPaths(markdown) { + const start = markdown.indexOf('') + const end = markdown.indexOf('') + if (start === -1 || end === -1 || end < start) { + throw new Error( + 'template/README.md has no block. ' + + 'That block is the checklist this check exists to verify.', + ) + } + const table = markdown.slice(start, end) + const paths = [] + for (const line of table.split('\n')) { + // A table row whose first cell is a backticked path. + const match = /^\|\s*`([^`]+)`\s*\|/.exec(line.trim()) + if (match) paths.push(match[1]) + } + return paths +} + +/** Everything wrong, as sentences. Empty means the checklist is current. */ +function problems({ files, listed, contains }) { + const out = [] + const listedSet = new Set(listed) + + const duplicates = listed.filter((p, i) => listed.indexOf(p) !== i) + for (const p of new Set(duplicates)) out.push(`${p} is listed in the checklist twice.`) + + for (const file of files) { + if (file === SELF) continue + if (!contains(file)) continue + if (!listedSet.has(file)) { + out.push( + `${file} still mentions the placeholder and is NOT in the rename checklist. ` + + 'Add a row for it, or take the placeholder out of the file.', + ) + } + } + + const present = new Set(files) + for (const file of listed) { + if (!present.has(file)) { + out.push(`the checklist lists ${file}, which does not exist. Remove the row or restore the file.`) + } else if (!contains(file)) { + out.push( + `the checklist lists ${file}, which no longer mentions the placeholder. ` + + 'A row that has stopped matching tells a reader to edit something that is not there.', + ) + } + } + + return out +} + +module.exports = { PLACEHOLDER, checklistPaths, problems, templateFiles, TEMPLATE } + +if (require.main !== module) return + +if (!fs.existsSync(TEMPLATE)) { + console.log('checkRenameSites: no template/ yet — nothing to check') + process.exit(0) +} + +const files = templateFiles() +const listed = checklistPaths(fs.readFileSync(CHECKLIST, 'utf8')) +const contains = (file) => PLACEHOLDER.test(fs.readFileSync(path.join(TEMPLATE, file), 'utf8')) + +const found = problems({ files, listed, contains }) + +if (found.length) { + console.error(`\n${found.length} problem(s) with the rename checklist in template/README.md:\n`) + for (const p of found) console.error(` - ${p}`) + console.error('') + process.exit(1) +} + +console.log(`OK — the rename checklist matches the template (${listed.length} files).`) diff --git a/scripts/checkRenameSites.test.js b/scripts/checkRenameSites.test.js new file mode 100644 index 0000000..d4027db --- /dev/null +++ b/scripts/checkRenameSites.test.js @@ -0,0 +1,107 @@ +// The rename check, checked. +// +// A check that has never been shown to fail is a check nobody knows the state of. +// This one gates the only instructions a reader has for the first thing they do +// with the template, so both directions of it are exercised here against +// fixtures — no filesystem, because `problems()` takes its three inputs as +// arguments precisely so that it can be tested this way. +// +// Run by CI as `node --test scripts/`, which needs no dependencies and no +// package.json: Node's own test runner, over a repo with nothing installed. + +const test = require('node:test') +const assert = require('node:assert') + +const { PLACEHOLDER, checklistPaths, problems } = require('./checkRenameSites') + +/** `problems()` with a `contains` built from a set of file names. */ +const check = (files, listed, dirty) => + problems({ files, listed, contains: (f) => new Set(dirty).has(f) }) + +test('a clean, complete checklist has no problems', () => { + assert.deepStrictEqual(check(['a.js', 'b.js', 'clean.js'], ['a.js', 'b.js'], ['a.js', 'b.js']), []) +}) + +test('a file that mentions the placeholder and is not listed fails', () => { + const found = check(['a.js', 'new.js'], ['a.js'], ['a.js', 'new.js']) + assert.strictEqual(found.length, 1) + assert.match(found[0], /new\.js.*NOT in the rename checklist/s) +}) + +test('a listed file that no longer mentions the placeholder fails', () => { + // The direction that is easy to leave out, and the more valuable of the two: a + // row that has stopped matching reads as instructions to edit something that is + // not there any more. + const found = check(['a.js', 'b.js'], ['a.js', 'b.js'], ['a.js']) + assert.strictEqual(found.length, 1) + assert.match(found[0], /b\.js.*no longer mentions/s) +}) + +test('a listed file that has been deleted fails', () => { + const found = check(['a.js'], ['a.js', 'gone.js'], ['a.js']) + assert.strictEqual(found.length, 1) + assert.match(found[0], /gone\.js.*does not exist/s) +}) + +test('a duplicated row fails', () => { + const found = check(['a.js'], ['a.js', 'a.js'], ['a.js']) + assert.ok(found.some((p) => /listed in the checklist twice/.test(p))) +}) + +test('the checklist file itself is exempt', () => { + // It is a table OF the placeholder, so it would otherwise always list itself. + assert.deepStrictEqual(check(['README.md'], [], ['README.md']), []) +}) + +test('the placeholder pattern matches every form a rename touches', () => { + for (const text of [ + "const ID = 'examplegame'", + 'ExamplegameWorldStatus', + 'name: "Example Game"', + "worldName: 'Example World'", + 'examplegame_world_status', + 'example-game', + ]) { + assert.ok(PLACEHOLDER.test(text), `should match: ${text}`) + } +}) + +test('the placeholder pattern does not fire on ordinary prose', () => { + // The reason the id is `examplegame` rather than `example`: a check that + // false-alarms on the word "example" in a comment is a check whose failures + // stop being read. + for (const text of [ + '// for example, a router mounted under /shard', + 'an example of what to catch', + 'exampleValue', + 'the game world', + ]) { + assert.ok(!PLACEHOLDER.test(text), `should not match: ${text}`) + } +}) + +test('checklistPaths reads only the rows between the markers', () => { + const md = [ + '# Heading', + '', + 'Prose quoting `not/a/row.js` and a tree diagram.', + '', + '', + '', + '| File | What to change |', + '| --- | --- |', + '| `module.json` | the id |', + '| `server/core.js` | the message |', + '', + '', + '', + 'More prose about `also/not/a/row.js`.', + ].join('\n') + assert.deepStrictEqual(checklistPaths(md), ['module.json', 'server/core.js']) +}) + +test('a README with no markers is an error, not an empty checklist', () => { + // Silently reading zero entries would make every later assertion vacuous, and + // the check would pass on a README whose checklist someone deleted. + assert.throws(() => checklistPaths('# Heading\n\nno markers here\n'), /rename-sites/) +}) diff --git a/template/.gitea/workflows/release.yml b/template/.gitea/workflows/release.yml new file mode 100644 index 0000000..5718407 --- /dev/null +++ b/template/.gitea/workflows/release.yml @@ -0,0 +1,270 @@ +# ── Publish an installable bundle (Gitea Actions) ───────────────────────── +# +# **This file does nothing where it sits.** Gitea only runs workflows found at +# the REPOSITORY root, and inside the kit this one is at `template/.gitea/…`. It +# arms itself the moment your copy of `template/` is a repository of its own — +# which is the point: packaging is the part of a module you cannot guess at, and +# copying a file beats retyping one out of a chapter. +# +# There is a GitHub Actions twin next door in `.github/workflows/release.yml`. +# Keep whichever host you use and delete the other. +# +# ── What a release IS ───────────────────────────────────────────────────── +# +# **An operator never builds anything.** That constraint is the shape of the +# whole module system, so a release is not source: it is the directory core's +# loader expects to find at `modules//`, already assembled — the prebuilt +# client chunk, any runtime dependency installed, the schema fragment and the +# OpenAPI fragment — packed exactly as it will be unpacked. The website's admin +# install downloads the tarball, verifies it against the `sha256` in the manifest, +# and unpacks it onto the volume. Nothing runs `npm` on the way. +# +# ── The version is DECLARED, not derived ────────────────────────────────── +# +# Your module already has one authoritative version: `module.json`'s. It is what +# core records in `installed_modules`, what the admin screen shows, and it sits +# beside the `coreApi` range you have to consider a bump against. Two sources for +# one number is how they drift — so **a release happens when a push to `main` +# leaves `module.json` at a version that has no release yet.** Bumping the version +# is an ordinary reviewed pull request; publishing is this file's business. +# +# It follows that this workflow never writes to a branch. It tags and publishes, +# so a protected `main` needs no push exception — which matters, because a release +# engine that has to push to `main` stops working the day someone tightens the +# rule. Re-running on an already-released version is a no-op. +# +# ── Before this can run ─────────────────────────────────────────────────── +# +# 1. Change GITEA_HOST and REPO below to yours. +# 2. Settings → Actions → Secrets: add REGISTRY_TOKEN, a Gitea access token +# with `write:repository`, so the job can push the tag and create the release. + +name: Release + +on: + push: + branches: [main] + +concurrency: + group: release-module + cancel-in-progress: false + +env: + # ── CHANGE THESE ──────────────────────────────────────────────────────── + GITEA_HOST: gitea.example.com + REPO: your-org/your-module + +jobs: + release: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Decide whether this commit releases + id: plan + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -euo pipefail + ID="$(node -p "require('./module.json').id")" + VERSION="$(node -p "require('./module.json').version")" + echo "module.json: ${ID} ${VERSION}" + + # Does a release already exist for this version? 404 means no, 200 means + # yes, and anything else — a network failure, a bad token — is not + # evidence of absence. Guessing "no" would publish over a good release, + # so refuse instead. + HTTP="$(curl -s -o /dev/null -w '%{http_code}' \ + -H "Authorization: token $(printf '%s' "${REGISTRY_TOKEN:-}" | tr -d '\r\n')" \ + "https://${GITEA_HOST}/api/v1/repos/${REPO}/releases/tags/v${VERSION}" || echo 000)" + + case "$HTTP" in + 404) RELEASE=true ;; + 200) RELEASE=false; echo "v${VERSION} is already released — nothing to do." ;; + *) echo "::error::Could not determine whether v${VERSION} is released (HTTP ${HTTP}). Refusing to guess."; exit 1 ;; + esac + + echo "id=${ID}" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT" + echo "release=${RELEASE}" >> "$GITHUB_OUTPUT" + + # Before anything is built or tagged, so a repo without secrets fails + # legibly rather than half-publishing: the tag push can succeed on the + # credential `actions/checkout` left in the git config while the release API + # call 401s, leaving the repo tagged and unreleased. + - name: Verify release credentials are configured + if: ${{ steps.plan.outputs.release == 'true' }} + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -euo pipefail + if [ -z "$(printf '%s' "${REGISTRY_TOKEN:-}" | tr -d '\r\n')" ]; then + echo "::error::Missing Actions secret REGISTRY_TOKEN (needs write:repository) on ${REPO}." + exit 1 + fi + echo "Release credentials present." + + - name: Build the client chunk + if: ${{ steps.plan.outputs.release == 'true' }} + run: | + npm ci --prefix client + npm run build --prefix client + + # `--omit=dev`, and then PACKED. express and swagger-autogen are build- and + # test-time only — the shipped half is handed express on `ctx` — so this + # installs only what `dependencies` declares. Node resolves those by walking + # up from `modules//server/`, which is why they ship INSIDE the tarball + # rather than being installed on the operator's box. + # + # With no runtime dependencies at all this produces an empty tree and the + # copy below is a no-op. That is the shape to aim for. + - name: Install the shipped runtime dependencies + if: ${{ steps.plan.outputs.release == 'true' }} + run: npm ci --omit=dev --prefix server + + # ── Assemble exactly what an operator's volume gets ────────────────── + # + # Stated as an INCLUDE list, never an exclude list. An exclude list ships + # whatever it forgot: the day someone adds `server/tools/` with a scratch + # credential in it, an exclude list packs it and nobody finds out. + - name: Assemble the bundle + if: ${{ steps.plan.outputs.release == 'true' }} + run: | + set -euo pipefail + ID="${{ steps.plan.outputs.id }}" + VERSION="${{ steps.plan.outputs.version }}" + OUT="dist/${ID}-${VERSION}" + rm -rf dist && mkdir -p "$OUT" + + # The manifest core reads, the OpenAPI fragment, and the licence the + # code is under — a bundle shipping GPL code without its licence is not + # distributable. + cp module.json swagger-fragment.json LICENSE.md README.md "$OUT/" + + # The server half, minus everything that never runs inside core's + # process: no `test/`, no `scripts/`, no `swagger/`. + mkdir -p "$OUT/server" + for d in boot.js core.js index.js db model router; do + cp -r "server/$d" "$OUT/server/" + done + cp server/package.json "$OUT/server/" + [ -d server/node_modules ] && cp -r server/node_modules "$OUT/server/" || true + + # The client half is the BUILT chunk only. `client/src` is source an + # operator has no use for and core will never read. + mkdir -p "$OUT/client/dist" + cp client/dist/entry.js "$OUT/client/dist/" + + # Prove the bundle is loadable before publishing it: these are the exact + # paths core's loader resolves out of module.json. A release whose entry + # point is missing otherwise fails on an operator's box, as a + # `startup_failed` row, instead of here. + node -e ' + const fs = require("fs"), path = require("path"); + const root = process.argv[1]; + const m = JSON.parse(fs.readFileSync(path.join(root, "module.json"), "utf8")); + for (const p of [m.server, m.schema, m.purge, m.client && m.client.entry, "swagger-fragment.json"]) { + if (!p) continue; + if (!fs.existsSync(path.join(root, p))) { console.error("bundle is missing " + p); process.exit(1); } + } + console.log("bundle contents check: ok"); + ' "$OUT" + + tar -C dist -czf "dist/${ID}-${VERSION}.tar.gz" "${ID}-${VERSION}" + rm -rf "$OUT" + + SHA="$(sha256sum "dist/${ID}-${VERSION}.tar.gz" | cut -d' ' -f1)" + SIZE="$(stat -c%s "dist/${ID}-${VERSION}.tar.gz")" + + # The install manifest — the URL an operator pastes into Admin → + # Modules. A per-asset sha256 fetched over HTTPS, no signatures. + jq -n \ + --arg id "$ID" \ + --arg name "$(node -p "require('./module.json').name")" \ + --arg version "$VERSION" \ + --arg coreApi "$(node -p "require('./module.json').coreApi")" \ + --arg artifact "${ID}-${VERSION}.tar.gz" \ + --arg sha256 "$SHA" \ + --argjson size "$SIZE" \ + --arg url "https://${GITEA_HOST}/${REPO}/releases/download/v${VERSION}/${ID}-${VERSION}.tar.gz" \ + '{schema:1, id:$id, name:$name, version:$version, coreApi:$coreApi, + artifact:$artifact, url:$url, sha256:$sha256, size:$size}' \ + > "dist/${ID}-${VERSION}.json" + + echo "${SHA} ${ID}-${VERSION}.tar.gz" > dist/SHA256SUMS + cat "dist/${ID}-${VERSION}.json" + + - name: Write the changelog + if: ${{ steps.plan.outputs.release == 'true' }} + run: | + set -euo pipefail + ID="${{ steps.plan.outputs.id }}" + VERSION="${{ steps.plan.outputs.version }}" + LAST_TAG="$(git describe --tags --match 'v*' --abbrev=0 2>/dev/null || true)" + RANGE="${LAST_TAG:+${LAST_TAG}..}HEAD" + { + echo "## ${ID} v${VERSION}" + echo + echo "Install from the website's Admin → Modules screen by pasting the URL of" + echo "\`${ID}-${VERSION}.json\`, or unpack the tarball onto the modules volume as" + echo "\`modules/${ID}/\`. Requires a core whose \`MODULE_API_VERSION\` satisfies" + echo "\`$(node -p "require('./module.json').coreApi")\`." + echo + echo "### Changes" + if [ -n "$LAST_TAG" ]; then echo "Since ${LAST_TAG}:"; fi + git log --no-merges --format='- %s' $RANGE || true + echo + echo "### Verifying this download" + echo + echo "Releases are **unsigned** — the \`sha256\` in \`${ID}-${VERSION}.json\` is the" + echo "trust anchor, and the website verifies it before unpacking." + echo + echo '```bash' + echo "sha256sum -c SHA256SUMS --ignore-missing" + echo '```' + } > dist/CHANGELOG.md + + - name: Tag the release + if: ${{ steps.plan.outputs.release == 'true' }} + run: | + set -euo pipefail + TAG="${{ steps.plan.outputs.tag }}" + git config user.name 'Module CI' + git config user.email 'ci@example.com' + git tag -a "$TAG" -m "${{ steps.plan.outputs.id }} ${TAG}" + git push origin "$TAG" + + - name: Create the release and upload the bundle + if: ${{ steps.plan.outputs.release == 'true' }} + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -euo pipefail + ID="${{ steps.plan.outputs.id }}" + TAG="${{ steps.plan.outputs.tag }}" + VERSION="${{ steps.plan.outputs.version }}" + API="https://${GITEA_HOST}/api/v1/repos/${REPO}" + CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')" + + REL_ID="$(curl -sSf -X POST "${API}/releases" \ + -H "Authorization: token ${CI_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg tag "$TAG" --arg body "$(cat dist/CHANGELOG.md)" \ + '{tag_name:$tag, name:$tag, body:$body, draft:false, prerelease:false}')" \ + | jq -r '.id')" + echo "Created release ${TAG} (id=${REL_ID})" + + for f in "${ID}-${VERSION}.tar.gz" "${ID}-${VERSION}.json" SHA256SUMS; do + curl -sSf -X POST "${API}/releases/${REL_ID}/assets?name=${f}" \ + -H "Authorization: token ${CI_TOKEN}" \ + -F "attachment=@dist/${f}" >/dev/null + echo " uploaded ${f}" + done diff --git a/template/.github/workflows/release.yml b/template/.github/workflows/release.yml new file mode 100644 index 0000000..ce30456 --- /dev/null +++ b/template/.github/workflows/release.yml @@ -0,0 +1,241 @@ +# ── Publish an installable bundle (GitHub Actions) ──────────────────────── +# +# The GitHub twin of `.gitea/workflows/release.yml`. **Keep whichever host your +# module lives on and delete the other** — nothing breaks if both are present, +# but two release engines racing to tag the same version is a mess nobody needs. +# +# **This file does nothing where it sits.** Workflows run only from the +# REPOSITORY root, and inside the kit this one is at `template/.github/…`. It arms +# itself the moment your copy of `template/` is a repository of its own. +# +# Nothing about a module's release depends on where it is hosted: core installs +# from a **URL**. Point Admin → Modules at the install manifest this job attaches +# to the release and add your host to the website's `MODULE_SOURCE_HOSTS` +# allowlist, and a module released here installs exactly like one released +# anywhere else. +# +# ── What a release IS ───────────────────────────────────────────────────── +# +# **An operator never builds anything.** So a release is not source: it is the +# directory core's loader expects to find at `modules//`, already assembled — +# the prebuilt client chunk, any runtime dependency installed, the schema fragment +# and the OpenAPI fragment — packed exactly as it will be unpacked. The website +# downloads the tarball, verifies it against the `sha256` in the manifest, and +# unpacks it onto the volume. Nothing runs `npm` on the way. +# +# ── The version is DECLARED, not derived ────────────────────────────────── +# +# Your module already has one authoritative version: `module.json`'s. It is what +# core records in `installed_modules` and what the admin screen shows. Two sources +# for one number is how they drift — so **a release happens when a push to `main` +# leaves `module.json` at a version that has no release yet.** Bumping the version +# is an ordinary reviewed pull request; publishing is this file's business. +# +# This workflow never writes to a branch, so a protected `main` needs no push +# exception. Re-running on an already-released version is a no-op. +# +# ── Before this can run ─────────────────────────────────────────────────── +# +# Nothing to configure. `GITHUB_TOKEN` is provided automatically; the `contents: +# write` permission below is what lets it push a tag and create a release. + +name: Release + +on: + push: + branches: [main] + +permissions: + contents: write + +concurrency: + group: release-module + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Decide whether this commit releases + id: plan + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + ID="$(node -p "require('./module.json').id")" + VERSION="$(node -p "require('./module.json').version")" + echo "module.json: ${ID} ${VERSION}" + + # `gh release view` exits non-zero when the release does not exist — but + # it also exits non-zero when the API is unreachable, and those two are + # not the same answer. Ask for the status code instead: 404 means no, + # 200 means yes, anything else is not evidence of absence, and guessing + # "no" would publish over a good release. + HTTP="$(curl -s -o /dev/null -w '%{http_code}' \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/v${VERSION}" || echo 000)" + + case "$HTTP" in + 404) RELEASE=true ;; + 200) RELEASE=false; echo "v${VERSION} is already released — nothing to do." ;; + *) echo "::error::Could not determine whether v${VERSION} is released (HTTP ${HTTP}). Refusing to guess."; exit 1 ;; + esac + + echo "id=${ID}" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT" + echo "release=${RELEASE}" >> "$GITHUB_OUTPUT" + + - name: Build the client chunk + if: ${{ steps.plan.outputs.release == 'true' }} + run: | + npm ci --prefix client + npm run build --prefix client + + # `--omit=dev`, and then PACKED. express and swagger-autogen are build- and + # test-time only — the shipped half is handed express on `ctx` — so this + # installs only what `dependencies` declares. Node resolves those by walking + # up from `modules//server/`, which is why they ship INSIDE the tarball + # rather than being installed on the operator's box. + # + # With no runtime dependencies at all this produces an empty tree and the + # copy below is a no-op. That is the shape to aim for. + - name: Install the shipped runtime dependencies + if: ${{ steps.plan.outputs.release == 'true' }} + run: npm ci --omit=dev --prefix server + + # ── Assemble exactly what an operator's volume gets ────────────────── + # + # Stated as an INCLUDE list, never an exclude list. An exclude list ships + # whatever it forgot: the day someone adds `server/tools/` with a scratch + # credential in it, an exclude list packs it and nobody finds out. + - name: Assemble the bundle + if: ${{ steps.plan.outputs.release == 'true' }} + run: | + set -euo pipefail + ID="${{ steps.plan.outputs.id }}" + VERSION="${{ steps.plan.outputs.version }}" + OUT="dist/${ID}-${VERSION}" + rm -rf dist && mkdir -p "$OUT" + + # The manifest core reads, the OpenAPI fragment, and the licence the + # code is under — a bundle shipping GPL code without its licence is not + # distributable. + cp module.json swagger-fragment.json LICENSE.md README.md "$OUT/" + + # The server half, minus everything that never runs inside core's + # process: no `test/`, no `scripts/`, no `swagger/`. + mkdir -p "$OUT/server" + for d in boot.js core.js index.js db model router; do + cp -r "server/$d" "$OUT/server/" + done + cp server/package.json "$OUT/server/" + [ -d server/node_modules ] && cp -r server/node_modules "$OUT/server/" || true + + # The client half is the BUILT chunk only. + mkdir -p "$OUT/client/dist" + cp client/dist/entry.js "$OUT/client/dist/" + + # Prove the bundle is loadable before publishing it: these are the exact + # paths core's loader resolves out of module.json. A release whose entry + # point is missing otherwise fails on an operator's box, as a + # `startup_failed` row, instead of here. + node -e ' + const fs = require("fs"), path = require("path"); + const root = process.argv[1]; + const m = JSON.parse(fs.readFileSync(path.join(root, "module.json"), "utf8")); + for (const p of [m.server, m.schema, m.purge, m.client && m.client.entry, "swagger-fragment.json"]) { + if (!p) continue; + if (!fs.existsSync(path.join(root, p))) { console.error("bundle is missing " + p); process.exit(1); } + } + console.log("bundle contents check: ok"); + ' "$OUT" + + tar -C dist -czf "dist/${ID}-${VERSION}.tar.gz" "${ID}-${VERSION}" + rm -rf "$OUT" + + SHA="$(sha256sum "dist/${ID}-${VERSION}.tar.gz" | cut -d' ' -f1)" + SIZE="$(stat -c%s "dist/${ID}-${VERSION}.tar.gz")" + + # The install manifest — the URL an operator pastes into Admin → + # Modules. A per-asset sha256 fetched over HTTPS, no signatures. + jq -n \ + --arg id "$ID" \ + --arg name "$(node -p "require('./module.json').name")" \ + --arg version "$VERSION" \ + --arg coreApi "$(node -p "require('./module.json').coreApi")" \ + --arg artifact "${ID}-${VERSION}.tar.gz" \ + --arg sha256 "$SHA" \ + --argjson size "$SIZE" \ + --arg url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/download/v${VERSION}/${ID}-${VERSION}.tar.gz" \ + '{schema:1, id:$id, name:$name, version:$version, coreApi:$coreApi, + artifact:$artifact, url:$url, sha256:$sha256, size:$size}' \ + > "dist/${ID}-${VERSION}.json" + + echo "${SHA} ${ID}-${VERSION}.tar.gz" > dist/SHA256SUMS + cat "dist/${ID}-${VERSION}.json" + + - name: Write the changelog + if: ${{ steps.plan.outputs.release == 'true' }} + run: | + set -euo pipefail + ID="${{ steps.plan.outputs.id }}" + VERSION="${{ steps.plan.outputs.version }}" + LAST_TAG="$(git describe --tags --match 'v*' --abbrev=0 2>/dev/null || true)" + RANGE="${LAST_TAG:+${LAST_TAG}..}HEAD" + { + echo "## ${ID} v${VERSION}" + echo + echo "Install from the website's Admin → Modules screen by pasting the URL of" + echo "\`${ID}-${VERSION}.json\`, or unpack the tarball onto the modules volume as" + echo "\`modules/${ID}/\`. Requires a core whose \`MODULE_API_VERSION\` satisfies" + echo "\`$(node -p "require('./module.json').coreApi")\`." + echo + echo "The website only installs from hosts on its \`MODULE_SOURCE_HOSTS\` allowlist —" + echo "an operator installing this needs \`github.com\` on theirs." + echo + echo "### Changes" + if [ -n "$LAST_TAG" ]; then echo "Since ${LAST_TAG}:"; fi + git log --no-merges --format='- %s' $RANGE || true + echo + echo "### Verifying this download" + echo + echo "Releases are **unsigned** — the \`sha256\` in \`${ID}-${VERSION}.json\` is the" + echo "trust anchor, and the website verifies it before unpacking." + echo + echo '```bash' + echo "sha256sum -c SHA256SUMS --ignore-missing" + echo '```' + } > dist/CHANGELOG.md + + - name: Tag and publish + if: ${{ steps.plan.outputs.release == 'true' }} + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + ID="${{ steps.plan.outputs.id }}" + TAG="${{ steps.plan.outputs.tag }}" + VERSION="${{ steps.plan.outputs.version }}" + + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git tag -a "$TAG" -m "${ID} ${TAG}" + git push origin "$TAG" + + gh release create "$TAG" \ + --title "$TAG" \ + --notes-file dist/CHANGELOG.md \ + "dist/${ID}-${VERSION}.tar.gz" \ + "dist/${ID}-${VERSION}.json" \ + dist/SHA256SUMS diff --git a/template/LICENSE.md b/template/LICENSE.md new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/template/LICENSE.md @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/template/README.md b/template/README.md new file mode 100644 index 0000000..04b17c0 --- /dev/null +++ b/template/README.md @@ -0,0 +1,145 @@ +# The template module + +A Runic Gateway module that builds, loads, and does almost nothing. Copy it, +rename it, and you have a running module before you have read a chapter. + +Installed into a core, it adds: + +- **one public page** at `/examplegame/status`, and a nav row pointing at it; +- **one API route**, `GET /api/v1/public/world/status`, described in an OpenAPI + fragment core merges into its own `/api/docs`; +- **one table**, `examplegame_world_status`, created by an idempotent schema + fragment and dropped by a purge file; +- **both lifecycle hooks**, so there is something to see at boot and at shutdown. + +That is deliberately less than your module will do. What it is *complete* about is +the shape: every seam a real module uses is here once, with the reasoning next to +it, and CI proves the whole thing still builds against a pinned core. + +## The tree + +``` +module.json what core reads first — id, version, coreApi, mounts +server/ + index.js register(ctx, api) — the entire server-side handshake + core.js the lazy accessors over ctx; read this second + boot.js onBoot / onShutdown + db/schema.sql idempotent, replayed every boot + db/purge.sql destructive, run only by an explicit admin purge + model/worldStatus/ the .db.js / .model.js pair + router/public/ one router, one controller, the #swagger annotations + swagger/doc.js tags and schemas the annotations refer to + scripts/checkImports.js the module boundary, enforced + scripts/swaggerFragment.js generates swagger-fragment.json from your own routes + test/ the suites — start with entry.test.js +client/ + vite.config.js the library build: anchored aliases, external: [] + src/entry.jsx registers routes and nav at evaluation time + src/core.js what core hands you: the seven-member UI kit + src/shim/ the four shared dependencies, re-exported from core + src/routes/public/ the page + scripts/checkExternals.js asks the BUILT chunk whether a bare import survived + test/ build.test.js and registration.test.js +.gitea/workflows/release.yml packaging CI — Gitea +.github/workflows/release.yml the same, for GitHub. Keep one, delete the other. +swagger-fragment.json generated; commit it +``` + +Neither workflow runs while it sits inside the kit — a workflow is only read from +a repository root. They arm themselves when your copy is a repository of its own. + +## Build it + +```bash +npm ci --prefix server +npm test --prefix server +npm run check:imports --prefix server + +npm ci --prefix client +npm run build --prefix client # → client/dist/entry.js, the chunk that ships +npm run check:externals --prefix client +npm test --prefix client # build FIRST: two of these tests read the chunk +``` + +`npm test` in `client/` passes with no build, by skipping the tests that need one. +That is on purpose — the suite has to be runnable before the build — and it means +**a CI job that tests without building is a job asking nothing.** Build first. + +Regenerate the OpenAPI fragment whenever a route or an annotation changes: + +```bash +npm run swagger --prefix server # writes swagger-fragment.json +npm run check:swagger --prefix server # fails if it is stale +``` + +## Install it + +Three supported ways, and none of them builds anything on the operator's machine: + +1. **Admin → Modules**, pasting the URL of an install manifest — the JSON the + release workflow attaches beside the tarball. This is how an operator installs + your module. +2. **The `MODULES` environment variable**, `@=`, for a + deployment that declares its module set rather than clicking it. +3. **A directory on the volume.** Copy this whole tree to `/modules//` + and restart. The fastest loop while you are developing. + +For (3): **copy, do not symlink.** The loader lists directory entries and a +symlink is not a directory, so a linked module is skipped in silence. + +## Rename it + +Change `id` in `module.json` first, then work down the list. Nothing here is +subtle, and the suites catch most of a half-finished job: `schema.test.js` fails +the moment a table name stops matching the id, and `registration.test.js` fails +when a nav row stops matching its route. + +Your id must match `^[a-z][a-z0-9-]{1,31}$`, must equal the directory name core +loads you from, and becomes your table prefix — so **no hyphen unless you enjoy +backticking table names**. + + + +| File | What to change | +| --- | --- | +| `module.json` | `id`, `name`, `version`, the `mounts` prefix, `capabilities` | +| `server/package.json` | package `name` and `description` | +| `server/core.js` | the message every accessor throws | +| `server/boot.js` | the placeholder world name | +| `server/db/schema.sql` | every table name — the prefix must be your id | +| `server/db/purge.sql` | the same table names | +| `server/model/worldStatus/worldStatus.db.js` | the `TABLE` constant | +| `server/router/public/world.router.js` | the `#swagger.tags` name | +| `server/swagger/doc.js` | the tag, and the `Examplegame…` schema prefix | +| `server/scripts/swaggerFragment.js` | the generated fragment's `info.title` | +| `server/test/_fakes.js` | `ctx.moduleId` | +| `server/test/worldStatus.test.js` | the fixture's world name | +| `server/package-lock.json` | **regenerated** — `npm install --prefix server` | +| `client/package.json` | package `name` and `description` | +| `client/vite.config.js` | the guard plugin's `name` | +| `client/src/core.js` | the console tag on the identity check | +| `client/src/shim/rg.js` | the console tag on the missing-global error | +| `client/src/entry.jsx` | `ID`, and every route path and nav `to` | +| `client/test/registration.test.js` | the example path in the comment | +| `client/package-lock.json` | **regenerated** — `npm install --prefix client` | +| `swagger-fragment.json` | **regenerated** — `npm run swagger --prefix server` | + + + +That table is checked. `scripts/checkRenameSites.js` at the root of this kit +compares it against the tree on every pull request: a file that still mentions the +placeholder and is not listed fails the build, and so does a listed file with +nothing left to rename. A checklist nobody verifies is a checklist that is wrong +by the second edit. + +Two things you do **not** rename: the mount prefix `/world` need not be your id +(the server's prefix namespace is shared with core's, and `/status`, `/settings`, +`/version` and `/contact` are already taken), and the `world` / `worldStatus` +naming throughout is ordinary vocabulary you should replace with your own domain's +when you replace the feature. + +## Licence + +GPL-3.0-or-later, like everything else in this project — see +[LICENSE.md](LICENSE.md). This directory is meant to be copied and made yours; it +carries that licence, and so does anything derived from it. diff --git a/template/client/package-lock.json b/template/client/package-lock.json new file mode 100644 index 0000000..ed0afd8 --- /dev/null +++ b/template/client/package-lock.json @@ -0,0 +1,1792 @@ +{ + "name": "examplegame-module-client", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "examplegame-module-client", + "version": "0.1.0", + "license": "GPL-3.0-or-later", + "devDependencies": { + "@vitejs/plugin-react": "^4.3.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2", + "vite": "^5.4.8" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz", + "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.405", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.405.tgz", + "integrity": "sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/template/client/package.json b/template/client/package.json new file mode 100644 index 0000000..4587e0c --- /dev/null +++ b/template/client/package.json @@ -0,0 +1,24 @@ +{ + "name": "examplegame-module-client", + "version": "0.1.0", + "private": true, + "description": "Client half of the Example Game module — a prebuilt ESM chunk core injects into its own SPA", + "license": "GPL-3.0-or-later", + "type": "module", + "scripts": { + "build": "vite build", + "test": "node --test", + "check:externals": "node scripts/checkExternals.js" + }, + "engines": { + "node": ">=20" + }, + "//dependencies": "Deliberately none that ship. react, react-dom/client, react/jsx-runtime and react-router-dom are aliased to the shims in src/shim/ and arrive at runtime on window.__rg - there is exactly one React in the page and core owns it (MODULE_API.md 3.2, 3.6). They are devDependencies so that Vite and the JSX transform can resolve them during the build, and for no other reason.", + "devDependencies": { + "@vitejs/plugin-react": "^4.3.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2", + "vite": "^5.4.8" + } +} diff --git a/template/client/scripts/checkExternals.js b/template/client/scripts/checkExternals.js new file mode 100644 index 0000000..031b226 --- /dev/null +++ b/template/client/scripts/checkExternals.js @@ -0,0 +1,172 @@ +#!/usr/bin/env node +// ── §5.1's client half — what stayed a bare import in the built chunk ────── +// +// The server half's boundary check reads source. The client half's has to read +// the BUILD OUTPUT, because the failure it exists to catch is invisible in +// source: `import { useState } from 'react'` is correct in every file, and +// whether it ends up as core's React or as a second copy welded into the chunk +// is decided by vite.config.js's aliases. A missed alias changes nothing you can +// see until a hook throws in the browser. +// +// So: build, then ask the artifact two questions. +// +// 1. **Is there a bare import left?** There must not be. Aliased shims are +// bundled, so a surviving bare specifier means an alias missed and +// `external` caught it — the loud failure the config prefers, but still a +// failure, and better found here than by a browser refusing to load. +// 2. **Did a shared dependency get bundled?** React's own source has +// fingerprints that no module of ours would contain by accident. Finding +// one means the chunk carries a second React, which is the silent version +// of the same mistake and the one worth the fingerprint check. +// +// Run after `npm run build`, in CI, on the artifact that ships. + +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const CHUNK = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'entry.js') + +/** + * Which characters of the chunk are inside a string, template or comment. + * + * **A check that reads code with a regexp fails on code that talks about + * itself.** The first real chunk this script ever saw — slice 3's, the first + * with any content in it — was rejected for importing `" }),\n !l && …`, + * because a button reading "Approve and import" put the token `import` + * immediately before a quote and the pattern could not tell that from a + * statement. Slice 0's chunk was 0.2 kB and this branch had never run against + * anything. + * + * The server half hit the same wall from the other side and answered it the same + * way (`server/scripts/checkImports.js`): a character walk, not a cleverer + * regexp. There is no regexp that distinguishes a keyword from the same letters + * inside a string, because that distinction is a property of the parse. + * + * A mask rather than a rewrite, because the two halves of a real import — the + * keyword and the specifier — sit on opposite sides of the boundary: the keyword + * must be OUTSIDE a string and the specifier must be a string. Blanking strings + * would take the answer with the noise. + */ +export function stringMask(src) { + const inString = new Uint8Array(src.length) + let i = 0 + while (i < src.length) { + const c = src[i] + const two = src.slice(i, i + 2) + if (two === '//') { + const nl = src.indexOf('\n', i) + const end = nl === -1 ? src.length : nl + inString.fill(1, i, end) + i = end + } else if (two === '/*') { + const close = src.indexOf('*/', i + 2) + const end = close === -1 ? src.length : close + 2 + inString.fill(1, i, end) + i = end + } else if (c === '"' || c === "'" || c === '`') { + // The opening quote itself stays unmasked: a specifier is read starting + // at its quote, and the regexp below anchors on that. + i += 1 + while (i < src.length && src[i] !== c) { + // A backslash escapes the next character, including the closing quote. + const step = src[i] === '\\' ? 2 : 1 + inString.fill(1, i, Math.min(i + step, src.length)) + i += step + } + i += 1 + } else { + i += 1 + } + } + return inString +} + +// Static and dynamic imports that survived into the output. A relative or +// absolute specifier is a chunk that was split, which this build does not do — +// `lib` mode with one entry emits one file — so anything here is a bare name. +// +// **This pattern used to require whitespace after `import`, and so could not see +// the one shape the build actually emits.** Minified Rollup output is +// `import{useState}from"react"`, with no space anywhere in it; the old +// `import\s+[^'"]*?from` needed at least one, fell through to the bare-specifier +// alternative, met `{` instead of a quote and matched nothing. A bare named +// import — the most likely way for an alias to miss — would have passed this +// check silently. It was found by writing the test for the false POSITIVE above +// it, which is the argument for testing a check against both answers. +// +// `(?:^|[^\w$.])` rather than a whitespace class, so `a.import(x)` and +// `myimport"x"` are excluded for the right reason: `import` must not be preceded +// by an identifier character or a dot. `[^'"()]*?` cannot swallow a dynamic +// import's parenthesis. +const IMPORTS = /(?:^|[^\w$.])import\s*(?:\(\s*|[^'"()]*?from\s*)?['"]([^'"]+)['"]/g + +/** Every bare specifier the chunk still imports at runtime. */ +export function bareImports(chunk) { + const masked = stringMask(chunk) + const bare = new Set() + for (const match of chunk.matchAll(IMPORTS)) { + // Where the `import` keyword itself starts — one past the leading delimiter, + // unless the match began at position 0. + const keywordAt = match.index + (match[0].startsWith('import') ? 0 : 1) + if (masked[keywordAt]) continue // the letters, inside a string. Not a statement. + const specifier = match[1] + if (!specifier.startsWith('.') && !specifier.startsWith('/')) bare.add(specifier) + } + return [...bare] +} + +// Fingerprints from the shared libraries' own source. Each is a string those +// packages ship and this module has no other reason to contain. +// +// These are matched against the RAW chunk, deliberately unmasked: a bundled +// library's source arrives as code AND as its own error-message strings, and +// masking would discard half the evidence. The direction of the risk is opposite +// to the import check's — here a false positive is a fingerprint too generic, +// which is a fixable choice of probe, not a property of the parse. +const BUNDLED = [ + { what: 'react', probe: 'react.development.js' }, + { what: 'react', probe: 'Invalid hook call' }, + { what: 'react-dom', probe: 'react-dom.development.js' }, + { what: 'react-router-dom', probe: 'useRoutes() may be used only in the context of a component' }, +] + +/** Every problem with this chunk, as sentences. Empty means it ships. */ +export function problemsWith(chunk) { + const problems = [] + const bare = bareImports(chunk) + if (bare.length) { + problems.push( + `the chunk still imports ${bare.map((s) => `"${s}"`).join(', ')} — ` + + 'nothing can resolve a bare specifier in the browser without an import map, ' + + 'and CSP forbids one. Alias it to a shim in vite.config.js (MODULE_API.md §3.6).', + ) + } + for (const { what, probe } of BUNDLED) { + if (chunk.includes(probe)) { + problems.push( + `the chunk appears to BUNDLE ${what} (found ${JSON.stringify(probe)}). ` + + 'There is exactly one React in the page and core owns it — a second copy ' + + 'loads fine and then fails at the first hook (MODULE_API.md §3.2).', + ) + } + } + return problems +} + +// Only when run as a script. Importing this from a test must not read a chunk +// that may not have been built, and must not call process.exit. +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + if (!fs.existsSync(CHUNK)) { + console.error(`No chunk at ${CHUNK} — run \`npm run build\` first.`) + process.exit(1) + } + const problems = problemsWith(fs.readFileSync(CHUNK, 'utf8')) + if (problems.length) { + console.error('\nThe built chunk breaks the shared-dependency rule:\n') + for (const p of problems) console.error(` - ${p}\n`) + process.exit(1) + } + const kb = (fs.statSync(CHUNK).size / 1024).toFixed(1) + console.log(`OK — dist/entry.js (${kb} kB) has no bare imports and bundles no shared dependency.`) +} diff --git a/template/client/src/api.js b/template/client/src/api.js new file mode 100644 index 0000000..d7c54c8 --- /dev/null +++ b/template/client/src/api.js @@ -0,0 +1,34 @@ +// ── This module's own API bindings ──────────────────────────────────────── +// +// Core hands out the request PRIMITIVE and nothing above it (MODULE_API.md +// §3.5): same-origin `/api/v1`, cookies included, JSON in and out, and an +// `ApiError` thrown on any non-2xx. The paths are yours, because the routes at +// the other end are yours — `server/router/**` in this repo serves them. +// +// **Do not build your own fetch wrapper.** The primitive is what carries the +// session cookie, the CSRF handling and the error shape core's `ErrorState` +// knows how to render. A module that calls `fetch` directly gets none of that +// and finds out one page at a time. +// +// Keeping the bindings in one file, ordered the way the routers are, is +// convention rather than contract — but the two halves of every call live in +// different directories and nothing checks them against each other, so anything +// that makes a mismatch easy to see is worth doing. + +import rg from './core.js' + +const { request: req, BASE } = rg.api + +// ── public ──────────────────────────────────────────────────────────────── +// Token-free, same-origin reads. Paths are relative to `/api/v1`, so this hits +// `/api/v1/public/world/status` — the route `server/router/public/world.router.js` +// registers under the `/world` prefix `module.json` declares. +export const world = { + status: () => req('/public/world/status'), +} + +// Exported for the rare caller that needs the base itself — an ``, a +// download link, an EventSource. Reach for `request` first. +export { BASE } + +export default { world, BASE } diff --git a/template/client/src/core.js b/template/client/src/core.js new file mode 100644 index 0000000..74bd4a4 --- /dev/null +++ b/template/client/src/core.js @@ -0,0 +1,77 @@ +// ── What core hands this module, on the client side ──────────────────────── +// +// The client twin of `server/core.js`, and deliberately much simpler than it. +// Every page imports its layout, its state components and its hooks from here, +// so the boundary is one file. The normative contract is MODULE_API.md §3.2 and +// §3.4. +// +// **Why this is a plain read and the server's is a lazy accessor.** On the +// server, `ctx` arrives at `register(ctx)` — after every `require` has already +// run — so `server/core.js` has to defer resolution to call time or a router +// would capture `undefined` at file scope. There is no such gap here. +// `window.__rg` is published by core's own bundle (client/src/modules/shared.js), +// and every module chunk is a deferred script the server injects *after* that +// bundle's tag, so by the time the first line of this file executes the global +// is already there. Reading it once, at module scope, is safe — and it means a +// component keeps the ordinary `import { PageHeader } from '…'` shape rather +// than being wrapped in an accessor that would cost it its identity. +// +// The absent-global case is handled by `shim/rg.js`, which every shim beside it +// also goes through — the shims touch the global before this file does, so a +// check here would be unreachable. + +import { createElement } from 'react' +import { createRoot } from 'react-dom/client' +import { Link } from 'react-router-dom' +import { rg as shared } from './shim/rg.js' + +const rg = shared() + +// ── The shared-dependency self-check ─────────────────────────────────────── +// +// Keep this. There are two BUILD guards on the same rule — `assertSharedNotBundled` +// in vite.config.js at resolution time, and `scripts/checkExternals.js` on the +// finished artifact — and both reason about the chunk in isolation. Neither can +// see the one failure that only exists once the chunk meets a core: a +// `window.__rg` whose React is not the React that rendered the page. +// +// Identity is the only question worth asking. A second React satisfies every +// type check, renders its first element happily, and then throws about an invalid +// hook call somewhere unrelated — in a component that has nothing to do with it. +if (createElement !== rg.react.createElement || createRoot !== rg.reactDom.createRoot || Link !== rg.router.Link) { + console.error( + '[examplegame] the bindings this chunk imported are not the ones core published — it has bundled ' + + 'its own copy of a shared dependency. Check the aliases in vite.config.js (MODULE_API.md §3.6).', + ) +} + +// The curated kit (§3.4). Seven members, and it is CLOSED: layout, headings, the +// three data-page states, the fetch hook, and read-only access to the session and +// the site's settings. Anything else your pages need — tables, tabs, an editor — +// you bundle yourself, in a `components/` directory of your own. +// +// Closed is a real constraint and it is the price of the boundary being worth +// anything: adding a member is a minor `MODULE_API_VERSION` bump, and changing a +// kit component's props is a major one. Use them, though. A module page that +// ships its own layout is a page that stops looking like the site it is installed +// in, and drifts further every time core changes. +export const { + PublicLayout, + PageHeader, + Loading, + ErrorState, + EmptyState, + useAsync, + useAuth, + useSite, +} = rg.ui + +// The registry, for entry.jsx. Everything else here is read by pages. +export const registry = rg.registry + +// The core API version this module was loaded against. Logged by entry.jsx — +// `module.json`'s `coreApi` range is checked by the loader before this file is +// ever served, so there is nothing to re-check, only something to report. +export const coreApiVersion = rg.version + +export default rg diff --git a/template/client/src/entry.jsx b/template/client/src/entry.jsx new file mode 100644 index 0000000..240db3e --- /dev/null +++ b/template/client/src/entry.jsx @@ -0,0 +1,79 @@ +// ── The client entry point ──────────────────────────────────────────────── +// +// Core serves `dist/entry.js` from your module's directory and injects it into +// its own HTML as a same-origin `