[E00-S01-T08] Node engine restricted to 24.x #375
@@ -27,9 +27,11 @@ accidental dependency graph: no `packages/*` package may import a concrete
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **Git** — to clone the repository.
|
- **Git** — to clone the repository.
|
||||||
- **Node.js 24.x** — the CI pipeline runs Node 24 and this is the supported
|
- **Node.js 24.x** — required. The root `package.json` restricts the Node
|
||||||
line for the workspace (a hard `engines` restriction is tracked separately as
|
engine to 24.x (`engines.node`), and the committed `pnpm-workspace.yaml` sets
|
||||||
E00-S01-T08). Any Node ≥ 22 is enough for the current bootstrap commands.
|
`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
|
- **pnpm 11.23.0** — pinned via the `packageManager` field in the root
|
||||||
`package.json`. The easiest way to get exactly this version is Corepack,
|
`package.json`. The easiest way to get exactly this version is Corepack,
|
||||||
which ships with Node.js (`corepack enable`).
|
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 ...`. |
|
| `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. |
|
| `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)). |
|
| `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). |
|
| `.env` files | `.env`/`.env.*` are git-ignored; a committed `.env.example` template lands with the environment story (E00-S04). |
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "EPPP — a modular personal blogging platform. Workspace bootstrap (E00-S01).",
|
"description": "EPPP — a modular personal blogging platform. Workspace bootstrap (E00-S01).",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.0.0 <25.0.0"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm -r run build",
|
"build": "pnpm -r run build",
|
||||||
"test": "node --test \"tests/**/*.test.mjs\"",
|
"test": "node --test \"tests/**/*.test.mjs\"",
|
||||||
|
|||||||
@@ -2,3 +2,8 @@ packages:
|
|||||||
- 'apps/*'
|
- 'apps/*'
|
||||||
- 'packages/*'
|
- 'packages/*'
|
||||||
- 'extensions/*'
|
- '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
|
||||||
|
|||||||
@@ -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)',
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user