From 37dec4496db637184ba504897e819c8383c40dfb Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Fri, 16 Oct 2026 14:08:36 -0300 Subject: [PATCH 1/3] feat: support the Alpha release phase Node.js 27 introduces Alpha releases versioned as vN.0.0-alpha.X.Y.Z (see nodejs/node#63135). - Keep Alpha versions in the release data instead of letting nodevu coerce them into vN.0.0, and give them the new Alpha status. - Add a /download/alpha page and warn that Alpha releases are not meant for production. - Fall back to the latest LTS on /download/current and /download/archive/current while there is no Current release. - Match pre-releases in the platform and installer checks. Refs: https://github.com/nodejs/Release/issues/1187 Signed-off-by: RafaelGSS --- .changeset/brave-alpha-releases.md | 5 + .../download/archive/[version]/page.tsx | 5 +- .../Downloads/Release/VersionDropdown.tsx | 10 +- apps/site/components/withDownloadSection.tsx | 8 +- apps/site/components/withReleaseAlertBox.tsx | 16 +++ .../__tests__/majorNodeReleases.test.mjs | 100 ++++++++++++++++++ .../generators/__tests__/releaseData.test.mjs | 68 ++++++++++++ .../generators/majorNodeReleases.mjs | 57 +++++++++- .../site/next-data/generators/releaseData.mjs | 6 ++ apps/site/next.constants.mjs | 1 + apps/site/pages/en/download/alpha.mdx | 50 +++++++++ .../scripts/release-post/downloadsTable.mjs | 11 +- apps/site/types/releases.ts | 2 +- apps/site/util/__tests__/download.test.mjs | 22 ++++ apps/site/util/download/archive.tsx | 4 +- apps/site/util/download/constants.json | 2 +- apps/site/util/download/index.tsx | 4 +- packages/i18n/src/locales/en.json | 1 + 18 files changed, 359 insertions(+), 13 deletions(-) create mode 100644 .changeset/brave-alpha-releases.md create mode 100644 apps/site/next-data/generators/__tests__/majorNodeReleases.test.mjs create mode 100644 apps/site/pages/en/download/alpha.mdx diff --git a/.changeset/brave-alpha-releases.md b/.changeset/brave-alpha-releases.md new file mode 100644 index 0000000000000..85169618dc240 --- /dev/null +++ b/.changeset/brave-alpha-releases.md @@ -0,0 +1,5 @@ +--- +'@node-core/website-i18n': patch +--- + +Add the Alpha release warning. diff --git a/apps/site/app/[locale]/download/archive/[version]/page.tsx b/apps/site/app/[locale]/download/archive/[version]/page.tsx index cf3a343578e2b..6b16badea89ef 100644 --- a/apps/site/app/[locale]/download/archive/[version]/page.tsx +++ b/apps/site/app/[locale]/download/archive/[version]/page.tsx @@ -51,7 +51,10 @@ const getPage: FC = async props => { if (version === 'current') { const releaseData = await provideReleaseData(); - const release = releaseData.find(release => release.status === 'Current'); + // There's no Current release while the next major is in its Alpha phase + const release = + releaseData.find(release => release.status === 'Current') ?? + releaseData.find(release => release.status === 'LTS'); redirect(`/${locale}/download/archive/${release?.versionWithPrefix}`); } diff --git a/apps/site/components/Downloads/Release/VersionDropdown.tsx b/apps/site/components/Downloads/Release/VersionDropdown.tsx index c13c7a7488f2f..d70ca70872835 100644 --- a/apps/site/components/Downloads/Release/VersionDropdown.tsx +++ b/apps/site/components/Downloads/Release/VersionDropdown.tsx @@ -27,7 +27,10 @@ const VersionDropdown: FC = () => { ({ versionWithPrefix }) => versionWithPrefix === version ); - if (release?.status === 'LTS' && pathname.includes('current')) { + if ( + release?.status === 'LTS' && + (pathname.includes('current') || pathname.includes('alpha')) + ) { redirect({ href: '/download', locale }); return; } @@ -37,6 +40,11 @@ const VersionDropdown: FC = () => { return; } + if (release?.status === 'Alpha' && !pathname.includes('alpha')) { + redirect({ href: '/download/alpha', locale }); + return; + } + setVersion(version); }; diff --git a/apps/site/components/withDownloadSection.tsx b/apps/site/components/withDownloadSection.tsx index 373eebaafb290..90d335b1fe4f4 100644 --- a/apps/site/components/withDownloadSection.tsx +++ b/apps/site/components/withDownloadSection.tsx @@ -9,7 +9,7 @@ import { ReleasesProvider, } from '#site/providers/releaseProvider'; -import type { NodeRelease } from '../types'; +import type { NodeRelease, NodeReleaseStatus } from '../types'; import type { FC, PropsWithChildren } from 'react'; type WithDownloadSectionProps = PropsWithChildren<{ @@ -39,7 +39,11 @@ const WithDownloadSection: FC = async ({ .concat(localeSnippets); // Decides which initial release to use based on the current pathname - const initialRelease = pathname.endsWith('/current') ? 'Current' : 'LTS'; + const initialRelease: Array = pathname.endsWith('/alpha') + ? ['Alpha', 'Current', 'LTS'] + : pathname.endsWith('/current') + ? ['Current', 'LTS'] + : ['LTS']; return ( diff --git a/apps/site/components/withReleaseAlertBox.tsx b/apps/site/components/withReleaseAlertBox.tsx index fe57bfe24e44a..c2e896b468612 100644 --- a/apps/site/components/withReleaseAlertBox.tsx +++ b/apps/site/components/withReleaseAlertBox.tsx @@ -26,6 +26,22 @@ const WithReleaseAlertBox: FC = ({ status }) => { })} ); + case 'Alpha': + return ( + + {t.rich('layouts.download.codeBox.alphaVersionWarning', { + link: text => ( + + {text} + + ), + })} + + ); case 'LTS': return ( ({ + version, + date, + files: [], + npm: '11.0.0', + v8: '14.0.0.0', + modules: '150', + lts, + security: false, +}); + +const SCHEDULE = { + v26: { + start: '2026-05-05', + lts: '2026-10-28', + maintenance: '2027-10-20', + end: '2029-04-30', + codename: '', + }, + v27: { + alpha: '2026-10-28', + start: '2027-04-22', + lts: '2027-10-20', + end: '2030-04-30', + codename: '', + }, +}; + +const mockFetch = (index, schedule = SCHEDULE) => { + globalThis.fetch = async url => ({ + json: async () => (url.includes('schedule') ? schedule : index), + }); +}; + +describe('getMajorNodeReleases', () => { + it('keeps the pre-release identifiers of Alpha releases', async () => { + mockFetch([ + createRelease('v27.0.0-alpha.1.0.0', '2026-11-25'), + createRelease('v27.0.0-alpha.0.0.0', '2026-10-28'), + createRelease('v26.1.0', '2026-10-28', 'Codename'), + ]); + + const majors = await getMajorNodeReleases(); + + assert.deepEqual( + majors.map(([version]) => version), + ['v27', 'v26'] + ); + + const [, v27] = majors[0]; + + assert.deepEqual(Object.keys(v27.releases), [ + 'v27.0.0-alpha.1.0.0', + 'v27.0.0-alpha.0.0.0', + ]); + assert.equal( + v27.releases['v27.0.0-alpha.1.0.0'].semver.raw, + '27.0.0-alpha.1.0.0' + ); + assert.equal(v27.releases['v27.0.0-alpha.1.0.0'].semver.major, 27); + assert.equal(v27.releases['v27.0.0-alpha.1.0.0'].lts.isLts, false); + assert.equal(v27.support.phases.dates.end, '2030-04-30'); + }); + + it('lists Alpha releases after the stable releases of the same line', async () => { + mockFetch([ + createRelease('v27.0.0', '2027-04-22'), + createRelease('v27.0.0-alpha.1.0.0', '2026-11-25'), + createRelease('v27.0.0-alpha.0.0.0', '2026-10-28'), + ]); + + const [[version, v27]] = await getMajorNodeReleases(); + + assert.equal(version, 'v27'); + assert.deepEqual(Object.keys(v27.releases), [ + 'v27.0.0', + 'v27.0.0-alpha.1.0.0', + 'v27.0.0-alpha.0.0.0', + ]); + }); + + it('ignores Alpha releases of release lines without documented support', async () => { + mockFetch([ + createRelease('v28.0.0-alpha.0.0.0', '2027-10-27'), + createRelease('v26.1.0', '2026-10-28', 'Codename'), + ]); + + const majors = await getMajorNodeReleases(); + + assert.deepEqual( + majors.map(([version]) => version), + ['v26'] + ); + }); +}); diff --git a/apps/site/next-data/generators/__tests__/releaseData.test.mjs b/apps/site/next-data/generators/__tests__/releaseData.test.mjs index 52133bdb6865f..73654cbb725ac 100644 --- a/apps/site/next-data/generators/__tests__/releaseData.test.mjs +++ b/apps/site/next-data/generators/__tests__/releaseData.test.mjs @@ -130,6 +130,74 @@ describe('generateReleaseData', () => { assert.equal(result[0]?.status, 'Current'); }); + it('returns Alpha when release is not EOL and latest is a pre-release', async t => { + const result = await runWithNodevuData(t, '2026-11-01', { + 27: { + releases: { + 'v27.0.0-alpha.0.0.0': { + semver: { major: 27, raw: '27.0.0-alpha.0.0.0' }, + dependencies: { npm: '11.6.0', v8: '14.6.0.0' }, + lts: { isLts: false }, + releaseDate: '2026-10-28', + modules: { version: '150' }, + }, + }, + support: { + phases: { + dates: { + alpha: '2026-10-28', + start: '2027-04-22', + maintenance: '2027-10-20', + end: '2030-04-30', + }, + }, + }, + }, + }); + + assert.equal(result[0]?.status, 'Alpha'); + assert.equal(result[0]?.versionWithPrefix, 'v27.0.0-alpha.0.0.0'); + }); + + it('returns Current when latest is a stable release following pre-releases', async t => { + const result = await runWithNodevuData(t, '2027-04-22', { + 27: { + releases: { + 'v27.0.0': { + semver: { major: 27, raw: '27.0.0' }, + dependencies: { npm: '11.6.0', v8: '14.6.0.0' }, + lts: { isLts: false }, + releaseDate: '2027-04-22', + modules: { version: '150' }, + }, + 'v27.0.0-alpha.0.0.0': { + semver: { major: 27, raw: '27.0.0-alpha.0.0.0' }, + dependencies: { npm: '11.6.0', v8: '14.6.0.0' }, + lts: { isLts: false }, + releaseDate: '2026-10-28', + modules: { version: '150' }, + }, + }, + support: { + phases: { + dates: { + alpha: '2026-10-28', + start: '2027-04-22', + maintenance: '2027-10-20', + end: '2030-04-30', + }, + }, + }, + }, + }); + + assert.equal(result[0]?.status, 'Current'); + assert.equal( + result[0]?.minorVersions[1]?.versionWithPrefix, + 'v27.0.0-alpha.0.0.0' + ); + }); + it('uses latest and earliest release dates for latestReleaseDate and initialReleaseDate', async t => { const result = await runWithNodevuData(t, '2026-04-14', { 26: { diff --git a/apps/site/next-data/generators/majorNodeReleases.mjs b/apps/site/next-data/generators/majorNodeReleases.mjs index 28cb0dc07e1ed..d280db0fd536c 100644 --- a/apps/site/next-data/generators/majorNodeReleases.mjs +++ b/apps/site/next-data/generators/majorNodeReleases.mjs @@ -1,14 +1,69 @@ 'use strict'; import nodevu from '@nodevu/core'; +import semVer from 'semver'; import { fetchWithRetry } from '#site/next.fetch.mjs'; +const toNodevuRelease = ({ version, date, npm, v8, modules, lts }) => { + const { version: raw, major, minor, patch } = semVer.parse(version); + + return { + semver: { raw, major, minor, patch, line: `v${major}` }, + releaseDate: date, + modules: { version: modules }, + dependencies: { npm, v8 }, + lts: { isLts: !!lts }, + }; +}; + /** * Filters Node.js release data to return only major releases with documented support. */ export default async function getMajorNodeReleases() { - const nodevuData = await nodevu({ fetch: fetchWithRetry }); + const preReleases = []; + let schedule = {}; + + // nodevu coerces versions (v27.0.0-alpha.0.0.0 becomes v27.0.0), hence + // pre-releases are removed before nodevu parses index.json and added back + const fetchWithoutPreReleases = async (url, options) => { + const data = await fetchWithRetry(url, options).then(r => r.json()); + + if (!Array.isArray(data)) { + schedule = data; + + return Response.json(data); + } + + preReleases.push( + ...data.filter(({ version }) => semVer.prerelease(version)) + ); + + return Response.json(data.filter(r => !preReleases.includes(r))); + }; + + const releaseLines = await nodevu({ fetch: fetchWithoutPreReleases }); + + const preReleaseLines = {}; + + preReleases.forEach(release => { + const line = `v${semVer.major(release.version)}`; + + if (!releaseLines[line] && !preReleaseLines[line]) { + const { codename, ...dates } = schedule[line] ?? {}; + + preReleaseLines[line] = { + releases: {}, + support: schedule[line] && { codename, phases: { dates } }, + }; + } + + const { releases } = releaseLines[line] ?? preReleaseLines[line]; + + releases[release.version] = toNodevuRelease(release); + }); + + const nodevuData = { ...preReleaseLines, ...releaseLines }; return Object.entries(nodevuData).filter(([version, { support }]) => { // Filter out those without documented support diff --git a/apps/site/next-data/generators/releaseData.mjs b/apps/site/next-data/generators/releaseData.mjs index 921d5cff302ba..890acb5b3fa3c 100644 --- a/apps/site/next-data/generators/releaseData.mjs +++ b/apps/site/next-data/generators/releaseData.mjs @@ -1,5 +1,7 @@ 'use strict'; +import semVer from 'semver'; + import getMajorNodeReleases from './majorNodeReleases.mjs'; // Gets the appropriate release status for each major release @@ -14,6 +16,10 @@ const getNodeReleaseStatus = (latest, eol) => { return 'LTS'; } + if (semVer.prerelease(latest.semver.raw)) { + return 'Alpha'; + } + return 'Current'; }; diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs index abc3e4da2a87d..b9509bb4c19f3 100644 --- a/apps/site/next.constants.mjs +++ b/apps/site/next.constants.mjs @@ -145,6 +145,7 @@ export const STATUS_KIND_MAP = { EOL: 'warning', LTS: 'info', Current: 'default', + Alpha: 'default', }; /** diff --git a/apps/site/pages/en/download/alpha.mdx b/apps/site/pages/en/download/alpha.mdx new file mode 100644 index 0000000000000..cc435c8af1e41 --- /dev/null +++ b/apps/site/pages/en/download/alpha.mdx @@ -0,0 +1,50 @@ +--- +layout: download +title: Download Node.js® +--- + +
+ +Get Node.js® for using with + + + + + +Or get a prebuilt Node.js® for running a architecture. + + + + + +
+ +
+ +Read the changelog or blog post for this version. + +Learn more about [Node.js releases](/about/previous-releases), including the release schedule and LTS status. + +Learn how to verify signed SHASUMS. + +Looking for Node.js source? Download a signed Node.js source tarball. + +Check out our nightly binaries or +all previous releases +or the unofficial binaries for other platforms. + +
+ +--- + +
+

Proudly supported by the partners below:

+ + + We are able to serve Node.js's downloads and maintain our infrastructure + proudly due to the support of these partners, and more. + +
+ +
+
diff --git a/apps/site/scripts/release-post/downloadsTable.mjs b/apps/site/scripts/release-post/downloadsTable.mjs index 1a02c4feb5fc7..1e57631e87e62 100644 --- a/apps/site/scripts/release-post/downloadsTable.mjs +++ b/apps/site/scripts/release-post/downloadsTable.mjs @@ -85,13 +85,16 @@ const resolveUrl = (item, version) => { const resolveDownloads = version => { let downloads = downloadOptions; - if (semVer.satisfies(version, '< 16.0.0')) { + const satisfies = range => + semVer.satisfies(version, range, { includePrerelease: true }); + + if (satisfies('< 16.0.0')) { downloads = downloads.filter( ver => ver.title !== 'macOS Apple Silicon 64-bit Binary' ); } - if (semVer.satisfies(version, '< 19.9.0')) { + if (satisfies('< 19.9.0')) { downloads = downloads.filter( ver => ver.title !== 'Windows ARM 64-bit Installer' && @@ -99,7 +102,7 @@ const resolveDownloads = version => { ); } - if (semVer.satisfies(version, '>= 23.0.0')) { + if (satisfies('>= 23.0.0')) { downloads = downloads.filter( ver => ver.title !== 'Windows 32-bit Installer' && @@ -107,7 +110,7 @@ const resolveDownloads = version => { ); } - if (semVer.satisfies(version, '>= 24.0.0')) { + if (satisfies('>= 24.0.0')) { downloads = downloads.filter(ver => ver.title !== 'ARMv7 32-bit Binary'); } diff --git a/apps/site/types/releases.ts b/apps/site/types/releases.ts index 967fcbeaf512a..8d1c53eafee3d 100644 --- a/apps/site/types/releases.ts +++ b/apps/site/types/releases.ts @@ -1,4 +1,4 @@ -export type NodeReleaseStatus = 'LTS' | 'Current' | 'EOL'; +export type NodeReleaseStatus = 'LTS' | 'Current' | 'Alpha' | 'EOL'; export type NodeReleaseSource = { major: number; diff --git a/apps/site/util/__tests__/download.test.mjs b/apps/site/util/__tests__/download.test.mjs index c1aa3ffd1c655..b6f14982d029c 100644 --- a/apps/site/util/__tests__/download.test.mjs +++ b/apps/site/util/__tests__/download.test.mjs @@ -115,6 +115,28 @@ describe('parseCompat', () => { assert.ok(result[0].disabled); }); + it('should match semver constraints for pre-release versions', () => { + const items = [ + { + value: 'testItem', + compatibility: { semver: ['>= 16.9.0'] }, + }, + { + value: 'testItem2', + compatibility: { semver: ['< 23.0.0'] }, + }, + ]; + const result = parseCompat(items, { + os: 'MAC', + installMethod: '', + platform: 'x64', + version: 'v27.0.0-alpha.0.0.0', + release: { status: 'Alpha' }, + }); + assert.equal(result[0].disabled, false); + assert.ok(result[1].disabled); + }); + it('should disable items if release status is not supported', () => { const items = [ { diff --git a/apps/site/util/download/archive.tsx b/apps/site/util/download/archive.tsx index 6c74c30470a91..9acfa5dffaf9f 100644 --- a/apps/site/util/download/archive.tsx +++ b/apps/site/util/download/archive.tsx @@ -65,7 +65,9 @@ const getCompatiblePlatforms = ( } return compatibility.semver.every(version => - semVer.satisfies(versionWithPrefix, version) + semVer.satisfies(versionWithPrefix, version, { + includePrerelease: true, + }) ); }) .map(platform => ({ diff --git a/apps/site/util/download/constants.json b/apps/site/util/download/constants.json index 440637e5b72dc..13a42f929d367 100644 --- a/apps/site/util/download/constants.json +++ b/apps/site/util/download/constants.json @@ -242,5 +242,5 @@ "bitness": ["64", "32"], "architecture": ["arm", "x86"] }, - "statusOrder": ["Current", "LTS", "EOL"] + "statusOrder": ["Alpha", "Current", "LTS", "EOL"] } diff --git a/apps/site/util/download/index.tsx b/apps/site/util/download/index.tsx index 69713f4fb005a..e626890ffaf7b 100644 --- a/apps/site/util/download/index.tsx +++ b/apps/site/util/download/index.tsx @@ -59,7 +59,9 @@ export const parseCompat = < compatibility.installMethod.includes(installMethod), !compatibility.platform || compatibility.platform.includes(platform), !compatibility.semver || - compatibility.semver.every(semver => satisfies(version, semver)), + compatibility.semver.every(semver => + satisfies(version, semver, { includePrerelease: true }) + ), !compatibility.releases || compatibility.releases.includes(release.status), ]; diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index e0e60db3cd1f9..e55ee78c8d3f7 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -248,6 +248,7 @@ }, "codeBox": { "unsupportedVersionWarning": "This version is out of maintenance. Please use a currently supported version. Understand EOL support.", + "alphaVersionWarning": "Alpha releases are meant for early testing and are not intended for production use. Their APIs may change between releases. Learn more about Alpha releases.", "communityPlatformInfo": "Installation methods that involve community software are supported by the teams maintaining that software.", "externalSupportInfo": "If you encounter any issues please visit {platform}'s website", "noScriptDetected": "This page requires JavaScript. You can download Node.js without JavaScript by visiting the downloads archive page directly.", From 99b298aa441d288ddbd1db50c484532a185c9a56 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Wed, 21 Oct 2026 13:37:31 +0200 Subject: [PATCH 2/3] chore: bump @nodevu/core to 1.1.0 1.1.0 supports the Alpha release phase: Alpha releases keep their pre-release identifiers instead of being coerced into `vN.0.0`, and every release has an `alpha.isAlpha` flag, like `lts.isLts`. Refs: https://github.com/cutenode/nodevu/pull/86 Assisted-by: Claude Opus 5.5 --- apps/site/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/site/package.json b/apps/site/package.json index 77e46b68c047b..ac8b9deabe374 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -32,7 +32,7 @@ "@node-core/rehype-shiki": "workspace:*", "@node-core/ui-components": "workspace:*", "@node-core/website-i18n": "workspace:*", - "@nodevu/core": "0.3.0", + "@nodevu/core": "1.1.0", "@orama/orama": "^3.1.18", "@radix-ui/react-tabs": "^1.1.18", "@radix-ui/react-tooltip": "^1.2.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 539a0081be605..05afca6a6b778 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -276,8 +276,8 @@ importers: specifier: workspace:* version: link:../../packages/i18n '@nodevu/core': - specifier: 0.3.0 - version: 0.3.0 + specifier: 1.1.0 + version: 1.1.0 '@orama/orama': specifier: ^3.1.18 version: 3.1.18 @@ -2570,8 +2570,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nodevu/core@0.3.0': - resolution: {integrity: sha512-kVJh6kQViCE8Qf1j7pc7yhUke2kKb1eZM+rqln+EytHqyOq+eZUEtfWbDk1vfWhgiknnAcFOhdVA5apT8rTejQ==} + '@nodevu/core@1.1.0': + resolution: {integrity: sha512-wb0p/vJNgIojZt738Qd3JRaCnAwShOKBIkIkmTbpNFJbjWj7iWVl7u4JZfVOBrSpJRiRaVFg49+3xQtBAHtGwA==} '@nodevu/parsefiles@0.0.3': resolution: {integrity: sha512-IjwkVqA2SlH8XweoAw7EJkeyPGk7gW9imlvhpywrKiPCLP1H0meG/blNAF5X38pirsmvhxPe/BCyywT7Exwuow==} @@ -11776,7 +11776,7 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@nodevu/core@0.3.0': + '@nodevu/core@1.1.0': dependencies: '@nodevu/parsefiles': 0.0.3 luxon: 3.6.1 From d89f8d7b5f9c10c2b0d451852173b9eba73280ca Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Wed, 21 Oct 2026 13:37:49 +0200 Subject: [PATCH 3/3] refactor: drop the nodevu workaround for Alpha releases nodevu now keeps Alpha releases as they are, so the site no longer strips pre-releases before nodevu parses index.json and rebuilds them afterwards. The Alpha status comes from `alpha.isAlpha`, the same way LTS comes from `lts.isLts`. Assisted-by: Claude Opus 5.5 --- .../__tests__/majorNodeReleases.test.mjs | 100 ------------------ .../generators/__tests__/releaseData.test.mjs | 8 ++ .../generators/majorNodeReleases.mjs | 57 +--------- .../site/next-data/generators/releaseData.mjs | 4 +- 4 files changed, 10 insertions(+), 159 deletions(-) delete mode 100644 apps/site/next-data/generators/__tests__/majorNodeReleases.test.mjs diff --git a/apps/site/next-data/generators/__tests__/majorNodeReleases.test.mjs b/apps/site/next-data/generators/__tests__/majorNodeReleases.test.mjs deleted file mode 100644 index dd981a27ec10f..0000000000000 --- a/apps/site/next-data/generators/__tests__/majorNodeReleases.test.mjs +++ /dev/null @@ -1,100 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; - -import getMajorNodeReleases from '#site/next-data/generators/majorNodeReleases.mjs'; - -const createRelease = (version, date, lts = false) => ({ - version, - date, - files: [], - npm: '11.0.0', - v8: '14.0.0.0', - modules: '150', - lts, - security: false, -}); - -const SCHEDULE = { - v26: { - start: '2026-05-05', - lts: '2026-10-28', - maintenance: '2027-10-20', - end: '2029-04-30', - codename: '', - }, - v27: { - alpha: '2026-10-28', - start: '2027-04-22', - lts: '2027-10-20', - end: '2030-04-30', - codename: '', - }, -}; - -const mockFetch = (index, schedule = SCHEDULE) => { - globalThis.fetch = async url => ({ - json: async () => (url.includes('schedule') ? schedule : index), - }); -}; - -describe('getMajorNodeReleases', () => { - it('keeps the pre-release identifiers of Alpha releases', async () => { - mockFetch([ - createRelease('v27.0.0-alpha.1.0.0', '2026-11-25'), - createRelease('v27.0.0-alpha.0.0.0', '2026-10-28'), - createRelease('v26.1.0', '2026-10-28', 'Codename'), - ]); - - const majors = await getMajorNodeReleases(); - - assert.deepEqual( - majors.map(([version]) => version), - ['v27', 'v26'] - ); - - const [, v27] = majors[0]; - - assert.deepEqual(Object.keys(v27.releases), [ - 'v27.0.0-alpha.1.0.0', - 'v27.0.0-alpha.0.0.0', - ]); - assert.equal( - v27.releases['v27.0.0-alpha.1.0.0'].semver.raw, - '27.0.0-alpha.1.0.0' - ); - assert.equal(v27.releases['v27.0.0-alpha.1.0.0'].semver.major, 27); - assert.equal(v27.releases['v27.0.0-alpha.1.0.0'].lts.isLts, false); - assert.equal(v27.support.phases.dates.end, '2030-04-30'); - }); - - it('lists Alpha releases after the stable releases of the same line', async () => { - mockFetch([ - createRelease('v27.0.0', '2027-04-22'), - createRelease('v27.0.0-alpha.1.0.0', '2026-11-25'), - createRelease('v27.0.0-alpha.0.0.0', '2026-10-28'), - ]); - - const [[version, v27]] = await getMajorNodeReleases(); - - assert.equal(version, 'v27'); - assert.deepEqual(Object.keys(v27.releases), [ - 'v27.0.0', - 'v27.0.0-alpha.1.0.0', - 'v27.0.0-alpha.0.0.0', - ]); - }); - - it('ignores Alpha releases of release lines without documented support', async () => { - mockFetch([ - createRelease('v28.0.0-alpha.0.0.0', '2027-10-27'), - createRelease('v26.1.0', '2026-10-28', 'Codename'), - ]); - - const majors = await getMajorNodeReleases(); - - assert.deepEqual( - majors.map(([version]) => version), - ['v26'] - ); - }); -}); diff --git a/apps/site/next-data/generators/__tests__/releaseData.test.mjs b/apps/site/next-data/generators/__tests__/releaseData.test.mjs index 73654cbb725ac..ff89e6eaa4919 100644 --- a/apps/site/next-data/generators/__tests__/releaseData.test.mjs +++ b/apps/site/next-data/generators/__tests__/releaseData.test.mjs @@ -54,6 +54,7 @@ describe('generateReleaseData', () => { semver: { major: 20, raw: '20.12.0' }, dependencies: { npm: '10.8.2', v8: '11.3.244.8' }, lts: { isLts: false }, + alpha: { isAlpha: false }, releaseDate: '2026-03-26', modules: { version: '115' }, }, @@ -82,6 +83,7 @@ describe('generateReleaseData', () => { semver: { major: 22, raw: '22.7.0' }, dependencies: { npm: '10.9.0', v8: '12.4.254.10' }, lts: { isLts: true }, + alpha: { isAlpha: false }, releaseDate: '2026-02-18', modules: { version: '124' }, }, @@ -110,6 +112,7 @@ describe('generateReleaseData', () => { semver: { major: 24, raw: '24.1.0' }, dependencies: { npm: '11.1.0', v8: '13.0.12.7' }, lts: { isLts: false }, + alpha: { isAlpha: false }, releaseDate: '2026-03-10', modules: { version: '130' }, }, @@ -138,6 +141,7 @@ describe('generateReleaseData', () => { semver: { major: 27, raw: '27.0.0-alpha.0.0.0' }, dependencies: { npm: '11.6.0', v8: '14.6.0.0' }, lts: { isLts: false }, + alpha: { isAlpha: true }, releaseDate: '2026-10-28', modules: { version: '150' }, }, @@ -167,6 +171,7 @@ describe('generateReleaseData', () => { semver: { major: 27, raw: '27.0.0' }, dependencies: { npm: '11.6.0', v8: '14.6.0.0' }, lts: { isLts: false }, + alpha: { isAlpha: false }, releaseDate: '2027-04-22', modules: { version: '150' }, }, @@ -174,6 +179,7 @@ describe('generateReleaseData', () => { semver: { major: 27, raw: '27.0.0-alpha.0.0.0' }, dependencies: { npm: '11.6.0', v8: '14.6.0.0' }, lts: { isLts: false }, + alpha: { isAlpha: true }, releaseDate: '2026-10-28', modules: { version: '150' }, }, @@ -206,6 +212,7 @@ describe('generateReleaseData', () => { semver: { major: 26, raw: '26.2.0' }, dependencies: { npm: '11.3.1', v8: '13.2.20.1' }, lts: { isLts: false }, + alpha: { isAlpha: false }, releaseDate: '2026-04-01', modules: { version: '132' }, }, @@ -213,6 +220,7 @@ describe('generateReleaseData', () => { semver: { major: 26, raw: '26.0.0' }, dependencies: { npm: '11.0.0', v8: '13.1.0.0' }, lts: { isLts: false }, + alpha: { isAlpha: false }, releaseDate: '2025-10-21', modules: { version: '131' }, }, diff --git a/apps/site/next-data/generators/majorNodeReleases.mjs b/apps/site/next-data/generators/majorNodeReleases.mjs index d280db0fd536c..28cb0dc07e1ed 100644 --- a/apps/site/next-data/generators/majorNodeReleases.mjs +++ b/apps/site/next-data/generators/majorNodeReleases.mjs @@ -1,69 +1,14 @@ 'use strict'; import nodevu from '@nodevu/core'; -import semVer from 'semver'; import { fetchWithRetry } from '#site/next.fetch.mjs'; -const toNodevuRelease = ({ version, date, npm, v8, modules, lts }) => { - const { version: raw, major, minor, patch } = semVer.parse(version); - - return { - semver: { raw, major, minor, patch, line: `v${major}` }, - releaseDate: date, - modules: { version: modules }, - dependencies: { npm, v8 }, - lts: { isLts: !!lts }, - }; -}; - /** * Filters Node.js release data to return only major releases with documented support. */ export default async function getMajorNodeReleases() { - const preReleases = []; - let schedule = {}; - - // nodevu coerces versions (v27.0.0-alpha.0.0.0 becomes v27.0.0), hence - // pre-releases are removed before nodevu parses index.json and added back - const fetchWithoutPreReleases = async (url, options) => { - const data = await fetchWithRetry(url, options).then(r => r.json()); - - if (!Array.isArray(data)) { - schedule = data; - - return Response.json(data); - } - - preReleases.push( - ...data.filter(({ version }) => semVer.prerelease(version)) - ); - - return Response.json(data.filter(r => !preReleases.includes(r))); - }; - - const releaseLines = await nodevu({ fetch: fetchWithoutPreReleases }); - - const preReleaseLines = {}; - - preReleases.forEach(release => { - const line = `v${semVer.major(release.version)}`; - - if (!releaseLines[line] && !preReleaseLines[line]) { - const { codename, ...dates } = schedule[line] ?? {}; - - preReleaseLines[line] = { - releases: {}, - support: schedule[line] && { codename, phases: { dates } }, - }; - } - - const { releases } = releaseLines[line] ?? preReleaseLines[line]; - - releases[release.version] = toNodevuRelease(release); - }); - - const nodevuData = { ...preReleaseLines, ...releaseLines }; + const nodevuData = await nodevu({ fetch: fetchWithRetry }); return Object.entries(nodevuData).filter(([version, { support }]) => { // Filter out those without documented support diff --git a/apps/site/next-data/generators/releaseData.mjs b/apps/site/next-data/generators/releaseData.mjs index 890acb5b3fa3c..cdf6e6c203ffc 100644 --- a/apps/site/next-data/generators/releaseData.mjs +++ b/apps/site/next-data/generators/releaseData.mjs @@ -1,7 +1,5 @@ 'use strict'; -import semVer from 'semver'; - import getMajorNodeReleases from './majorNodeReleases.mjs'; // Gets the appropriate release status for each major release @@ -16,7 +14,7 @@ const getNodeReleaseStatus = (latest, eol) => { return 'LTS'; } - if (semVer.prerelease(latest.semver.raw)) { + if (latest.alpha.isAlpha) { return 'Alpha'; }