diff --git a/apps/site/components/Downloads/Release/PrebuiltDownloadButtons.tsx b/apps/site/components/Downloads/Release/PrebuiltDownloadButtons.tsx index 6b23dd1ed508e..ca037f6a443c2 100644 --- a/apps/site/components/Downloads/Release/PrebuiltDownloadButtons.tsx +++ b/apps/site/components/Downloads/Release/PrebuiltDownloadButtons.tsx @@ -9,8 +9,8 @@ import type { FC } from 'react'; import Button from '#site/components/Common/Button'; import { ReleaseContext } from '#site/providers/releaseProvider'; import { - OperatingSystemLabel, OS_NOT_SUPPORTING_INSTALLERS, + OperatingSystemLabel, } from '#site/util/downloadUtils'; import { getNodeDownloadUrl } from '#site/util/getNodeDownloadUrl'; diff --git a/apps/site/types/userOS.ts b/apps/site/types/userOS.ts index 39a3911e55b0e..76bd462f345e5 100644 --- a/apps/site/types/userOS.ts +++ b/apps/site/types/userOS.ts @@ -1,15 +1,14 @@ -export type UserOS = 'MAC' | 'WIN' | 'LINUX' | 'AIX' | 'OTHER'; +import type constants from '../util/downloadUtils/constants.json'; -export type UserBitness = '64' | '32'; +// Extract OS key type from the systems object +export type UserOS = keyof typeof constants.systems; -export type UserArchitecture = 'arm' | 'x86'; +// Derive the union type of UserPlatform from the userOptions +export type UserPlatform = (typeof constants.userOptions.platforms)[number]; -export type UserPlatform = - | 'arm64' - | 'armv7l' - | 'ppc64le' - | 'ppc64' - | 's390x' - | 'ppc64' - | 'x64' - | 'x86'; +// Derive the union type of UserBitness from the userOptions +export type UserBitness = (typeof constants.userOptions.bitness)[number]; + +// Derive the union type of UserArchitecture from the userOptions +export type UserArchitecture = + (typeof constants.userOptions.architecture)[number]; diff --git a/apps/site/util/downloadUtils.tsx b/apps/site/util/downloadUtils.tsx deleted file mode 100644 index 6536a3b6747ab..0000000000000 --- a/apps/site/util/downloadUtils.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import type { SelectValue } from '@node-core/ui-components/Common/Select'; -import * as InstallMethodIcons from '@node-core/ui-components/Icons/InstallationMethod'; -import * as OSIcons from '@node-core/ui-components/Icons/OperatingSystem'; -import * as PackageManagerIcons from '@node-core/ui-components/Icons/PackageManager'; -import satisfies from 'semver/functions/satisfies'; - -import type { NodeReleaseStatus } from '#site/types'; -import type * as Types from '#site/types/release'; -import type { UserOS, UserPlatform } from '#site/types/userOS'; - -// This is a manual list of OS's that do not support/have a way of being installed -// with an executable installer. This is used to disable the installer button. -// Note: Windows has one tiny exception for x64 on Node.js versions < 4.0.0 -export const OS_NOT_SUPPORTING_INSTALLERS: Array = [ - 'LINUX', - 'AIX', - 'OTHER', - 'LOADING', -]; - -export enum OperatingSystemLabel { - WIN = 'Windows', - MAC = 'macOS', - LINUX = 'Linux', - AIX = 'AIX', - OTHER = 'Other', - LOADING = 'N/A', -} - -export enum InstallationMethodLabel { - NVM = 'nvm', - FNM = 'fnm', - BREW = 'Brew', - CHOCO = 'Chocolatey', - DEVBOX = 'Devbox', - DOCKER = 'Docker', - N = 'n', - VOLTA = 'Volta', -} - -export enum PackageManagerLabel { - NPM = 'npm', - YARN = 'Yarn', - PNPM = 'pnpm', -} - -type DownloadCompatibility = { - os: Array; - installMethod: Array; - platform: Array; - semver: Array; - releases: Array; -}; - -// Defines the Type definition for a Release Dropdown Item -type DownloadDropdownItem = { - // A label to be used within the Dropdown message - label: string; - // A flag that indicates if the item is recommended or not (official or community based) - recommended?: boolean; - // A URL pointing to the docs or support page for the item - url?: string; - // A bottom info that provides additional information about the item - info?: IntlMessageKeys; - // A compatibility object that defines the compatibility of the item with the current Release Context - compatibility: Partial; -} & Omit, 'label'>; - -// This function is used to get the next item in the dropdown -// when the current item is disabeld/excluded/not valid -// And this is useful when a parent release value (i.e. OS) is changed -// and requires the current dropdown (i.e. Platform) to be updated -export const nextItem = ( - current: T, - items: Array> -): T => { - const item = items.find(({ value }) => String(value) === String(current)); - - const isDisabledOrExcluded = !item || item.disabled; - - if (isDisabledOrExcluded) { - const nextItem = items.find(({ disabled }) => !disabled); - - if (nextItem) { - return nextItem.value; - } - } - - return current; -}; - -// This function is used to parse the compatibility of the dropdown items -// In a nice and static way that allows a lot of abstraction and flexibility -export const parseCompat = < - K extends string, - T extends DownloadDropdownItem, ->( - items: Array, - { os, installMethod, platform, version, release }: Types.ReleaseContextType -): Array => { - const satisfiesSemver = (semver: string) => satisfies(version, semver); - - const supportsOS = (i: T['compatibility']) => i.os?.includes(os) ?? true; - - const supportsInstallMethod = (i: T['compatibility']) => - i.installMethod?.includes(installMethod) ?? true; - - const supportsPlatform = (i: T['compatibility']) => - i.platform?.includes(platform) ?? true; - - const supportsVersion = (i: T['compatibility']) => - i.semver?.some(satisfiesSemver) ?? true; - - const supportsRelease = (i: T['compatibility']) => - i.releases?.includes(release.status) ?? true; - - return items.map(item => ({ - ...item, - disabled: - !supportsOS(item.compatibility) || - !supportsInstallMethod(item.compatibility) || - !supportsPlatform(item.compatibility) || - !supportsVersion(item.compatibility) || - !supportsRelease(item.compatibility), - })); -}; - -export const OPERATING_SYSTEMS: Array> = [ - { - label: OperatingSystemLabel.WIN, - value: 'WIN', - compatibility: {}, - iconImage: , - }, - { - label: OperatingSystemLabel.MAC, - value: 'MAC', - compatibility: {}, - iconImage: , - }, - { - label: OperatingSystemLabel.LINUX, - value: 'LINUX', - compatibility: {}, - iconImage: , - }, - { - label: OperatingSystemLabel.AIX, - value: 'AIX', - compatibility: { installMethod: [''], semver: ['>= 6.7.0'] }, - iconImage: , - }, -]; - -export const INSTALL_METHODS: Array< - DownloadDropdownItem & - // Since the ReleaseCodeBox requires an info key to be provided, we force this - // to be mandatory for install methods - Required< - Pick, 'info' | 'url'> - > -> = [ - { - label: InstallationMethodLabel.NVM, - value: 'NVM', - compatibility: { os: ['MAC', 'LINUX', 'OTHER'] }, - iconImage: , - recommended: true, - url: 'https://github.com/nvm-sh/nvm', - info: 'layouts.download.codeBox.platformInfo.nvm', - }, - { - label: InstallationMethodLabel.FNM, - value: 'FNM', - compatibility: { os: ['MAC', 'LINUX', 'WIN'] }, - iconImage: , - recommended: true, - url: 'https://github.com/Schniz/fnm', - info: 'layouts.download.codeBox.platformInfo.fnm', - }, - { - label: InstallationMethodLabel.BREW, - value: 'BREW', - compatibility: { os: ['MAC', 'LINUX'], releases: ['Current', 'LTS'] }, - iconImage: , - url: 'https://brew.sh/', - info: 'layouts.download.codeBox.platformInfo.brew', - }, - { - label: InstallationMethodLabel.DEVBOX, - value: 'DEVBOX', - compatibility: { os: ['MAC', 'LINUX'] }, - iconImage: , - url: 'https://jetify.com/devbox/', - info: 'layouts.download.codeBox.platformInfo.devbox', - }, - { - label: InstallationMethodLabel.CHOCO, - value: 'CHOCO', - compatibility: { os: ['WIN'] }, - iconImage: , - url: 'https://chocolatey.org/', - info: 'layouts.download.codeBox.platformInfo.choco', - }, - { - label: InstallationMethodLabel.DOCKER, - value: 'DOCKER', - compatibility: { os: ['WIN', 'MAC', 'LINUX'] }, - iconImage: , - recommended: true, - url: 'https://docs.docker.com/get-started/get-docker/', - info: 'layouts.download.codeBox.platformInfo.docker', - }, - { - label: InstallationMethodLabel.N, - value: 'N', - compatibility: { os: ['MAC', 'LINUX'] }, - iconImage: , - url: 'https://github.com/tj/n', - info: 'layouts.download.codeBox.platformInfo.n', - }, - { - label: InstallationMethodLabel.VOLTA, - value: 'VOLTA', - compatibility: { os: ['WIN', 'MAC', 'LINUX'] }, - iconImage: , - url: 'https://docs.volta.sh/guide/getting-started', - info: 'layouts.download.codeBox.platformInfo.volta', - }, -]; - -export const PACKAGE_MANAGERS: Array< - DownloadDropdownItem -> = [ - { - label: PackageManagerLabel.NPM, - value: 'NPM', - compatibility: {}, - iconImage: , - }, - { - label: PackageManagerLabel.YARN, - value: 'YARN', - compatibility: { semver: ['>= v14.19.0', '>= v16.9.0'] }, - iconImage: , - }, - { - label: PackageManagerLabel.PNPM, - value: 'PNPM', - compatibility: { semver: ['>= v14.19.0', '>= v16.9.0'] }, - iconImage: , - }, -]; - -export const PLATFORMS: Record< - UserOS, - Array> -> = { - WIN: [ - { - label: 'x64', - value: 'x64', - compatibility: {}, - }, - { - label: 'x86', - value: 'x86', - compatibility: { semver: ['< 23.0.0'] }, - }, - { - label: 'ARM64', - value: 'arm64', - compatibility: { semver: ['>= 19.9.0'] }, - }, - ], - MAC: [ - { - label: 'x64', - value: 'x64', - compatibility: {}, - }, - { - label: 'ARM64', - value: 'arm64', - compatibility: {}, - }, - ], - LINUX: [ - { - label: 'x64', - value: 'x64', - compatibility: {}, - }, - { - label: 'ARMv7', - value: 'armv7l', - compatibility: { semver: ['>= 4.0.0'] }, - }, - { - label: 'ARM64', - value: 'arm64', - compatibility: { semver: ['>= 4.0.0'] }, - }, - { - label: 'Power LE', - value: 'ppc64le', - compatibility: { semver: ['>= 4.4.0'] }, - }, - { - label: 'System Z', - value: 's390x', - compatibility: { semver: ['>= 6.6.0'] }, - }, - ], - AIX: [ - { - label: 'Power', - value: 'ppc64', - compatibility: { semver: ['>= 6.7.0'] }, - }, - ], - OTHER: [], -}; diff --git a/apps/site/util/downloadUtils/constants.json b/apps/site/util/downloadUtils/constants.json new file mode 100644 index 0000000000000..526e738fb0357 --- /dev/null +++ b/apps/site/util/downloadUtils/constants.json @@ -0,0 +1,226 @@ +{ + "systems": { + "WIN": { + "name": "Windows", + "icon": "Microsoft", + "supportsInstallers": true, + "compatibility": {}, + "platforms": [ + { + "label": "x64", + "value": "x64" + }, + { + "label": "x86", + "value": "x86", + "compatibility": { + "semver": ["< 23.0.0"] + } + }, + { + "label": "ARM64", + "value": "arm64", + "compatibility": { + "semver": [">= 19.9.0"] + } + } + ] + }, + "MAC": { + "name": "macOS", + "icon": "Apple", + "supportsInstallers": true, + "compatibility": {}, + "platforms": [ + { + "label": "x64", + "value": "x64" + }, + { + "label": "ARM64", + "value": "arm64", + "compatibility": { + "semver": [">= 19.9.0"] + } + } + ] + }, + "LINUX": { + "name": "Linux", + "icon": "Linux", + "supportsInstallers": false, + "compatibility": {}, + "platforms": [ + { + "label": "x64", + "value": "x64" + }, + { + "label": "ARMv7", + "value": "armv7l", + "compatibility": { + "semver": [">= 4.0.0"] + } + }, + { + "label": "ARM64", + "value": "arm64", + "compatibility": { + "semver": [">= 4.0.0"] + } + }, + { + "label": "Power LE", + "value": "ppc64le", + "compatibility": { + "semver": [">= 4.4.0"] + } + }, + { + "label": "System Z", + "value": "s390x", + "compatibility": { + "semver": [">= 6.6.0"] + } + } + ] + }, + "AIX": { + "name": "AIX", + "icon": "AIX", + "supportsInstallers": false, + "compatibility": { + "semver": [">= 6.7.0"] + }, + "platforms": [ + { + "label": "Power", + "value": "ppc64", + "compatibility": { + "semver": [">= 6.7.0"] + } + } + ] + }, + "OTHER": { + "name": "Other", + "supportsInstallers": false, + "platforms": [] + }, + "LOADING": { + "name": "N/A", + "supportsInstallers": false, + "platforms": [] + } + }, + "installMethods": [ + { + "id": "NVM", + "icon": "NVM", + "name": "nvm", + "compatibility": { + "os": ["MAC", "LINUX", "OTHER"] + }, + "recommended": true, + "url": "https://github.com/nvm-sh/nvm", + "info": "layouts.download.codeBox.platformInfo.nvm" + }, + { + "id": "FNM", + "icon": "NVM", + "name": "fnm", + "compatibility": { + "os": ["MAC", "LINUX", "WIN"] + }, + "recommended": true, + "url": "https://github.com/Schniz/fnm", + "info": "layouts.download.codeBox.platformInfo.fnm" + }, + { + "id": "BREW", + "icon": "Homebrew", + "name": "Brew", + "compatibility": { + "os": ["MAC", "LINUX"], + "releases": ["Current", "LTS"] + }, + "url": "https://brew.sh/", + "info": "layouts.download.codeBox.platformInfo.brew" + }, + { + "id": "DEVBOX", + "icon": "Devbox", + "name": "Devbox", + "compatibility": { + "os": ["MAC", "LINUX"] + }, + "url": "https://jetify.com/devbox/", + "info": "layouts.download.codeBox.platformInfo.devbox" + }, + { + "id": "CHOCO", + "icon": "Choco", + "name": "Chocolatey", + "compatibility": { + "os": ["WIN"] + }, + "url": "https://chocolatey.org/", + "info": "layouts.download.codeBox.platformInfo.choco" + }, + { + "id": "DOCKER", + "icon": "Docker", + "name": "Docker", + "compatibility": { + "os": ["WIN", "MAC", "LINUX"] + }, + "recommended": true, + "url": "https://docs.docker.com/get-started/get-docker/", + "info": "layouts.download.codeBox.platformInfo.docker" + }, + { + "id": "N", + "icon": "N", + "name": "n", + "compatibility": { + "os": ["MAC", "LINUX"] + }, + "url": "https://github.com/tj/n", + "info": "layouts.download.codeBox.platformInfo.n" + }, + { + "id": "VOLTA", + "icon": "Volta", + "name": "Volta", + "compatibility": { + "os": ["WIN", "MAC", "LINUX"] + }, + "url": "https://docs.volta.sh/guide/getting-started", + "info": "layouts.download.codeBox.platformInfo.volta" + } + ], + "packageManagers": [ + { + "id": "NPM", + "name": "npm", + "compatibility": {} + }, + { + "id": "YARN", + "name": "Yarn", + "compatibility": { + "semver": [">= v14.19.0", ">= v16.9.0"] + } + }, + { + "id": "PNPM", + "name": "pnpm", + "compatibility": {} + } + ], + "userOptions": { + "platforms": ["arm64", "armv7l", "ppc64le", "ppc64", "s390x", "x64", "x86"], + "bitness": ["64", "32"], + "architecture": ["arm", "x86"] + } +} diff --git a/apps/site/util/downloadUtils/index.tsx b/apps/site/util/downloadUtils/index.tsx new file mode 100644 index 0000000000000..2ecfeb77bdea0 --- /dev/null +++ b/apps/site/util/downloadUtils/index.tsx @@ -0,0 +1,151 @@ +import type { SelectValue } from '@node-core/ui-components/Common/Select'; +import * as InstallMethodIcons from '@node-core/ui-components/Icons/InstallationMethod'; +import * as OSIcons from '@node-core/ui-components/Icons/OperatingSystem'; +import * as PackageManagerIcons from '@node-core/ui-components/Icons/PackageManager'; +import type { ElementType } from 'react'; +import satisfies from 'semver/functions/satisfies'; + +import type { NodeReleaseStatus } from '#site/types'; +import type * as Types from '#site/types/release'; +import type { UserOS, UserPlatform } from '#site/types/userOS'; + +import constants from './constants.json'; + +const { systems, installMethods, packageManagers } = constants; + +// Extract the non-installer supporting OSes +export const OS_NOT_SUPPORTING_INSTALLERS = Object.entries(systems) + .filter(([, data]) => !data.supportsInstallers) + .map(([key]) => key); + +// Create OS label mapping for backward compatibility +export const OperatingSystemLabel = Object.fromEntries( + Object.entries(systems).map(([key, data]) => [key, data.name]) +); + +// Base types for dropdown functionality +type DownloadCompatibility = { + os?: Array; + installMethod?: Array; + platform?: Array; + semver?: Array; + releases?: Array; +}; + +type DownloadDropdownItem = { + label: string; + recommended?: boolean; + url?: string; + info?: string; + compatibility: DownloadCompatibility; +} & Omit, 'label'>; + +/** + * Gets the next valid item when current item is disabled/excluded + */ +export const nextItem = ( + current: T, + items: Array> +): T => { + const currentItem = items.find( + ({ value }) => String(value) === String(current) + ); + return currentItem && !currentItem.disabled + ? current + : items.find(({ disabled }) => !disabled)?.value || current; +}; + +/** + * Parses compatibility of dropdown items based on context + */ +export const parseCompat = < + K extends string, + T extends DownloadDropdownItem, +>( + items: Array, + { os, installMethod, platform, version, release }: Types.ReleaseContextType +): Array => { + const checkCompatibility = (compatibility: T['compatibility']) => { + const checks = [ + !compatibility.os || compatibility.os.includes(os), + !compatibility.installMethod || + compatibility.installMethod.includes(installMethod), + !compatibility.platform || compatibility.platform.includes(platform), + !compatibility.semver || + compatibility.semver.some(semver => satisfies(version, semver)), + !compatibility.releases || + compatibility.releases.includes(release.status), + ]; + + return checks.every(Boolean); + }; + + return items.map(item => ({ + ...item, + disabled: !checkCompatibility(item.compatibility), + })); +}; + +/** + * Creates an icon element for a component + */ +const createIcon = ( + IconModule: Record, + iconName: string +) => { + const IconComponent = IconModule[iconName]; + return ; +}; + +// Operating System dropdown items +type ActualSystems = Omit; +export const OPERATING_SYSTEMS = Object.entries(systems as ActualSystems) + .filter(([key]) => key !== 'LOADING' && key !== 'OTHER') + .map(([key, data]) => ({ + label: data.name, + value: key as UserOS, + compatibility: data.compatibility, + iconImage: createIcon(OSIcons, data.icon), + })); + +// Installation Method dropdown items +export const INSTALL_METHODS = installMethods.map(method => ({ + key: method.id, + value: method.id as Types.InstallationMethod, + label: method.name, + iconImage: createIcon(InstallMethodIcons, method.icon), + recommended: method.recommended, + url: method.url, + info: method.info, + compatibility: { + ...method.compatibility, + os: method.compatibility?.os?.map(os => os as UserOS), + releases: method.compatibility?.releases?.map( + release => release as NodeReleaseStatus + ), + }, +})); + +// Package Manager dropdown items +export const PACKAGE_MANAGERS = packageManagers.map(manager => ({ + key: manager.id, + value: manager.id as Types.PackageManager, + label: manager.name, + iconImage: createIcon(PackageManagerIcons, manager.id), + compatibility: { + ...manager.compatibility, + semver: manager.compatibility?.semver, + }, +})); + +// Platform-specific dropdown items +export const PLATFORMS = Object.fromEntries( + Object.entries(systems).map(([key, data]) => [ + key, + data.platforms.map(platform => ({ + label: platform.label, + value: platform.value as UserPlatform, + compatibility: platform.compatibility || {}, + })), + ]) +) as Record>>;