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.",