From d405ee5cfa54b00a513a70b7fdbbae3ed18384cd Mon Sep 17 00:00:00 2001 From: implementer Date: Fri, 28 Aug 2026 09:15:09 +0000 Subject: [PATCH 1/3] feat: restrict Node engine to 24.x (E00-S01-T08) --- package.json | 3 +++ pnpm-workspace.yaml | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/package.json b/package.json index 65f155f..8b9a001 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "private": true, "type": "module", "description": "EPPP — a modular personal blogging platform. Workspace bootstrap (E00-S01).", + "engines": { + "node": ">=24.0.0 <25.0.0" + }, "scripts": { "build": "pnpm -r run build", "test": "node --test \"tests/**/*.test.mjs\"", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9cf8977..8db344c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,8 @@ packages: - 'apps/*' - 'packages/*' - 'extensions/*' + +# E00-S01-T08 — reject installs on unsupported Node versions instead of +# warning: root package.json restricts engines.node to 24.x, and engineStrict +# turns a mismatch into a hard install error (ERR_PNPM_UNSUPPORTED_ENGINE). +engineStrict: true -- 2.54.0 From 7c548ac43bfe76abbd05462d61d52e87c173dd0f Mon Sep 17 00:00:00 2001 From: implementer Date: Fri, 28 Aug 2026 09:15:09 +0000 Subject: [PATCH 2/3] test: lock in Node 24.x engine restriction (E00-S01-T08) --- tests/node-engine.test.mjs | 133 +++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 tests/node-engine.test.mjs diff --git a/tests/node-engine.test.mjs b/tests/node-engine.test.mjs new file mode 100644 index 0000000..211b53d --- /dev/null +++ b/tests/node-engine.test.mjs @@ -0,0 +1,133 @@ +/** + * Node engine test — locks in the [E00-S01-T08] Node engine restriction to + * 24.x for the workspace. + * + * Acceptance criteria covered (each test fails without the committed config): + * - "Node engine is restricted to 24.x in the package manifest" → root + * `package.json` declares `engines.node` exactly `>=24.0.0 <25.0.0`, and a + * minimal semver range check proves the range admits 24.x versions + * (24.0.0 … 24.99.99) while excluding every other major line. + * - "an unsupported Node version is rejected" → the same range check proves + * 18.x/22.x/23.x/25.x/26.x are outside the range, the committed + * `pnpm-workspace.yaml` sets `engineStrict: true` so pnpm hard-fails the + * install on those versions (ERR_PNPM_UNSUPPORTED_ENGINE) instead of + * warning, and the current runtime is asserted to satisfy the range (so + * `pnpm test` on an unsupported Node fails loudly). + * + * Run: `node --test tests/node-engine.test.mjs` + * (node:test — built into Node >= 18; no dependencies, lockfile untouched.) + */ + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8'); + +/** The exact engines.node value committed in the root package.json (24.x). */ +const NODE_ENGINE_RANGE = '>=24.0.0 <25.0.0'; + +/** Versions inside the 24.x line that must satisfy the range. */ +const SUPPORTED_EXAMPLES = ['24.0.0', '24.1.0', '24.20.0', '24.99.99']; + +/** Versions outside the 24.x line that must be rejected by the range. */ +const UNSUPPORTED_EXAMPLES = ['18.0.0', '20.0.0', '22.0.0', '22.23.2', '23.0.0', '25.0.0', '26.0.0']; + +// --------------------------------------------------------------------------- +// Minimal semver range checker (no dependencies, lockfile untouched) +// --------------------------------------------------------------------------- + +/** Parses "v?MAJOR.MINOR.PATCH" (pre-release/build suffixes ignored). */ +function parseVersion(version) { + const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(String(version).trim()); + assert.ok(match, `"${version}" is not a MAJOR.MINOR.PATCH semver version`); + return [Number(match[1]), Number(match[2]), Number(match[3])]; +} + +/** Compares two [major, minor, patch] tuples: -1, 0 or 1. */ +function compareVersions(a, b) { + for (let i = 0; i < 3; i += 1) { + if (a[i] < b[i]) return -1; + if (a[i] > b[i]) return 1; + } + return 0; +} + +/** + * Checks a version against a whitespace-separated comparator range such as + * ">=24.0.0 <25.0.0" (AND semantics). Throws on any comparator form this + * workspace does not use, so an intentional range change must update this + * checker in the same change. + */ +function satisfies(version, range) { + const comparators = range.trim().split(/\s+/); + assert.ok(comparators.length > 0, `engines range must not be empty: "${range}"`); + const v = parseVersion(version); + return comparators.every((token) => { + const match = /^(>=|<=|>|<|=)?v?(\d+)\.(\d+)\.(\d+)$/.exec(token); + assert.ok(match, `unsupported comparator in engines range "${range}": "${token}"`); + const [, operator = '=', major, minor, patch] = match; + const c = compareVersions(v, [Number(major), Number(minor), Number(patch)]); + switch (operator) { + case '>=': return c >= 0; + case '<=': return c <= 0; + case '>': return c > 0; + case '<': return c < 0; + default: return c === 0; // '=' (exact) + } + }); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +test('root package.json declares engines.node restricted to 24.x', () => { + const manifest = JSON.parse(read('package.json')); + assert.equal( + manifest.engines?.node, + NODE_ENGINE_RANGE, + 'root package.json must declare engines.node exactly ">=24.0.0 <25.0.0" (24.x)', + ); + for (const version of SUPPORTED_EXAMPLES) { + assert.ok(satisfies(version, NODE_ENGINE_RANGE), `${version} must satisfy ${NODE_ENGINE_RANGE}`); + } +}); + +test('the declared engines range rejects unsupported Node versions', () => { + for (const version of UNSUPPORTED_EXAMPLES) { + assert.equal( + satisfies(version, NODE_ENGINE_RANGE), + false, + `${version} must be outside the 24.x range (${NODE_ENGINE_RANGE})`, + ); + } +}); + +test('the current Node runtime satisfies the declared engines range', () => { + assert.ok( + satisfies(process.versions.node, NODE_ENGINE_RANGE), + `runtime Node ${process.versions.node} is not within the supported 24.x line — ` + + 'install a Node 24.x release (install is rejected via engine-strict otherwise)', + ); +}); + +test('the committed pnpm-workspace.yaml enforces engines so an unsupported Node is rejected', () => { + const workspace = read('pnpm-workspace.yaml'); + assert.ok( + workspace.includes('engineStrict: true'), + 'pnpm-workspace.yaml must set engineStrict: true so pnpm rejects an unsupported Node version at install', + ); +}); + +test('committed CI workflow installs on Node 24', () => { + const workflow = read('.gitea/workflows/ci.yml'); + assert.ok( + workflow.includes("node-version: '24'"), + 'CI must run on Node 24 (the supported engines line)', + ); +}); -- 2.54.0 From 6323e486bc33ca7488dde0ddd9b58a98f63abd47 Mon Sep 17 00:00:00 2001 From: implementer Date: Fri, 28 Aug 2026 09:15:09 +0000 Subject: [PATCH 3/3] docs: document enforced Node 24.x engine restriction (E00-S01-T08) --- docs/development/non-container.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/development/non-container.md b/docs/development/non-container.md index b531cc0..ef9ca38 100644 --- a/docs/development/non-container.md +++ b/docs/development/non-container.md @@ -27,9 +27,11 @@ accidental dependency graph: no `packages/*` package may import a concrete ## Prerequisites - **Git** — to clone the repository. -- **Node.js 24.x** — the CI pipeline runs Node 24 and this is the supported - line for the workspace (a hard `engines` restriction is tracked separately as - E00-S01-T08). Any Node ≥ 22 is enough for the current bootstrap commands. +- **Node.js 24.x** — required. The root `package.json` restricts the Node + engine to 24.x (`engines.node`), and the committed `pnpm-workspace.yaml` sets + `engineStrict: true`, so `pnpm install` on any other Node version is + **rejected** (`ERR_PNPM_UNSUPPORTED_ENGINE`) instead of merely warned. + Install Node 24.x via `nvm`, `fnm` or another version manager to match CI. - **pnpm 11.23.0** — pinned via the `packageManager` field in the root `package.json`. The easiest way to get exactly this version is Corepack, which ships with Node.js (`corepack enable`). @@ -128,7 +130,7 @@ pnpm --filter @personal-blog/server start # loads compiled server entrypoint, | --- | --- | | `pnpm: command not found` | Corepack shims not activated — run `corepack enable`, or prefix commands with `corepack pnpm ...`. | | `ERR_PNPM_OUTDATED_LOCKFILE` | `pnpm-lock.yaml` is out of date with the manifests. Run `pnpm install` (unfrozen) and commit the lockfile update. | -| Node version warnings / unexpected behavior | Use Node 24.x to match CI (e.g. via `nvm`, `fnm` or another version manager). A hard `engines` restriction is tracked as E00-S01-T08. | +| `ERR_PNPM_UNSUPPORTED_ENGINE` on install | Your Node version is outside the supported 24.x engine line (`engines.node` in the root `package.json`, enforced by `engineStrict: true` in `pnpm-workspace.yaml`). Install Node 24.x (e.g. via `nvm`, `fnm` or another version manager). | | `start` exits immediately with no output | Expected at bootstrap — the server entrypoint is a placeholder module; the Fastify 5 shell is a later story (see [Run](#run)). | | `.env` files | `.env`/`.env.*` are git-ignored; a committed `.env.example` template lands with the environment story (E00-S04). | -- 2.54.0