diff --git a/README.md b/README.md index 31e80bb..b0ebcb9 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,8 @@ Until the first release tag exists, the root mirrors the preview build. To cut a release: -1. Bump `version` in `package.json` (and the `.app-version` badge in `index.html`). +1. Bump `version` in `package.json` — the only place it lives. The build injects + it into the in-app badge (`scripts/build.js`), and the E2E test derives it. 2. Commit and merge to `main`. 3. Tag the merge commit and push the tag: ```bash diff --git a/doc/arc42.md b/doc/arc42.md index 2dc389b..57f5487 100644 --- a/doc/arc42.md +++ b/doc/arc42.md @@ -300,7 +300,8 @@ flowchart TD ### Release process -1. Bump `version` in `package.json` and the `.app-version` badge in `index.html`. +1. Bump `version` in `package.json` (single source — the build injects it into + the badge and the E2E test derives it). 2. Merge to `main` (updates `/preview`). 3. `git tag vX.Y.Z && git push origin vX.Y.Z` → version-check gate → deploy to root. @@ -321,8 +322,11 @@ flowchart TD notifications; the singleton is reset in tests for order-independence. - **Validation.** Structural validator with no external deps; CI validates all `examples/*.ocf.json`. -- **Versioning.** Version lives in `package.json`, the in-app badge, and the - version test; the CI gate keeps a release tag consistent with `package.json`. +- **Versioning.** The version lives solely in `package.json`. `scripts/build.js` + injects it into the bundle at build time (esbuild `define` → `__APP_VERSION__`, + read from `$npm_package_version`), `main.js` writes it into the badge, and the + E2E test derives the expected value from `package.json`. The CI gate keeps a + release tag consistent with `package.json`. --- diff --git a/index.html b/index.html index d3c53dd..527126f 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,7 @@

OCFeditor

- v0.2.1 +
diff --git a/package.json b/package.json index 3375a03..7274b92 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "node": ">=22" }, "scripts": { - "build": "esbuild src/main.js --bundle --outfile=ocf-bundle.js --format=iife --global-name=OCFEditor --sourcemap", - "watch": "esbuild src/main.js --bundle --outfile=ocf-bundle.js --format=iife --global-name=OCFEditor --sourcemap --watch", + "build": "node scripts/build.js", + "watch": "node scripts/build.js --watch", "validate-examples": "node scripts/validate-examples.js", "test": "npx playwright test", "test:ui": "npx playwright test --ui" diff --git a/scripts/build-pages.sh b/scripts/build-pages.sh index 4271cc4..ec70065 100755 --- a/scripts/build-pages.sh +++ b/scripts/build-pages.sh @@ -15,36 +15,35 @@ set -euo pipefail OUT="${1:-_site}" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -ESBUILD="$ROOT/node_modules/.bin/esbuild" # Files that make up a deployable site (everything index.html references). STATIC_FILES=(style.css opencoachingformat-v1.schema.json) -# Build one variant from a git ref into a target directory, stamping the version. -# $1 = git ref (tag or branch), $2 = target dir, $3 = version label for the badge +# Build one variant from a git ref into a target directory. +# $1 = git ref (tag or branch), $2 = target dir, $3 = version label (optional; +# empty → scripts/build.js derives "v" from that ref's package.json). build_variant() { - local ref="$1" dest="$2" version="$3" + local ref="$1" dest="$2" label="$3" local src src="$(mktemp -d)" - echo "→ building '$ref' as version '$version' into $dest" + echo "→ building '$ref' (version: ${label:-from package.json}) into $dest" git archive "$ref" | tar -x -C "$src" - mkdir -p "$dest" - "$ESBUILD" "$src/src/main.js" --bundle \ - --outfile="$dest/ocf-bundle.js" --format=iife \ - --global-name=OCFEditor --sourcemap + # Build with this checkout's scripts/build.js (it has esbuild via node_modules) + # against the extracted ref as the working directory, so the version is taken + # from the ref's own package.json — or from APP_VERSION when we pass a label. + # Older refs whose main.js predates build-time injection simply keep their + # static badge, which for a release tag already equals the tag version. + ( cd "$src" && APP_VERSION="$label" node "$ROOT/scripts/build.js" ) + mkdir -p "$dest" + cp "$src/ocf-bundle.js" "$src/ocf-bundle.js.map" "$dest"/ cp "$src/index.html" "$dest/index.html" for f in "${STATIC_FILES[@]}"; do [ -e "$src/$f" ] && cp "$src/$f" "$dest/" done [ -d "$src/examples" ] && cp -r "$src/examples" "$dest/examples" - # Stamp the in-app version badge so the deployed build self-reports its version. - if [ -n "$version" ]; then - sed -i "s#[^<]*#${version}#" "$dest/index.html" - fi - rm -rf "$src" } @@ -65,7 +64,8 @@ fi LATEST_TAG="$(git tag -l 'v*' --sort=-v:refname | head -n1 || true)" if [ -n "$LATEST_TAG" ]; then - build_variant "$LATEST_TAG" "$OUT" "$LATEST_TAG" + # Empty label → build.js stamps "v" from the tag's package.json. + build_variant "$LATEST_TAG" "$OUT" "" else # No release yet → root shows the preview build so the site is never empty. echo "→ no v* tag found; root will mirror the preview build" diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 0000000..7668cce --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,50 @@ +#!/usr/bin/env node +// +// Build the OCF Editor bundle with esbuild. +// +// The app version is injected at build time from a single source of truth — +// package.json — so it never has to be kept in sync by hand. npm exposes the +// version as $npm_package_version when this runs via `npm run build`; when the +// script is invoked directly (e.g. by build-pages.sh against a checked-out +// release) it falls back to the package.json in the current working directory. +// +// The display string can be overridden with the APP_VERSION env var (used by +// build-pages.sh to label the /preview build), otherwise it is `v`. +// +// Usage: node scripts/build.js [--watch] +'use strict'; + +const path = require('path'); +const esbuild = require('esbuild'); + +// Resolve project files relative to the working directory, so this script can +// build either the repo root (npm run build) or an extracted release tree. +const cwd = process.cwd(); +const pkg = require(path.join(cwd, 'package.json')); + +const version = + process.env.APP_VERSION || `v${process.env.npm_package_version || pkg.version}`; + +const options = { + entryPoints: [path.join(cwd, 'src/main.js')], + bundle: true, + outfile: path.join(cwd, 'ocf-bundle.js'), + format: 'iife', + globalName: 'OCFEditor', + sourcemap: true, + define: { __APP_VERSION__: JSON.stringify(version) }, +}; + +(async () => { + if (process.argv.includes('--watch')) { + const ctx = await esbuild.context(options); + await ctx.watch(); + console.log(`esbuild: watching for changes (version ${version})…`); + } else { + await esbuild.build(options); + console.log(`esbuild: built ocf-bundle.js (version ${version})`); + } +})().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/main.js b/src/main.js index 8e8ea08..3e15e60 100644 --- a/src/main.js +++ b/src/main.js @@ -58,6 +58,11 @@ export function init() { playerControlsEl = document.getElementById('player-controls'); modeToggle = document.getElementById('mode-toggle'); + // Show the app version, injected at build time from package.json (see + // scripts/build.js). Single source of truth — no hand-maintained copies. + const versionEl = document.querySelector('.app-version'); + if (versionEl) versionEl.textContent = __APP_VERSION__; + // Init UI components new Palette(paletteEl, editorState); new Toolbar(toolbarEl, editorState); diff --git a/tests/editor.spec.js b/tests/editor.spec.js index 0af141c..67e467e 100644 --- a/tests/editor.spec.js +++ b/tests/editor.spec.js @@ -4,6 +4,11 @@ */ import { test, expect } from '@playwright/test'; +import { readFileSync } from 'node:fs'; + +// Single source of truth: the version the app should display comes from +// package.json (injected into the bundle at build time by scripts/build.js). +const { version } = JSON.parse(readFileSync('package.json', 'utf8')); // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -31,7 +36,7 @@ test.describe('App loads', () => { test('page title and version are correct', async ({ page }) => { await openEditor(page); await expect(page).toHaveTitle(/OCF Editor/); - await expect(page.locator('.app-version')).toHaveText('v0.2.1'); + await expect(page.locator('.app-version')).toHaveText(`v${version}`); }); test('court SVG is rendered (no 404 on bundle)', async ({ page }) => {