diff --git a/package-lock.json b/package-lock.json index 2f6fc9ed..fc45ca85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fantasy-map-generator", - "version": "1.113.2", + "version": "1.113.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fantasy-map-generator", - "version": "1.113.2", + "version": "1.113.6", "license": "MIT", "dependencies": { "alea": "^1.0.1", @@ -1353,6 +1353,7 @@ "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1393,6 +1394,7 @@ "integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/browser": "4.0.18", "@vitest/mocker": "4.0.18", @@ -1874,6 +1876,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -2160,6 +2163,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2471,6 +2475,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -2546,6 +2551,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", diff --git a/public/modules/dynamic/supporters.js b/public/modules/dynamic/supporters.js index 985e391d..e0b120f9 100644 --- a/public/modules/dynamic/supporters.js +++ b/public/modules/dynamic/supporters.js @@ -606,4 +606,6 @@ dr_not_sam Mie96 Riley Amber Davis -tomtom1969vlbg`; +tomtom1969vlbg +Eric Knight +Adeline Lefizelier`; diff --git a/scripts/bump-version.js b/scripts/bump-version.js new file mode 100644 index 00000000..5604eb80 --- /dev/null +++ b/scripts/bump-version.js @@ -0,0 +1,212 @@ +#!/usr/bin/env node +"use strict"; + +/** + * Bump the project version (patch / minor / major). + * + * Updates: + * - public/versioning.js — VERSION constant + * - package.json — "version" field + * - src/index.html — ?v= cache-busting hashes for changed public/*.js files + * + * Usage: + * node scripts/bump-version.js # interactive prompt + * node scripts/bump-version.js patch # non-interactive + * node scripts/bump-version.js minor + * node scripts/bump-version.js major + * node scripts/bump-version.js --dry-run # preview only, no writes + */ + +const fs = require("fs"); +const path = require("path"); +const readline = require("readline"); +const {execSync} = require("child_process"); + +// --------------------------------------------------------------------------- +// Paths +// --------------------------------------------------------------------------- + +const repoRoot = path.resolve(__dirname, ".."); +const packageJsonPath = path.join(repoRoot, "package.json"); +const versioningPath = path.join(repoRoot, "public", "versioning.js"); +const indexHtmlPath = path.join(repoRoot, "src", "index.html"); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function readFile(filePath) { + return fs.readFileSync(filePath, "utf8"); +} + +function writeFile(filePath, content) { + fs.writeFileSync(filePath, content, "utf8"); +} + +function parseCurrentVersion() { + const content = readFile(versioningPath); + const match = content.match(/const VERSION = "(\d+\.\d+\.\d+)";/); + if (!match) throw new Error("Could not find VERSION constant in public/versioning.js"); + return match[1]; +} + +function bumpVersion(version, type) { + const [major, minor, patch] = version.split(".").map(Number); + if (type === "major") return `${major + 1}.0.0`; + if (type === "minor") return `${major}.${minor + 1}.0`; + return `${major}.${minor}.${patch + 1}`; +} + +function escapeRegExp(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +/** + * Returns public/*.js paths (relative to repo root) that have changed. + * Checks (in order, deduplicating): + * 1. Upstream branch diff — catches everything on a feature/PR branch + * 2. Staged (index) diff — catches files staged but not yet committed + * 3. Last-commit diff — fallback for main / detached HEAD + */ +function getChangedPublicJsFiles() { + const run = cmd => execSync(cmd, {encoding: "utf8", cwd: repoRoot}); + const parseFiles = output => + output + .split("\n") + .map(f => f.trim()) + .filter(f => f.startsWith("public/") && f.endsWith(".js")); + + const seen = new Set(); + const collect = files => files.forEach(f => seen.add(f)); + + // 1. Upstream branch diff + try { + const upstream = run("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}").trim(); + collect(parseFiles(run(`git diff --name-only ${upstream}...HEAD`))); + } catch { + /* no upstream */ + } + + // 2. Staged changes (useful when building before committing) + try { + collect(parseFiles(run("git diff --name-only --cached"))); + } catch { + /* ignore */ + } + + if (seen.size > 0) return [...seen]; + + // 3. Fallback: last commit diff + try { + return parseFiles(run("git diff --name-only HEAD~1 HEAD")); + } catch { + /* shallow / single-commit repo */ + } + + return []; +} + +// --------------------------------------------------------------------------- +// File updaters +// --------------------------------------------------------------------------- + +function updateVersioningJs(newVersion, dry) { + const original = readFile(versioningPath); + const updated = original.replace(/const VERSION = "\d+\.\d+\.\d+";/, `const VERSION = "${newVersion}";`); + if (original === updated) throw new Error("Failed to update VERSION in public/versioning.js"); + if (!dry) writeFile(versioningPath, updated); + console.log(` public/versioning.js → ${newVersion}`); +} + +function updatePackageJson(newVersion, dry) { + const original = readFile(packageJsonPath); + const pkg = JSON.parse(original); + const oldVersion = pkg.version; + pkg.version = newVersion; + if (!dry) writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`); + console.log(` package.json ${oldVersion} → ${newVersion}`); +} + +function updateIndexHtmlHashes(newVersion, dry) { + const changedFiles = getChangedPublicJsFiles(); + + if (changedFiles.length === 0) { + console.log(" src/index.html (no changed public/*.js files detected)"); + return; + } + + let html = readFile(indexHtmlPath); + const updated = []; + + for (const publicPath of changedFiles) { + const htmlPath = publicPath.replace(/^public\//, ""); + const pattern = new RegExp(`${escapeRegExp(htmlPath)}\\?v=[0-9.]+`, "g"); + if (pattern.test(html)) { + html = html.replace(pattern, `${htmlPath}?v=${newVersion}`); + updated.push(htmlPath); + } + } + + if (updated.length > 0) { + if (!dry) writeFile(indexHtmlPath, html); + console.log(` src/index.html hashes updated for:\n - ${updated.join("\n - ")}`); + } else { + console.log( + ` src/index.html (changed files not referenced: ${changedFiles.map(f => f.replace("public/", "")).join(", ")})` + ); + } +} + +// --------------------------------------------------------------------------- +// Prompt +// --------------------------------------------------------------------------- + +function promptBumpType(currentVersion) { + return new Promise(resolve => { + const rl = readline.createInterface({input: process.stdin, output: process.stdout}); + process.stdout.write(`\nCurrent version: ${currentVersion}\nBump type (patch / minor / major) [patch]: `); + rl.once("line", answer => { + rl.close(); + const input = answer.trim().toLowerCase(); + if (input === "minor" || input === "mi") return resolve("minor"); + if (input === "major" || input === "maj") return resolve("major"); + resolve("patch"); + }); + }); +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main() { + const args = process.argv.slice(2).map(a => a.toLowerCase()); + const dry = args.includes("--dry-run"); + + if (dry) console.log("\n[bump-version] DRY RUN — no files will be changed\n"); + + const currentVersion = parseCurrentVersion(); + + // Determine bump type: CLI arg → stdin prompt → default patch + let bumpType; + if (args.includes("major")) bumpType = "major"; + else if (args.includes("minor")) bumpType = "minor"; + else if (args.includes("patch")) bumpType = "patch"; + else if (process.stdin.isTTY) bumpType = await promptBumpType(currentVersion); + else bumpType = "patch"; // non-interactive (CI / pipe) + + const newVersion = bumpVersion(currentVersion, bumpType); + + console.log(`\n[bump-version] ${bumpType}: ${currentVersion} → ${newVersion}\n`); + + updateVersioningJs(newVersion, dry); + updatePackageJson(newVersion, dry); + updateIndexHtmlHashes(newVersion, dry); + + console.log(`\n[bump-version] ${dry ? "(dry run) " : ""}done.\n`); +} + +main().catch(err => { + console.error("\n[bump-version] Error:", err.message || err); + process.exit(1); +}); diff --git a/scripts/install-hooks.js b/scripts/install-hooks.js new file mode 100644 index 00000000..d6bd825e --- /dev/null +++ b/scripts/install-hooks.js @@ -0,0 +1,33 @@ +#!/usr/bin/env node +// Installs scripts/pre-push as .git/hooks/pre-push. +// Runs automatically via the `prepare` npm lifecycle hook (npm install). + +const fs = require("fs"); +const path = require("path"); + +const repoRoot = path.resolve(__dirname, ".."); +const hooksDir = path.join(repoRoot, ".git", "hooks"); +const source = path.join(repoRoot, "scripts", "pre-push"); +const target = path.join(hooksDir, "pre-push"); + +if (!fs.existsSync(path.join(repoRoot, ".git"))) { + // Not a git repo (e.g. Docker / CI build from tarball) — skip silently. + process.exit(0); +} + +if (!fs.existsSync(hooksDir)) { + fs.mkdirSync(hooksDir, {recursive: true}); +} + +try { + // Symlink so changes to scripts/pre-push are reflected immediately. + if (fs.existsSync(target) || fs.lstatSync(target).isSymbolicLink()) { + fs.unlinkSync(target); + } +} catch { + // Target doesn't exist yet — that's fine. +} + +fs.symlinkSync(source, target); +fs.chmodSync(source, 0o755); +console.log("[prepare] Installed git pre-push hook → .git/hooks/pre-push"); diff --git a/scripts/pre-push b/scripts/pre-push new file mode 100755 index 00000000..6e53cb5f --- /dev/null +++ b/scripts/pre-push @@ -0,0 +1,29 @@ +#!/usr/bin/env sh +# Git pre-push hook — automatically bumps the version before pushing. +# Installed by: npm run prepare (scripts/install-hooks.js) +# +# Prompts for patch / minor / major (default: patch), updates: +# - public/versioning.js +# - package.json +# - src/index.html (cache-busting ?v= hashes for changed modules) +# then commits those changes so they are included in the push. + +set -e + +REPO_ROOT="$(git rev-parse --show-toplevel)" + +echo "" +node "$REPO_ROOT/scripts/bump-version.js" + +# Stage files that may have been modified by the bump +git add \ + "$REPO_ROOT/public/versioning.js" \ + "$REPO_ROOT/package.json" \ + "$REPO_ROOT/src/index.html" 2>/dev/null || true + +# Only commit if there are staged changes from the bump +if ! git diff --cached --quiet; then + NEW_VERSION=$(node -e "const f=require('fs');const m=f.readFileSync('$REPO_ROOT/public/versioning.js','utf8').match(/const VERSION = \"([\d.]+)\"/);console.log(m[1])") + git commit -m "chore: bump version to $NEW_VERSION" + echo "[pre-push] Committed version bump → $NEW_VERSION" +fi