diff --git a/.gitignore b/.gitignore index aa56284814f58..f82222f8decc4 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ build public/robots.txt public/sitemap.xml public/en/feed/*.xml +public/node-releases-data.json pages/en/blog/year-[0-9][0-9][0-9][0-9].md # Jest diff --git a/.prettierignore b/.prettierignore index 7537b2a7a7707..e3c3f6b247bd4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,6 +13,7 @@ CODEOWNERS public/en/user-survey-report public/static/documents public/static/legacy +public/node-releases-data.json # We don't want to lint/prettify the Coverage Results coverage diff --git a/.storybook/constants.ts b/.storybook/constants.ts deleted file mode 100644 index 68be821be0aea..0000000000000 --- a/.storybook/constants.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { AppProps, NodeVersionData } from '../types'; - -const nodeVersionData: NodeVersionData[] = [ - { - node: 'v19.8.1', - nodeNumeric: '19.8.1', - nodeMajor: 'v19.x', - npm: '9.5.1', - isLts: false, - }, - { - node: 'v18.15.0', - nodeNumeric: '18.15.0', - nodeMajor: 'v18.x', - npm: '9.5.0', - isLts: true, - }, -]; - -export const pageProps = { nodeVersionData }; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 9103891e2ce52..914290f2a32df 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,11 +1,10 @@ import type { Preview } from '@storybook/react'; import NextImage from 'next/image'; import { ThemeProvider } from 'next-themes'; -import { NodeDataProvider } from '../providers/nodeDataProvider'; +import { NodeReleasesProvider } from '../providers/nodeReleasesProvider'; import { LocaleProvider } from '../providers/localeProvider'; import { openSans } from '../util/nextFonts'; import BaseApp, { setAppFont } from '../next.app'; -import { pageProps } from './constants'; import '../styles/index.scss'; @@ -34,11 +33,11 @@ export const decorators = [ - +
-
+
diff --git a/__fixtures__/nodeReleases.tsx b/__fixtures__/nodeReleases.tsx new file mode 100644 index 0000000000000..a4f69beddcf68 --- /dev/null +++ b/__fixtures__/nodeReleases.tsx @@ -0,0 +1,97 @@ +import type { NodeRelease } from '../types'; + +export const createNodeReleases = (): NodeRelease[] => [ + { + currentStart: '2023-04-18', + ltsStart: '2023-10-24', + maintenanceStart: '2024-10-22', + endOfLife: '2026-04-30', + major: 20, + version: '20.2.0', + versionWithPrefix: 'v20.2.0', + codename: '', + isLts: false, + status: 'Current', + npm: '9.6.6', + v8: '11.3.244.8', + releaseDate: '2023-05-16', + modules: '115', + }, + { + currentStart: '2022-10-18', + maintenanceStart: '2023-04-01', + endOfLife: '2023-06-01', + major: 19, + version: '19.9.0', + versionWithPrefix: 'v19.9.0', + codename: '', + isLts: false, + status: 'End-of-life', + npm: '9.6.3', + v8: '10.8.168.25', + releaseDate: '2023-04-10', + modules: '111', + }, + { + currentStart: '2022-04-19', + ltsStart: '2022-10-25', + maintenanceStart: '2023-10-18', + endOfLife: '2025-04-30', + major: 18, + version: '18.16.0', + versionWithPrefix: 'v18.16.0', + codename: 'Hydrogen', + isLts: true, + status: 'Active LTS', + npm: '9.5.1', + v8: '10.2.154.26', + releaseDate: '2023-04-12', + modules: '108', + }, + { + currentStart: '2021-10-19', + maintenanceStart: '2022-04-01', + endOfLife: '2022-06-01', + major: 17, + version: '17.9.1', + versionWithPrefix: 'v17.9.1', + codename: '', + isLts: false, + status: 'End-of-life', + npm: '8.11.0', + v8: '9.6.180.15', + releaseDate: '2022-06-01', + modules: '102', + }, + { + currentStart: '2021-04-20', + ltsStart: '2021-10-26', + maintenanceStart: '2022-10-18', + endOfLife: '2023-09-11', + major: 16, + version: '16.20.0', + versionWithPrefix: 'v16.20.0', + codename: 'Gallium', + isLts: true, + status: 'Maintenance LTS', + npm: '8.19.4', + v8: '9.4.146.26', + releaseDate: '2023-03-28', + modules: '93', + }, + { + currentStart: '2020-10-20', + maintenanceStart: '2021-04-01', + endOfLife: '2021-06-01', + major: 15, + version: '15.14.0', + versionWithPrefix: 'v15.14.0', + codename: '', + isLts: false, + status: 'End-of-life', + npm: '7.7.6', + v8: '8.6.395.17', + releaseDate: '2021-04-06', + modules: '88', + }, +]; diff --git a/components/Downloads/DownloadList.tsx b/components/Downloads/DownloadList.tsx index ce144d256eec9..08efaade78d6e 100644 --- a/components/Downloads/DownloadList.tsx +++ b/components/Downloads/DownloadList.tsx @@ -1,17 +1,15 @@ import { FormattedMessage } from 'react-intl'; import LocalizedLink from '../LocalizedLink'; import { useNavigation } from '../../hooks/useNavigation'; -import type { NodeVersionData } from '../../types'; +import type { NodeRelease } from '../../types'; import type { FC } from 'react'; -type DownloadListProps = Pick; - -const DownloadList: FC = ({ node }) => { +const DownloadList: FC = ({ versionWithPrefix }) => { const { getSideNavigation } = useNavigation(); const [, ...downloadNavigation] = getSideNavigation('download', { - shaSums: { nodeVersion: node }, - allDownloads: { nodeVersion: node }, + shaSums: { nodeVersion: versionWithPrefix }, + allDownloads: { nodeVersion: versionWithPrefix }, }); return ( diff --git a/components/Downloads/DownloadReleasesTable.tsx b/components/Downloads/DownloadReleasesTable.tsx index a258c27b00bdf..01b74cd21a9a9 100644 --- a/components/Downloads/DownloadReleasesTable.tsx +++ b/components/Downloads/DownloadReleasesTable.tsx @@ -2,55 +2,57 @@ import { FormattedMessage } from 'react-intl'; import Link from 'next/link'; import { getNodejsChangelog } from '../../util/getNodeJsChangelog'; import { getNodeApiLink } from '../../util/getNodeApiLink'; -import type { ExtendedNodeVersionData } from '../../types'; +import { useNodeReleases } from '../../hooks/useNodeReleases'; import type { FC } from 'react'; -type DownloadReleasesTableProps = { releases: ExtendedNodeVersionData[] }; +const DownloadReleasesTable: FC = () => { + const { releases } = useNodeReleases(); -const DownloadReleasesTable: FC = ({ - releases, -}) => ( - - - - - - - - - - - - - - {releases.map((release, key) => ( - - - - - - - - + {releases.map(release => ( + + + + + + + + + + ))} + +
VersionLTSDateV8npm - NODE_MODULE_VERSION[1] - -
Node.js {release.nodeNumeric}{release.ltsName} - - {release.v8}{release.npm}{release.modules} - - - - - - - - - + return ( + + + + + + + + + + - ))} - -
VersionLTSDateV8npm + NODE_MODULE_VERSION[1] +
-); + +
Node.js {release.version}{release.codename} + + {release.v8}{release.npm}{release.modules} + + + + + + + + + +
+ ); +}; export default DownloadReleasesTable; diff --git a/components/Downloads/PrimaryDownloadMatrix.tsx b/components/Downloads/PrimaryDownloadMatrix.tsx index 39541801c8423..9376a67064e75 100644 --- a/components/Downloads/PrimaryDownloadMatrix.tsx +++ b/components/Downloads/PrimaryDownloadMatrix.tsx @@ -1,27 +1,25 @@ import classNames from 'classnames'; import semVer from 'semver'; import LocalizedLink from '../LocalizedLink'; +import { useDetectOS } from '../../hooks/useDetectOS'; import { useNextraContext } from '../../hooks/useNextraContext'; -import type { NodeVersionData, LegacyDownloadsFrontMatter } from '../../types'; +import type { LegacyDownloadsFrontMatter, NodeRelease } from '../../types'; import type { FC } from 'react'; -type PrimaryDownloadMatrixProps = Pick< - NodeVersionData, - 'isLts' | 'node' | 'nodeNumeric' | 'npm' ->; - // @TODO: Instead of using a static list it should be created dynamically. This is done on `nodejs.dev` // since this is a temporary solution and going to be fixed in the future. -const PrimaryDownloadMatrix: FC = ({ - node, - nodeNumeric, - npm, +const PrimaryDownloadMatrix: FC = ({ + version, + versionWithPrefix, isLts, + npm, }) => { const nextraContext = useNextraContext(); + const { bitness } = useDetectOS(); + const { downloads } = nextraContext.frontMatter as LegacyDownloadsFrontMatter; - const hasWindowsArm64 = semVer.satisfies(node, '>= 19.9.0'); + const hasWindowsArm64 = semVer.satisfies(version, '>= 19.9.0'); const getIsVersionClassName = (isCurrent: boolean) => classNames({ 'is-version': isCurrent }); @@ -29,7 +27,7 @@ const PrimaryDownloadMatrix: FC = ({ return (

- {downloads.currentVersion}: {nodeNumeric} ( + {downloads.currentVersion}: {version} ( {downloads.includes || 'includes'} npm {npm})

{downloads.intro}

@@ -60,9 +58,8 @@ const PrimaryDownloadMatrix: FC = ({ @@ -115,19 +122,23 @@ const PrimaryDownloadMatrix: FC = ({ {downloads.WindowsInstaller} (.msi) - + 32-bit - + 64-bit {hasWindowsArm64 && ( ARM64 @@ -139,14 +150,14 @@ const PrimaryDownloadMatrix: FC = ({ {downloads.WindowsBinary} (.zip) 32-bit 64-bit @@ -154,7 +165,7 @@ const PrimaryDownloadMatrix: FC = ({ {hasWindowsArm64 && ( ARM64 @@ -165,7 +176,9 @@ const PrimaryDownloadMatrix: FC = ({ {downloads.MacOSInstaller} (.pkg) - + 64-bit / ARM64 @@ -174,14 +187,14 @@ const PrimaryDownloadMatrix: FC = ({ {downloads.MacOSBinary} (.tar.gz) 64-bit ARM64 @@ -192,7 +205,7 @@ const PrimaryDownloadMatrix: FC = ({ {downloads.LinuxBinaries} (x64) 64-bit @@ -202,14 +215,14 @@ const PrimaryDownloadMatrix: FC = ({ {downloads.LinuxBinaries} (ARM) ARMv7 ARMv8 @@ -219,8 +232,10 @@ const PrimaryDownloadMatrix: FC = ({ {downloads.SourceCode} - - node-{node}.tar.gz + + node-{versionWithPrefix}.tar.gz diff --git a/components/Downloads/SecondaryDownloadMatrix.tsx b/components/Downloads/SecondaryDownloadMatrix.tsx index e6bffb520f17e..e8e1d48a07fe6 100644 --- a/components/Downloads/SecondaryDownloadMatrix.tsx +++ b/components/Downloads/SecondaryDownloadMatrix.tsx @@ -1,14 +1,14 @@ import DownloadList from './DownloadList'; import { useNextraContext } from '../../hooks/useNextraContext'; -import type { NodeVersionData, LegacyDownloadsFrontMatter } from '../../types'; +import { WithNodeRelease } from '../../providers/withNodeRelease'; +import type { LegacyDownloadsFrontMatter, NodeRelease } from '../../types'; import type { FC } from 'react'; -type SecondaryDownloadMatrixProps = Pick; - // @TODO: Instead of using a static list it should be created dynamically. This is done on `nodejs.dev` // since this is a temporary solution and going to be fixed in the future. -const SecondaryDownloadMatrix: FC = ({ - node, +const SecondaryDownloadMatrix: FC = ({ + versionWithPrefix, + status, }) => { const nextraContext = useNextraContext(); @@ -33,7 +33,7 @@ const SecondaryDownloadMatrix: FC = ({ {additional.LinuxPowerSystems} 64-bit @@ -44,7 +44,7 @@ const SecondaryDownloadMatrix: FC = ({ {additional.LinuxSystemZ} 64-bit @@ -54,7 +54,7 @@ const SecondaryDownloadMatrix: FC = ({ {additional.AIXPowerSystems} 64-bit @@ -63,7 +63,9 @@ const SecondaryDownloadMatrix: FC = ({ - + + {({ release }) => } +
); }; diff --git a/components/Home/HomeDownloadButton.tsx b/components/Home/HomeDownloadButton.tsx index 467907620f8a9..b32be0c5edd62 100644 --- a/components/Home/HomeDownloadButton.tsx +++ b/components/Home/HomeDownloadButton.tsx @@ -1,30 +1,28 @@ import LocalizedLink from '../LocalizedLink'; +import { useDetectOS } from '../../hooks/useDetectOS'; import { useNextraContext } from '../../hooks/useNextraContext'; +import { downloadUrlByOS } from '../../util/downloadUrlByOS'; import { getNodejsChangelog } from '../../util/getNodeJsChangelog'; -import type { NodeVersionData } from '../../types'; import type { FC } from 'react'; +import type { NodeRelease } from '../../types'; -type HomeDownloadButtonProps = Pick< - NodeVersionData, - 'isLts' | 'node' | 'nodeMajor' | 'nodeNumeric' ->; - -const HomeDownloadButton: FC = ({ - node, - nodeMajor, - nodeNumeric, +const HomeDownloadButton: FC = ({ + major, + version, + versionWithPrefix, isLts, }) => { const { frontMatter: { labels }, } = useNextraContext(); - const nodeDownloadLink = `https://nodejs.org/dist/${node}/`; - const nodeApiLink = `https://nodejs.org/dist/latest-${nodeMajor}/docs/api/`; + const { os, bitness } = useDetectOS(); + + const nodeDownloadLink = downloadUrlByOS(versionWithPrefix, os, bitness); + const nodeApiLink = `https://nodejs.org/dist/latest-v${major}.x/docs/api/`; const nodeAllDownloadsLink = `/download${isLts ? '/' : '/current'}`; const nodeDownloadTitle = - `${labels.download} ${nodeNumeric}` + - ` ${labels[isLts ? 'lts' : 'current']}`; + `${labels.download} ${version}` + ` ${labels[isLts ? 'lts' : 'current']}`; return (
@@ -32,9 +30,9 @@ const HomeDownloadButton: FC = ({ href={nodeDownloadLink} className="home-downloadbutton" title={nodeDownloadTitle} - data-version={node} + data-version={versionWithPrefix} > - {nodeNumeric} {labels[isLts ? 'lts' : 'current']} + {version} {labels[isLts ? 'lts' : 'current']} {labels[`tagline-${isLts ? 'lts' : 'current'}`]} @@ -45,7 +43,7 @@ const HomeDownloadButton: FC = ({
  • - + {labels.changelog}
  • diff --git a/constants/swr.ts b/constants/swr.ts deleted file mode 100644 index 9905c9dda32bf..0000000000000 --- a/constants/swr.ts +++ /dev/null @@ -1 +0,0 @@ -export const UserAgentBitness = 'user-agent-bitness'; diff --git a/hooks/__tests__/useDetectOS.test.tsx b/hooks/__tests__/useDetectOS.test.tsx new file mode 100644 index 0000000000000..cab6c1c0f4928 --- /dev/null +++ b/hooks/__tests__/useDetectOS.test.tsx @@ -0,0 +1,76 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { useDetectOS } from '../useDetectOS'; + +const windowsUserAgent = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'; + +const macUserAgent = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'; + +const originalNavigator = global.navigator; + +describe('useDetectOS', () => { + afterEach(() => { + Object.defineProperty(global, 'navigator', { + value: originalNavigator, + writable: true, + }); + }); + + it('should detect WIN OS and 64 bitness', async () => { + Object.defineProperty(global, 'navigator', { + value: { + userAgent: windowsUserAgent, + userAgentData: { + getHighEntropyValues: jest.fn().mockResolvedValue({ bitness: 64 }), + }, + }, + writable: true, + }); + + const { result } = renderHook(() => useDetectOS()); + + await waitFor(() => { + expect(result.current).toStrictEqual({ + os: 'WIN', + bitness: 64, + }); + }); + }); + + it('should detect WIN OS and 64 bitness from user agent', async () => { + Object.defineProperty(global, 'navigator', { + value: { + userAgent: windowsUserAgent, + }, + writable: true, + }); + + const { result } = renderHook(() => useDetectOS()); + + await waitFor(() => { + expect(result.current).toStrictEqual({ + os: 'WIN', + bitness: 64, + }); + }); + }); + + it('should detect MAC OS and default bitness', async () => { + Object.defineProperty(global, 'navigator', { + value: { + userAgent: macUserAgent, + }, + writable: true, + }); + + const { result } = renderHook(() => useDetectOS()); + + await waitFor(() => { + expect(result.current).toStrictEqual({ + os: 'MAC', + bitness: 86, + }); + }); + }); +}); diff --git a/hooks/__tests__/useDownloadLink.test.ts b/hooks/__tests__/useDownloadLink.test.ts deleted file mode 100644 index 94b157111170c..0000000000000 --- a/hooks/__tests__/useDownloadLink.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { renderHook, waitFor } from '@testing-library/react'; -import { useDownloadLink } from '../useDownloadLink'; - -const mockNavigator = { - userAgent: - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36', - userAgentData: { - getHighEntropyValues: jest.fn().mockResolvedValue({ bitness: '64' }), - }, -}; - -const originalNavigator = global.navigator; - -describe('useDownloadLink', () => { - afterEach(() => { - // Reset the navigator global to the original value - Object.defineProperty(global, 'navigator', { - value: originalNavigator, - writable: true, - }); - }); - - it('should detect the user OS and bitness', async () => { - Object.defineProperty(global, 'navigator', { - value: mockNavigator, - // Allow us to change the value of navigator for the other tests - writable: true, - }); - - const { result } = renderHook(() => - useDownloadLink({ version: 'v18.16.0' }) - ); - - await waitFor(() => { - expect(result.current).toBe( - 'https://nodejs.org/dist/v18.16.0/node-v18.16.0-x64.msi' - ); - }); - - expect( - mockNavigator.userAgentData.getHighEntropyValues - ).toHaveBeenCalledWith(['bitness']); - }); - - it('should return the default url if global.navigator does not exist', async () => { - Object.defineProperty(global, 'navigator', { - value: undefined, - // Allow us to change the value of navigator for the other tests - writable: true, - }); - - const { result } = renderHook(() => - useDownloadLink({ version: 'v20.1.0' }) - ); - - await waitFor(() => { - expect(result.current).toBe( - 'https://nodejs.org/dist/v20.1.0/node-v20.1.0.tar.gz' - ); - }); - }); -}); diff --git a/hooks/useDetectOS.ts b/hooks/useDetectOS.ts new file mode 100644 index 0000000000000..740a14e603e04 --- /dev/null +++ b/hooks/useDetectOS.ts @@ -0,0 +1,34 @@ +import { useEffect, useState } from 'react'; +import { detectOS } from '../util/detectOS'; +import { getBitness } from '../util/getBitness'; +import type { UserOS } from '../types/userOS'; + +type UserOSState = { + os: UserOS; + bitness: number; +}; + +export const useDetectOS = () => { + const [userOSState, setUserOSState] = useState({ + os: 'OTHER', + bitness: 86, + }); + + useEffect(() => { + getBitness().then(bitness => { + const userAgent = navigator?.userAgent; + + setUserOSState({ + os: detectOS(), + bitness: + bitness === '64' || + userAgent?.includes('WOW64') || + userAgent?.includes('Win64') + ? 64 + : 86, + }); + }); + }, []); + + return userOSState; +}; diff --git a/hooks/useDownloadLink.ts b/hooks/useDownloadLink.ts deleted file mode 100644 index c29bddba3232e..0000000000000 --- a/hooks/useDownloadLink.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useMemo } from 'react'; -import useSWR from 'swr'; -import { detectOS } from '../util/detectOS'; -import { downloadUrlByOS } from '../util/downloadUrlByOS'; - -import { getBitness } from '../util/getBitness'; -import { UserAgentBitness } from '../constants/swr'; - -type UseDownloadLinkArgs = { - version: string; -}; - -export const useDownloadLink = ({ version }: UseDownloadLinkArgs) => { - const { data: bitness } = useSWR(UserAgentBitness, getBitness); - - const downloadLink = useMemo( - () => - downloadUrlByOS({ - userAgent: navigator?.userAgent, - userOS: detectOS(), - version, - bitness, - }), - [bitness, version] - ); - - return downloadLink; -}; diff --git a/hooks/useFetchNodeReleases.ts b/hooks/useFetchNodeReleases.ts new file mode 100644 index 0000000000000..b7d484125288b --- /dev/null +++ b/hooks/useFetchNodeReleases.ts @@ -0,0 +1,60 @@ +import { useMemo } from 'react'; +import useSWR from 'swr'; +import { useRouter } from './useRouter'; +import { getNodeReleaseStatus } from '../util/nodeRelease'; +import type { NodeRelease } from '../types'; + +interface NodeReleaseJSON { + major: number; + version: string; + codename?: string; + currentStart: string; + ltsStart?: string; + maintenanceStart?: string; + endOfLife: string; + npm?: string; + v8?: string; + releaseDate?: string; + modules?: string; +} + +const fetcher = (...args: Parameters) => + fetch(...args).then(res => res.json()); + +export const useFetchNodeReleases = (): NodeRelease[] => { + const { basePath } = useRouter(); + + const { data = [] } = useSWR( + `${basePath}/node-releases-data.json`, + fetcher + ); + + return useMemo(() => { + const now = new Date(); + + return data.map(raw => { + const support = { + currentStart: raw.currentStart, + ltsStart: raw.ltsStart, + maintenanceStart: raw.maintenanceStart, + endOfLife: raw.endOfLife, + }; + + const status = getNodeReleaseStatus(now, support); + + return { + ...support, + major: raw.major, + version: raw.version, + versionWithPrefix: `v${raw.version}`, + codename: raw.codename || '', + isLts: status === 'Active LTS' || status === 'Maintenance LTS', + status: status, + npm: raw.npm || '', + v8: raw.v8 || '', + releaseDate: raw.releaseDate || '', + modules: raw.modules || '', + }; + }); + }, [data]); +}; diff --git a/hooks/useNodeData.ts b/hooks/useNodeData.ts deleted file mode 100644 index 01c45ba8ae0e1..0000000000000 --- a/hooks/useNodeData.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useContext } from 'react'; -import { NodeDataContext } from '../providers/nodeDataProvider'; -import type { NodeVersionData } from '../types'; - -type UseNodeDataReturnType = { - currentNodeVersion?: NodeVersionData; - currentLtsVersion?: NodeVersionData; -}; - -export const useNodeData = (): UseNodeDataReturnType => { - const [currentNodeVersion, currentLtsVersion] = useContext(NodeDataContext); - - return { - currentLtsVersion: currentLtsVersion || currentNodeVersion, - currentNodeVersion, - }; -}; diff --git a/hooks/useNodeReleases.ts b/hooks/useNodeReleases.ts new file mode 100644 index 0000000000000..24e13138da131 --- /dev/null +++ b/hooks/useNodeReleases.ts @@ -0,0 +1,15 @@ +import { useCallback, useContext } from 'react'; +import { NodeReleasesContext } from '../providers/nodeReleasesProvider'; +import type { NodeReleaseStatus } from '../types'; + +export const useNodeReleases = () => { + const releases = useContext(NodeReleasesContext); + + const getReleaseByStatus = useCallback( + (status: NodeReleaseStatus) => + releases.find(release => release.status === status), + [releases] + ); + + return { releases, getReleaseByStatus }; +}; diff --git a/layouts/DocsLayout.tsx b/layouts/DocsLayout.tsx index f9a2b819c8629..b17fc6bd0c183 100644 --- a/layouts/DocsLayout.tsx +++ b/layouts/DocsLayout.tsx @@ -1,20 +1,26 @@ +import { useMemo } from 'react'; import BaseLayout from './BaseLayout'; import SideNavigation from '../components/SideNavigation'; -import { useNodeData } from '../hooks/useNodeData'; +import { useNodeReleases } from '../hooks/useNodeReleases'; import type { FC, PropsWithChildren } from 'react'; const DocsLayout: FC = ({ children }) => { - const { currentLtsVersion, currentNodeVersion } = useNodeData(); + const { getReleaseByStatus } = useNodeReleases(); + + const [lts, current] = useMemo( + () => [getReleaseByStatus('Active LTS'), getReleaseByStatus('Current')], + [getReleaseByStatus] + ); const translationContext = { apiLts: { - ltsNodeVersion: currentLtsVersion?.nodeMajor, - fullLtsNodeVersion: currentLtsVersion?.node, + ltsNodeVersion: lts ? `v${lts.major}.x` : undefined, + fullLtsNodeVersion: lts ? lts.versionWithPrefix : undefined, spanLts: LTS, }, apiCurrent: { - fullCurrentNodeVersion: currentNodeVersion?.node, - currentNodeVersion: currentNodeVersion?.nodeMajor, + fullCurrentNodeVersion: current ? current.versionWithPrefix : undefined, + currentNodeVersion: current ? `v${current.major}.x` : undefined, }, }; diff --git a/layouts/DownloadCurrentLayout.tsx b/layouts/DownloadCurrentLayout.tsx index 2d61f719d8ac8..174015020685d 100644 --- a/layouts/DownloadCurrentLayout.tsx +++ b/layouts/DownloadCurrentLayout.tsx @@ -2,13 +2,12 @@ import BaseLayout from './BaseLayout'; import PrimaryDownloadMatrix from '../components/Downloads/PrimaryDownloadMatrix'; import SecondaryDownloadMatrix from '../components/Downloads/SecondaryDownloadMatrix'; import { useNextraContext } from '../hooks/useNextraContext'; -import { useNodeData } from '../hooks/useNodeData'; +import { WithNodeRelease } from '../providers/withNodeRelease'; import type { FC, PropsWithChildren } from 'react'; -import type { LegacyDownloadsFrontMatter, NodeVersionData } from '../types'; +import type { LegacyDownloadsFrontMatter } from '../types'; const DownloadCurrentLayout: FC = ({ children }) => { const nextraContext = useNextraContext(); - const { currentNodeVersion = {} as NodeVersionData } = useNodeData(); const { downloads } = nextraContext.frontMatter as LegacyDownloadsFrontMatter; @@ -22,8 +21,14 @@ const DownloadCurrentLayout: FC = ({ children }) => { {children} - - + + {({ release }) => ( + <> + + + + )} +
    diff --git a/layouts/DownloadLayout.tsx b/layouts/DownloadLayout.tsx index 361529153b99e..16b910fd3a05a 100644 --- a/layouts/DownloadLayout.tsx +++ b/layouts/DownloadLayout.tsx @@ -2,13 +2,12 @@ import BaseLayout from './BaseLayout'; import PrimaryDownloadMatrix from '../components/Downloads/PrimaryDownloadMatrix'; import SecondaryDownloadMatrix from '../components/Downloads/SecondaryDownloadMatrix'; import { useNextraContext } from '../hooks/useNextraContext'; -import { useNodeData } from '../hooks/useNodeData'; +import { WithNodeRelease } from '../providers/withNodeRelease'; import type { FC, PropsWithChildren } from 'react'; -import type { LegacyDownloadsFrontMatter, NodeVersionData } from '../types'; +import type { LegacyDownloadsFrontMatter } from '../types'; const DownloadLayout: FC = ({ children }) => { const nextraContext = useNextraContext(); - const { currentLtsVersion = {} as NodeVersionData } = useNodeData(); const { downloads } = nextraContext.frontMatter as LegacyDownloadsFrontMatter; @@ -22,8 +21,14 @@ const DownloadLayout: FC = ({ children }) => { {children} - - + + {({ release }) => ( + <> + + + + )} + diff --git a/layouts/DownloadReleasesLayout.tsx b/layouts/DownloadReleasesLayout.tsx index 4dd70f576ceb4..2f7b2406d8d06 100644 --- a/layouts/DownloadReleasesLayout.tsx +++ b/layouts/DownloadReleasesLayout.tsx @@ -1,45 +1,14 @@ import { useMemo } from 'react'; -import useSWR from 'swr'; import { sanitize } from 'isomorphic-dompurify'; -import semVer from 'semver'; import BaseLayout from './BaseLayout'; import { useNextraContext } from '../hooks/useNextraContext'; import DownloadReleasesTable from '../components/Downloads/DownloadReleasesTable'; import type { FC, PropsWithChildren } from 'react'; import type { LegacyDownloadsReleasesFrontMatter } from '../types'; -const fetcher = (...args: Parameters) => - fetch(...args).then(res => res.json()); - const DownloadReleasesLayout: FC = ({ children }) => { const nextraContext = useNextraContext(); - const { data = [] } = useSWR( - 'https://nodejs.org/dist/index.json', - fetcher - ); - - const availableNodeVersions = useMemo(() => { - const majorVersions = new Map(); - - data.reverse().forEach(v => - majorVersions.set(semVer.major(v.version), { - node: v.version, - nodeNumeric: v.version.replace(/^v/, ''), - nodeMajor: `v${semVer.major(v.version)}.x`, - npm: v.npm || 'N/A', - v8: v.v8 || 'N/A', - openssl: v.openssl || 'N/A', - isLts: Boolean(v.lts), - releaseDate: v.date, - ltsName: v.lts || null, - modules: v.modules || '0', - }) - ); - - return [...majorVersions.values()].reverse(); - }, [data]); - const { modules, title } = nextraContext.frontMatter as LegacyDownloadsReleasesFrontMatter; @@ -59,7 +28,7 @@ const DownloadReleasesLayout: FC = ({ children }) => {
    {children}
    - +

    = ({ children }) => { - const { currentLtsVersion, currentNodeVersion } = useNodeData(); +const getDownloadHeadTextOS = (os: UserOS, bitness: number) => { + switch (os) { + case 'MAC': + return ' macOS'; + case 'WIN': + return ` Windows (x${bitness})`; + case 'LINUX': + return ` Linux (x64)`; + case 'OTHER': + return ''; + } +}; +const IndexLayout: FC = ({ children }) => { const { frontMatter: { labels }, } = useNextraContext(); + const { os, bitness } = useDetectOS(); + + const downloadHeadTextPrefix = + os === 'OTHER' ? labels['download'] : labels['download-for']; + const downloadHeadText = `${downloadHeadTextPrefix}${getDownloadHeadTextOS( + os, + bitness + )}`; + return (

    @@ -20,12 +42,15 @@ const IndexLayout: FC = ({ children }) => { -

    - {labels['download']} -

    +

    {downloadHeadText}

    + + + {({ release }) => } + - - + + {({ release }) => } +

    {labels['version-schedule-prompt']}{' '} diff --git a/next.data.mjs b/next.data.mjs index 9f5249553b85e..1bec5e188d479 100644 --- a/next.data.mjs +++ b/next.data.mjs @@ -1,28 +1,24 @@ -import * as preBuild from './scripts/next-data/generatePreBuildFiles.mjs'; +import * as nextData from './scripts/next-data/index.mjs'; -import getNodeVersionData from './scripts/next-data/getNodeVersionData.mjs'; -import getBlogData from './scripts/next-data/getBlogData.mjs'; +const cachedBlogData = nextData.getBlogData(); -const cachedBlogData = getBlogData(); +nextData.generateNodeReleasesJson(); // generates pre-build files for blog year pages (pagination) -preBuild.generateBlogYearPages(cachedBlogData); -preBuild.generateWebsiteFeeds(cachedBlogData); - -const cachedNodeVersionData = getNodeVersionData(); +nextData.generateBlogYearPages(cachedBlogData); +nextData.generateWebsiteFeeds(cachedBlogData); const getNextData = async (content, { route }) => { - const nodeVersionData = await cachedNodeVersionData(route); const blogData = await cachedBlogData(route); - const props = { ...nodeVersionData, ...blogData }; + const staticProps = { ...blogData }; return ` // add the mdx file content ${content} export const getStaticProps = () => { - return { props: ${JSON.stringify(props)} }; + return { props: ${JSON.stringify(staticProps)} }; } `; }; diff --git a/pages/_app.mdx b/pages/_app.mdx index 650df17b99fc5..efad3362355b4 100644 --- a/pages/_app.mdx +++ b/pages/_app.mdx @@ -1,5 +1,5 @@ import { Analytics } from '@vercel/analytics/react'; -import { NodeDataProvider } from '../providers/nodeDataProvider'; +import { NodeReleasesProvider } from '../providers/nodeReleasesProvider'; import { LocaleProvider } from '../providers/localeProvider'; import { sourceSansPro } from '../util/nextFonts'; import BaseApp, { setAppFont } from '../next.app'; @@ -11,10 +11,10 @@ export default function App({ Component, pageProps }) { return ( - + - + ); diff --git a/providers/nodeDataProvider.tsx b/providers/nodeDataProvider.tsx deleted file mode 100644 index 314bc2ef522f3..0000000000000 --- a/providers/nodeDataProvider.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { createContext } from 'react'; -import type { FC, PropsWithChildren } from 'react'; -import type { NodeVersionData } from '../types'; - -type NodeDataProviderProps = PropsWithChildren<{ - nodeVersionData: NodeVersionData[]; -}>; - -export const NodeDataContext = createContext([]); - -export const NodeDataProvider: FC = ({ - children, - nodeVersionData, -}) => ( - - {children} - -); diff --git a/providers/nodeReleasesProvider.tsx b/providers/nodeReleasesProvider.tsx new file mode 100644 index 0000000000000..821c29ed6f84c --- /dev/null +++ b/providers/nodeReleasesProvider.tsx @@ -0,0 +1,16 @@ +import { createContext } from 'react'; +import { useFetchNodeReleases } from '../hooks/useFetchNodeReleases'; +import type { FC, PropsWithChildren } from 'react'; +import type { NodeRelease } from '../types'; + +export const NodeReleasesContext = createContext([]); + +export const NodeReleasesProvider: FC = ({ children }) => { + const releases = useFetchNodeReleases(); + + return ( + + {children} + + ); +}; diff --git a/providers/withNodeRelease.tsx b/providers/withNodeRelease.tsx new file mode 100644 index 0000000000000..11f77497d4a26 --- /dev/null +++ b/providers/withNodeRelease.tsx @@ -0,0 +1,28 @@ +import { useMemo } from 'react'; +import { useNodeReleases } from '../hooks/useNodeReleases'; +import { isNodeRelease } from '../util/nodeRelease'; +import type { FC } from 'react'; +import type { NodeRelease, NodeReleaseStatus } from '../types'; + +type WithNodeReleaseProps = { + status: NodeReleaseStatus; + children: FC<{ release: NodeRelease }>; +}; + +export const WithNodeRelease: FC = ({ + status, + children: Component, +}) => { + const { getReleaseByStatus } = useNodeReleases(); + + const release = useMemo( + () => getReleaseByStatus(status), + [status, getReleaseByStatus] + ); + + if (isNodeRelease(release)) { + return ; + } + + return null; +}; diff --git a/public/static/js/legacyMain.js b/public/static/js/legacyMain.js index 876688fab8022..e7cf36fcfa478 100644 --- a/public/static/js/legacyMain.js +++ b/public/static/js/legacyMain.js @@ -80,79 +80,6 @@ const listenScrollToTopButton = () => { }); }; -const detectEnviromentAndSetDownloadOptions = async () => { - const userAgent = navigator.userAgent; - const userAgentData = navigator.userAgentData; - const osMatch = userAgent.match(/(Win|Mac|Linux)/); - const os = (osMatch && osMatch[1]) || ''; - - // detects the architecture through regular ways on user agents that - // expose the architecture and platform information - // @note this is not available on Windows11 anymore - // @see https://learn.microsoft.com/en-us/microsoft-edge/web-platform/how-to-detect-win11 - let arch = userAgent.match(/x86_64|Win64|WOW64/) ? 'x64' : 'x86'; - - // detects the platform through a legacy property on navigator - // available only on firefox - // @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/oscpu - if (navigator.oscpu && navigator.oscpu.length) { - arch = navigator.oscpu.match(/x86_64|Win64|WOW64/) ? 'x64' : 'x86'; - } - - // detects the platform through a legacy property on navigator - // only available on internet explorer - if (navigator.cpuClass && navigator.cpuClass.length) { - arch = navigator.cpuClass === 'x64' ? 'x64' : 'x86'; - } - - // detects the architecture and other platform data on the navigator - // available only on Chromium-based browsers - // @see https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/getHighEntropyValues - if (userAgentData && userAgentData.getHighEntropyValues) { - // note that getHighEntropyValues returns an object - const platform = await userAgentData.getHighEntropyValues(['bitness']); - - if (platform && platform.bitness) { - // note that platform.bitness returns a string - arch = platform.bitness === '64' ? 'x64' : 'x86'; - } - } - - const buttons = document.querySelectorAll('.home-downloadbutton'); - const downloadHead = document.querySelector('#home-downloadhead'); - - let dlLocal; - - if (downloadHead && buttons) { - dlLocal = downloadHead.getAttribute('data-dl-local'); - - switch (os) { - case 'Mac': - versionIntoHref(buttons, 'node-%version%.pkg'); - downloadHead.textContent = dlLocal + ' macOS'; - break; - case 'Win': - versionIntoHref(buttons, 'node-%version%-' + arch + '.msi'); - downloadHead.textContent = dlLocal + ' Windows (' + arch + ')'; - break; - case 'Linux': - versionIntoHref(buttons, 'node-%version%-linux-x64.tar.xz'); - downloadHead.textContent = dlLocal + ' Linux (x64)'; - break; - } - } - - // Windows button on download page - const winButton = document.querySelector('#windows-downloadbutton'); - - if (winButton && os === 'Win') { - const winText = winButton.querySelector('p'); - - winButton.href = winButton.href.replace(/x(86|64)/, arch); - winText.textContent = winText.textContent.replace(/x(86|64)/, arch); - } -}; - const setCurrentTheme = () => setTheme(getTheme() || (preferredColorScheme.matches ? 'dark' : 'light')); @@ -164,8 +91,6 @@ const startLegacyApp = () => { listenLanguagePickerButton(); listenThemeToggleButton(); listenScrollToTopButton(); - - detectEnviromentAndSetDownloadOptions(); }; setCurrentTheme(); diff --git a/scripts/next-data/generateNodeReleasesJson.mjs b/scripts/next-data/generateNodeReleasesJson.mjs new file mode 100644 index 0000000000000..676477498c28c --- /dev/null +++ b/scripts/next-data/generateNodeReleasesJson.mjs @@ -0,0 +1,51 @@ +import { writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import nodevu from '@nodevu/core'; + +import { getRelativePath } from './_helpers.mjs'; + +const __dirname = getRelativePath(import.meta.url); +const jsonFilePath = join(__dirname, '../../public/node-releases-data.json'); + +export const generateNodeReleasesJson = async () => { + const nodevuOutput = await nodevu(); + + // Filter out those without documented support + // Basically those not in schedule.json + const majors = Object.values(nodevuOutput).filter( + major => major?.support?.phases?.dates?.start + ); + + const nodeReleases = majors.map(major => { + const [latestVersion] = Object.values(major.releases); + + return { + major: latestVersion.semver.major, + version: latestVersion.semver.raw, + codename: major.support.codename, + currentStart: major.support.phases.dates.start, + ltsStart: major.support.phases.dates.lts, + maintenanceStart: major.support.phases.dates.maintenance, + endOfLife: major.support.phases.dates.end, + npm: latestVersion.dependencies.npm, + v8: latestVersion.dependencies.v8, + releaseDate: latestVersion.releaseDate, + modules: latestVersion.modules.version, + }; + }); + + return writeFile( + jsonFilePath, + JSON.stringify( + // nodevu returns duplicated v0.x versions (v0.12, v0.10, ...). + // This behavior seems intentional as the case is hardcoded in nodevu, + // see https://github.com/cutenode/nodevu/blob/0c8538c70195fb7181e0a4d1eeb6a28e8ed95698/core/index.js#L24. + // This line ignores those duplicated versions and takes the latest + // v0.x version (v0.12.18). It is also consistent with the legacy + // nodejs.org implementation. + nodeReleases.filter( + release => release.major !== 0 || release.version === '0.12.18' + ) + ) + ); +}; diff --git a/scripts/next-data/getBlogData.mjs b/scripts/next-data/getBlogData.mjs index 08a551c5d831b..5d4e56e64789a 100644 --- a/scripts/next-data/getBlogData.mjs +++ b/scripts/next-data/getBlogData.mjs @@ -37,7 +37,7 @@ const currentYear = new Date().getFullYear(); // Note.: This current structure is coupled to the current way how we do pagination and categories // This will definitely change over time once we start migrating to the `nodejs/nodejs.dev` codebase -const getBlogData = () => { +export const getBlogData = () => { const blogCategories = getDirectories(blogPath).then(c => c.map(s => [s, readdir(join(blogPath, s))]) ); @@ -125,5 +125,3 @@ const getBlogData = () => { return {}; }; }; - -export default getBlogData; diff --git a/scripts/next-data/getNodeVersionData.mjs b/scripts/next-data/getNodeVersionData.mjs deleted file mode 100644 index ef897ea0970e5..0000000000000 --- a/scripts/next-data/getNodeVersionData.mjs +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -import nodevu from '@nodevu/core'; - -import { getMatchingRoutes } from './_helpers.mjs'; - -const getNodeVersionData = () => { - const nodeVersionData = nodevu().then(majorVersions => { - const impodentVersions = Object.values(majorVersions); - - const latestNodeVersion = impodentVersions.find( - major => major.support && major.support.phases.current === 'start' - ); - - const currentLtsVersion = impodentVersions.find( - major => major.support && major.support.phases.current === 'lts' - ); - - const result = [latestNodeVersion, currentLtsVersion].map(major => { - const minorReleases = Object.entries(major.releases); - - const [[latestVersion, latestMetadata]] = minorReleases; - - return { - node: latestVersion, - nodeNumeric: latestMetadata.semver.raw, - nodeMajor: `${latestMetadata.semver.line}.x`, - npm: latestMetadata.dependencies.npm || 'N/A', - isLts: latestMetadata.lts.isLts, - }; - }); - - return { nodeVersionData: result }; - }); - - return (route = '/') => { - const [, , subDirectory] = route.split('/'); - - if (getMatchingRoutes(subDirectory, ['download', '', 'docs'])) { - // Retuns the cached version of the Node.js versions - // So that we do not calculate this every single time - return nodeVersionData; - } - - return {}; - }; -}; - -export default getNodeVersionData; diff --git a/scripts/next-data/index.mjs b/scripts/next-data/index.mjs new file mode 100644 index 0000000000000..c3e02087715ea --- /dev/null +++ b/scripts/next-data/index.mjs @@ -0,0 +1,3 @@ +export * from './generatePreBuildFiles.mjs'; +export * from './generateNodeReleasesJson.mjs'; +export * from './getBlogData.mjs'; diff --git a/types/index.ts b/types/index.ts index c5444ec268a23..9dd3239306795 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,7 +1,6 @@ import type { AppProps as DefaultAppProps } from 'next/app'; import type { BlogData } from './blog'; -import type { NodeVersionData } from './nodeVersions'; export * from './api'; export * from './blog'; @@ -12,13 +11,11 @@ export * from './frontmatter'; export * from './i18n'; export * from './layouts'; export * from './navigation'; -export * from './nodeVersions'; export * from './prevNextLink'; export * from './releases'; export * from './middlewares'; export interface AppProps { - nodeVersionData: Array; blogData?: BlogData; statusCode?: number; } diff --git a/types/nodeVersions.ts b/types/nodeVersions.ts deleted file mode 100644 index 9182e109ead39..0000000000000 --- a/types/nodeVersions.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface NodeReleaseSchedule { - start: string; - end: string; -} - -export interface NodeVersionData { - node: string; - nodeMajor: string; - nodeNumeric: string; - npm: string; - isLts: boolean; -} - -export interface ExtendedNodeVersionData extends NodeVersionData { - v8: string; - openssl: string; - ltsName: string | null; - releaseDate: string; - modules: string; -} diff --git a/types/releases.ts b/types/releases.ts index b4a5b4313d9f7..7b98efe595143 100644 --- a/types/releases.ts +++ b/types/releases.ts @@ -9,19 +9,31 @@ export interface UpcomingRelease { releases: UpcomingReleaseData[]; } -export interface NodeReleaseData { +export type NodeReleaseStatus = + | 'Maintenance LTS' + | 'Active LTS' + | 'Current' + | 'End-of-life' + | 'Pending'; + +export interface NodeRelease { + major: number; version: string; - fullVersion: string; + versionWithPrefix: string; codename: string; isLts: boolean; - status: - | 'Maintenance LTS' - | 'Active LTS' - | 'Current' - | 'End-of-life' - | 'Pending'; - initialRelease: string; - ltsStart: string | null; - maintenanceStart: string | null; + status: NodeReleaseStatus; + currentStart: string; + ltsStart?: string; + maintenanceStart?: string; endOfLife: string; + npm: string; + v8: string; + releaseDate: string; + modules: string; } + +export type NodeReleaseSupport = Pick< + NodeRelease, + 'currentStart' | 'ltsStart' | 'maintenanceStart' | 'endOfLife' +>; diff --git a/util/__tests__/downloadUrlByOS.test.ts b/util/__tests__/downloadUrlByOS.test.ts index f4e892122bc14..58fea2b425af8 100644 --- a/util/__tests__/downloadUrlByOS.test.ts +++ b/util/__tests__/downloadUrlByOS.test.ts @@ -1,72 +1,39 @@ import { downloadUrlByOS } from '../downloadUrlByOS'; const version = 'v18.16.0'; + describe('downloadUrlByOS', () => { it('returns the correct download URL for Mac', () => { - const userAgent = - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9'; - const userOS = 'MAC'; + const os = 'MAC'; + const bitness = 86; const expectedUrl = 'https://nodejs.org/dist/v18.16.0/node-v18.16.0.pkg'; - expect(downloadUrlByOS({ userAgent, userOS, version })).toBe(expectedUrl); + expect(downloadUrlByOS(version, os, bitness)).toBe(expectedUrl); }); it('returns the correct download URL for Windows (32-bit)', () => { - const userAgent = - 'Mozilla/5.0 (Windows NT 10.0; Win32; x86) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36'; - const userOS = 'WIN'; - const bitness = '32'; + const os = 'WIN'; + const bitness = 86; const expectedUrl = 'https://nodejs.org/dist/v18.16.0/node-v18.16.0-x86.msi'; - expect(downloadUrlByOS({ userAgent, userOS, version, bitness })).toBe( - expectedUrl - ); - }); - - it('returns the correct download URL for Windows (64-bit) because the userAgent contains Win64', () => { - const userAgent = - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246'; - const userOS = 'WIN'; - const bitness = ''; - const expectedUrl = - 'https://nodejs.org/dist/v18.16.0/node-v18.16.0-x64.msi'; - - expect(downloadUrlByOS({ userAgent, userOS, version, bitness })).toBe( - expectedUrl - ); - }); - - it('returns the correct download URL for Windows (64-bit) because the userAgent contains WOW64', () => { - const userAgent = - 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36'; - const userOS = 'WIN'; - const bitness = ''; - const expectedUrl = - 'https://nodejs.org/dist/v18.16.0/node-v18.16.0-x64.msi'; - - expect(downloadUrlByOS({ userAgent, userOS, version, bitness })).toBe( - expectedUrl - ); + expect(downloadUrlByOS(version, os, bitness)).toBe(expectedUrl); }); - it('returns the correct download URL for Windows (64-bit) because bitness = 64', () => { - const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'; - const userOS = 'WIN'; - const bitness = '64'; + it('returns the correct download URL for Windows (64-bit)', () => { + const os = 'WIN'; + const bitness = 64; const expectedUrl = 'https://nodejs.org/dist/v18.16.0/node-v18.16.0-x64.msi'; - expect(downloadUrlByOS({ userAgent, userOS, version, bitness })).toBe( - expectedUrl - ); + expect(downloadUrlByOS(version, os, bitness)).toBe(expectedUrl); }); it('returns the default download URL for other operating systems', () => { - const userAgent = 'Mozilla/5.0 (Linux; Android 11; SM-G975U1)'; - const userOS = 'OTHER'; + const os = 'OTHER'; + const bitness = 86; const expectedUrl = 'https://nodejs.org/dist/v18.16.0/node-v18.16.0.tar.gz'; - expect(downloadUrlByOS({ userAgent, userOS, version })).toBe(expectedUrl); + expect(downloadUrlByOS(version, os, bitness)).toBe(expectedUrl); }); }); diff --git a/util/downloadUrlByOS.ts b/util/downloadUrlByOS.ts index 6491324094931..1fa3e75c04785 100644 --- a/util/downloadUrlByOS.ts +++ b/util/downloadUrlByOS.ts @@ -1,30 +1,18 @@ import type { UserOS } from '../types/userOS'; -type DownloadUrlByOS = { - userAgent?: string | undefined; - userOS: UserOS; - version: string; - bitness?: string; -}; - -export const downloadUrlByOS = ({ - userAgent, - userOS, - version, - bitness, -}: DownloadUrlByOS): string => { - const baseURL = `https://nodejs.org/dist/${version}`; - const is64Bit = - bitness === '64' || - userAgent?.includes('WOW64') || - userAgent?.includes('Win64'); +export const downloadUrlByOS = ( + versionWithPrefix: string, + os: UserOS, + bitness: number +): string => { + const baseURL = `https://nodejs.org/dist/${versionWithPrefix}`; - switch (userOS) { + switch (os) { case 'MAC': - return `${baseURL}/node-${version}.pkg`; + return `${baseURL}/node-${versionWithPrefix}.pkg`; case 'WIN': - return `${baseURL}/node-${version}-x${is64Bit ? 64 : 86}.msi`; + return `${baseURL}/node-${versionWithPrefix}-x${bitness}.msi`; default: - return `${baseURL}/node-${version}.tar.gz`; + return `${baseURL}/node-${versionWithPrefix}.tar.gz`; } }; diff --git a/util/nodeRelease.ts b/util/nodeRelease.ts new file mode 100644 index 0000000000000..bdaeaf1f73f6d --- /dev/null +++ b/util/nodeRelease.ts @@ -0,0 +1,39 @@ +import type { + NodeRelease, + NodeReleaseStatus, + NodeReleaseSupport, +} from '../types/releases'; + +export const isNodeRelease = (release: any): release is NodeRelease => + typeof release === 'object' && release?.version; + +export const getNodeReleaseStatus = ( + now: Date, + support: NodeReleaseSupport +): NodeReleaseStatus => { + if (support.endOfLife) { + if (now > new Date(support.endOfLife)) { + return 'End-of-life'; + } + } + + if (support.maintenanceStart) { + if (now > new Date(support.maintenanceStart)) { + return 'Maintenance LTS'; + } + } + + if (support.ltsStart) { + if (now > new Date(support.ltsStart)) { + return 'Active LTS'; + } + } + + if (support.currentStart) { + if (now >= new Date(support.currentStart)) { + return 'Current'; + } + } + + return 'Pending'; +};