diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 66f2a1a..923cdea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 24 + node-version-file: .nvmrc cache: npm - run: npm install diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/CLAUDE.md b/CLAUDE.md index 6ce4bc5..5e788b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -249,8 +249,17 @@ in-range updates, evaluates each major separately, sweeps OSV.dev for advisories its "Held back" section records what is already known to be blocked and the exact condition that clears it. Do not re-derive that analysis. -- **Node**: `engines.node` is `^22.22.2 || ^24.15.0 || >=26.0.0`, mirroring the - strictest dev dependency (jsdom 30). Node 20 is EOL and unsupported. +- **Node**: pinned to the **Node 24 LTS line** — `engines.node` is `^24.15.0` + (the `24.15` floor is jsdom 30's, the strictest dev dependency). `@types/node` + is pinned to the matching major (`^24.13.4`); do not let it drift ahead of the + runtime. `.nvmrc` holds `24` and CI reads it via `node-version-file`. + **Vercel** deploys the latest `24.x` for this range (it only offers majors: + 24.x/22.x/20.x), so `engines.node` overrides whatever the project's + Build & Deployment setting says. Node 20 and 22 are no longer supported here. + **Hold this pin until Vercel's default Node version moves forward** — re-check + with , + then bump `engines.node`, `.nvmrc`, `@types/node`, and `REQUIRED_NODE_MAJOR` + in `scripts/setup.ts` together. - **CI gates tests only** — `.github/workflows/test.yml` runs `npm run test:coverage` and never `npm run build`, so type errors do not fail CI. Type-check locally. - **Coverage path is load-bearing**: CI uploads `coverage/coverage-final.json` to @@ -269,6 +278,7 @@ condition that clears it. Do not re-derive that analysis. | `typescript` 7 | No stable Compiler API until 7.1; `typescript-eslint` peers `typescript: >=4.8.4 <6.1.0` | `npm view typescript-eslint peerDependencies` | | `eslint` 10 | `eslint-plugin-react@7.37.5` (latest) peers `eslint ^9.7` and calls a removed context method | `npm view eslint-plugin-react peerDependencies` | | `grapesjs` 0.23 | `@grapesjs/react@2.0.0` (latest) peers `grapesjs ^0.22.5` | `npm view @grapesjs/react peerDependencies` | +| `@types/node` 25+ | Runtime is pinned to Node 24 (`engines.node: ^24.15.0`) because 24.x is Vercel's current default/newest offering; types must not lead the runtime | Vercel's [supported Node versions](https://vercel.com/docs/functions/runtimes/node-js/node-js-versions) | ## Reference Documents diff --git a/README.md b/README.md index 04f079f..fa27a1a 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Component -> Server Action -> Service (singleton) -> MPHelper -> Ministry Platfo ## Prerequisites -- **Node.js**: v20 or higher (enforced via `engines` in `package.json` and the setup script). Required by Next.js 16, React 19, and TypeScript 6.0. +- **Node.js**: **v24.15.0 or later on the 24.x line** (pinned via `engines.node` in `package.json`, `.nvmrc`, and the setup script). Node 24 is the current LTS and the version Vercel deploys; 20.x and 22.x are not supported. Use `nvm use` (or `fnm use`) to pick it up from `.nvmrc`. - **Package Manager**: npm - **Ministry Platform**: Active instance with API credentials and OAuth client configured - **Ministry Platform Database**: SQL install script applied (see [Database Setup](#database-setup) below) diff --git a/package-lock.json b/package-lock.json index ba18e1c..5b8aee2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", "@types/mjml": "^5.0.0", - "@types/node": "^26.5.1", + "@types/node": "^24.13.4", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", "@vitejs/plugin-react": "^6.1.1", @@ -72,7 +72,7 @@ "vitest": "^5.0.0" }, "engines": { - "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + "node": "^24.15.0" } }, "node_modules/@adobe/css-tools": { @@ -4746,13 +4746,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.5.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.1.tgz", - "integrity": "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==", + "version": "24.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz", + "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==", "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~8.9.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/react": { @@ -13825,9 +13825,9 @@ } }, "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "devOptional": true, "license": "MIT" }, diff --git a/package.json b/package.json index 83b6ca1..2966cb7 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "engines": { - "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + "node": "^24.15.0" }, "scripts": { "dev": "next dev", @@ -69,7 +69,7 @@ "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", "@types/mjml": "^5.0.0", - "@types/node": "^26.5.1", + "@types/node": "^24.13.4", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", "@vitejs/plugin-react": "^6.1.1", diff --git a/scripts/setup.ts b/scripts/setup.ts index 0e76ae9..d6aed33 100644 --- a/scripts/setup.ts +++ b/scripts/setup.ts @@ -74,7 +74,11 @@ const MODELS_PATH = path.join( ); const NEXT_BUILD_PATH = path.join(PROJECT_ROOT, '.next'); -const REQUIRED_NODE_VERSION = 20; +// Pinned to the Node 24 LTS line: `engines.node` is `^24.15.0` and Vercel only +// offers major versions (24.x is its current default). Bump both together when +// Vercel moves its default forward. +const REQUIRED_NODE_MAJOR = 24; +const REQUIRED_NODE_MINOR = 15; const SQL_INSTALL_PATH = path.join(PROJECT_ROOT, '_INSTALL', 'ministryplatform-install.sql'); @@ -341,9 +345,11 @@ async function execCommandStreaming( }); } -function getNodeVersion(): number | null { - const match = process.version.match(/^v(\d+)/); - return match ? parseInt(match[1], 10) : null; +function getNodeVersion(): { major: number; minor: number } | null { + const match = process.version.match(/^v(\d+)\.(\d+)/); + return match + ? { major: parseInt(match[1], 10), minor: parseInt(match[2], 10) } + : null; } function countFilesInDir(dir: string): number { @@ -555,17 +561,27 @@ function checkNodeVersion(): StepResult { }; } - if (version < REQUIRED_NODE_VERSION) { + const required = `v${REQUIRED_NODE_MAJOR}.${REQUIRED_NODE_MINOR}.0`; + + if (version.major !== REQUIRED_NODE_MAJOR) { + return { + success: false, + message: `Node.js ${process.version} is not on the pinned v${REQUIRED_NODE_MAJOR} line`, + details: `This project pins Node.js to ${REQUIRED_NODE_MAJOR}.x (see \`engines.node\` and \`.nvmrc\`). Install ${required} or later within v${REQUIRED_NODE_MAJOR}.`, + }; + } + + if (version.minor < REQUIRED_NODE_MINOR) { return { success: false, - message: `Node.js v${version} is below minimum required v${REQUIRED_NODE_VERSION}`, - details: 'Please upgrade Node.js to v18 or later', + message: `Node.js ${process.version} is below minimum required ${required}`, + details: `Please upgrade to ${required} or later within v${REQUIRED_NODE_MAJOR}.`, }; } return { success: true, - message: `Node.js ${process.version} (meets v${REQUIRED_NODE_VERSION}+ requirement)`, + message: `Node.js ${process.version} (meets the pinned ${required}+ requirement)`, }; }