diff --git a/apps/site/types/download.ts b/apps/site/types/download.ts index a22c0fc6ba53a..ac11cb7c1da32 100644 --- a/apps/site/types/download.ts +++ b/apps/site/types/download.ts @@ -19,6 +19,8 @@ type DownloadCompatibility = { installMethod?: Array; platform?: Array; semver?: Array; + /** At least one range must match when this field is set. */ + semverAny?: Array; releases?: Array; }; diff --git a/apps/site/util/__tests__/download.test.mjs b/apps/site/util/__tests__/download.test.mjs index c1aa3ffd1c655..8ecd39ba0a253 100644 --- a/apps/site/util/__tests__/download.test.mjs +++ b/apps/site/util/__tests__/download.test.mjs @@ -10,6 +10,8 @@ import { PLATFORMS, } from '#site/util/download'; +import { buildReleaseArtifacts } from '../download/archive'; + describe('parseCompat', () => { it('should handle all OS, install methods, and package managers', () => { OPERATING_SYSTEMS.forEach(os => { @@ -46,6 +48,38 @@ describe('parseCompat', () => { }); }); + it('should enable x64 musl only for releases that publish the artifact', () => { + const platform = PLATFORMS.LINUX.find(({ value }) => value === 'x64-musl'); + assert.ok( + platform, + 'Linux x64 musl should be available as a manual option' + ); + + const cases = [ + ['v22.23.3', false], + ['v22.23.2', true], + ['v23.11.1', true], + ['v24.20.0', false], + ['v24.19.0', true], + ['v25.0.0', true], + ['v26.8.0', false], + ['v26.7.0', true], + ['v27.0.0', true], + ]; + + for (const [version, disabled] of cases) { + const [result] = parseCompat([platform], { + os: 'LINUX', + installMethod: '', + platform: 'x64-musl', + version, + release: { status: 'LTS' }, + }); + + assert.equal(result.disabled, disabled, `compatibility for ${version}`); + } + }); + describe('extended tests', () => { it('should disable items if OS is not supported', () => { const items = [ @@ -134,6 +168,34 @@ describe('parseCompat', () => { }); }); +describe('buildReleaseArtifacts', () => { + it('should list x64 musl only for versions with published artifacts', () => { + const cases = [ + ['v22.23.3', true], + ['v22.23.2', false], + ['v23.11.1', false], + ['v24.20.0', true], + ['v24.19.0', false], + ['v25.0.0', false], + ['v26.8.0', true], + ['v26.7.0', false], + ['v27.0.0', false], + ]; + + for (const [version, shouldInclude] of cases) { + const { binaries } = buildReleaseArtifacts( + { minorVersions: [] }, + version + ); + const artifact = binaries.find( + ({ fileName }) => fileName === `node-${version}-linux-x64-musl.tar.xz` + ); + + assert.equal(Boolean(artifact), shouldInclude, `artifact for ${version}`); + } + }); +}); + describe('nextItem', () => { it('should find the first valid item if current is invalid', () => { const items = [ diff --git a/apps/site/util/__tests__/url.test.mjs b/apps/site/util/__tests__/url.test.mjs index 1bf4b97b724a8..1e80de4e23342 100644 --- a/apps/site/util/__tests__/url.test.mjs +++ b/apps/site/util/__tests__/url.test.mjs @@ -100,6 +100,18 @@ describe('getNodeDownloadUrl', () => { ); }); + it('should return the x64 musl Linux binary URL', () => { + assert.equal( + getNodeDownloadUrl({ + versionWithPrefix: 'v24.20.0', + os: 'LINUX', + platform: 'x64-musl', + kind: 'binary', + }), + 'https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-x64-musl.tar.xz' + ); + }); + describe('MAC', () => { it('should return .pkg link for installer', () => { const url = getNodeDownloadUrl({ diff --git a/apps/site/util/download/archive.tsx b/apps/site/util/download/archive.tsx index 6c74c30470a91..e9902aecb8753 100644 --- a/apps/site/util/download/archive.tsx +++ b/apps/site/util/download/archive.tsx @@ -1,5 +1,6 @@ import semVer from 'semver'; +import { DIST_URL } from '#site/next.constants.mjs'; import { OS_NOT_SUPPORTING_INSTALLERS, PLATFORMS } from '#site/util/download'; import { getNodeDownloadUrl } from '#site/util/url'; @@ -14,8 +15,6 @@ import type { } from '#site/types'; import type { NodeRelease } from '#site/types/releases'; -import { DIST_URL } from '#site/next.constants'; - /** * Creates a download artifact from platform data */ @@ -58,15 +57,24 @@ const getCompatiblePlatforms = ( return items .filter(({ compatibility }) => { // In the download constants file (apps/site/util/download/constants.json), - // if no compatibility is defined as a semver, we treat it as supporting all + // if no semver compatibility is defined, we treat it as supporting all // versions by default. - if (!compatibility.semver) { + if (!compatibility.semver && !compatibility.semverAny) { return true; } - return compatibility.semver.every(version => - semVer.satisfies(versionWithPrefix, version) - ); + const satisfiesAll = + !compatibility.semver || + compatibility.semver.every(version => + semVer.satisfies(versionWithPrefix, version) + ); + const satisfiesAny = + !compatibility.semverAny || + compatibility.semverAny.some(version => + semVer.satisfies(versionWithPrefix, version) + ); + + return satisfiesAll && satisfiesAny; }) .map(platform => ({ os, diff --git a/apps/site/util/download/constants.json b/apps/site/util/download/constants.json index 440637e5b72dc..68241a611938e 100644 --- a/apps/site/util/download/constants.json +++ b/apps/site/util/download/constants.json @@ -65,6 +65,17 @@ "label": "x64", "value": "x64" }, + { + "label": "x64 (musl)", + "value": "x64-musl", + "compatibility": { + "semverAny": [ + ">=22.23.3 <23.0.0", + ">=24.20.0 <25.0.0", + ">=26.8.0 <27.0.0" + ] + } + }, { "label": "ARMv7", "value": "armv7l", @@ -238,7 +249,16 @@ } ], "userOptions": { - "platforms": ["arm64", "armv7l", "ppc64le", "ppc64", "s390x", "x64", "x86"], + "platforms": [ + "arm64", + "armv7l", + "ppc64le", + "ppc64", + "s390x", + "x64", + "x64-musl", + "x86" + ], "bitness": ["64", "32"], "architecture": ["arm", "x86"] }, diff --git a/apps/site/util/download/index.tsx b/apps/site/util/download/index.tsx index 69713f4fb005a..02afbdd38facd 100644 --- a/apps/site/util/download/index.tsx +++ b/apps/site/util/download/index.tsx @@ -60,6 +60,8 @@ export const parseCompat = < !compatibility.platform || compatibility.platform.includes(platform), !compatibility.semver || compatibility.semver.every(semver => satisfies(version, semver)), + !compatibility.semverAny || + compatibility.semverAny.some(semver => satisfies(version, semver)), !compatibility.releases || compatibility.releases.includes(release.status), ];