From 7b9e8b1e641d07e68ac3e3c3bfdf27e5f16c3553 Mon Sep 17 00:00:00 2001 From: Shiba <128208841+HinataKah0@users.noreply.github.com> Date: Sat, 17 Jun 2023 16:27:34 +0800 Subject: [PATCH] chore: consolidate Node releases data (#5365) * chore: update fields' name version -> major fullVersion -> version initialRelease -> currentStart * chore: add script to generate Node releases data * chore: add Node releases data provider and hooks * chore: add fields from NodeVersionData * chore: replace NodeVersionData usages * chore: add files to .prettierignore * chore: add Node releases data fixture * refactor: change to inline conditional rendering * chore: don't fallback to current if LTS is undefined * chore: remove redundant entries from fixture * refactor: rename hook to useFetchNodeReleasesData * refactor: don't store if return immediately * refactor: remove redundant context in names * refactor: remove HomeDownloadButtonProps * refactor: simplify useNodeReleasesData hooks * refactor: extract getNodeReleaseStatus to util * chore: add versionWithPrefix to NodeReleaseData * refactor: move fetch hook call to be inside provider * refactor: move out __dirname and path * chore: handle undefined lts and current * refactor: no need to assign variable * refactor: move context type to be inside provider * refactor: introduce NodeReleaseStatus type * refactor: no need to declare empty props * chore: do not block when generating node releases data * refactor: clean up constants.ts * refactor: remove unnecessary 'Data' suffix * chore: update nodevu * chore: remove hack * refactor: make inline * chore: specify default value * refactor: rework how to use useNodeReleases hook * refactor: reorder providers * chore: skip duplicate v0.x * refactor: introduce NodeReleaseSupport type * refactor: remove excessive spaces * refactor: fix imports * chore: no need to ignore _app.mdx * chore: move out of static/ folder * refactor: use prettier syntax * refactor: simplify generateNodeReleasesData * refactor: introduce WithNodeRelease component * chore: fix filter 1. nodevu always returns `major.support.phases.dates.start` unless it doesn't exist in schedule.json. 2. Both dependencies and modules objects are also guaranteed to be returned by nodevu. * refactor: introduce NodeReleaseJSON type * refactor: move getReleaseByStatus function to useNodeReleases hook * refactor: add empty line * refactor: rename generateNodeReleasesData * refactor: add next-data/index.mjs * chore: change to use useRouter hook * chore: update nodeReleases fixture * refactor: rename props to staticProps * refactor: move WithNodeRelease to providers * fix: change to use useDownloadLink hook The missing filename is due to race condition. In nodejs.org, the link was rendered in Server Side. So when that function is invoked, the query selector can detect it and append the filename. But this is not the case since we are fetching the Node releases data in Client Side now. The query selector will be executed first before Node releases data is fetched. * refactor: deprecate legacyMain's detect OS * refactor: rework useDetectOS hook * refactor: rework downloadUrlByOS util function * refactor: remove unused constants * refactor: change file name * refactor: use inline * refactor: rework useDetectOS hook * chore: delete WithNodeRelease file It will be added back later * chore: add back withNodeRelease file --------- Co-authored-by: Wai.Tung Co-authored-by: Claudio Wunder --- .gitignore | 1 + .prettierignore | 1 + .storybook/constants.ts | 20 ---- .storybook/preview.tsx | 7 +- __fixtures__/nodeReleases.tsx | 97 +++++++++++++++++++ components/Downloads/DownloadList.tsx | 10 +- .../Downloads/DownloadReleasesTable.tsx | 92 +++++++++--------- .../Downloads/PrimaryDownloadMatrix.tsx | 83 +++++++++------- .../Downloads/SecondaryDownloadMatrix.tsx | 20 ++-- components/Home/HomeDownloadButton.tsx | 32 +++--- constants/swr.ts | 1 - hooks/__tests__/useDetectOS.test.tsx | 76 +++++++++++++++ hooks/__tests__/useDownloadLink.test.ts | 62 ------------ hooks/useDetectOS.ts | 34 +++++++ hooks/useDownloadLink.ts | 28 ------ hooks/useFetchNodeReleases.ts | 60 ++++++++++++ hooks/useNodeData.ts | 17 ---- hooks/useNodeReleases.ts | 15 +++ layouts/DocsLayout.tsx | 18 ++-- layouts/DownloadCurrentLayout.tsx | 15 ++- layouts/DownloadLayout.tsx | 15 ++- layouts/DownloadReleasesLayout.tsx | 33 +------ layouts/IndexLayout.tsx | 41 ++++++-- next.data.mjs | 18 ++-- pages/_app.mdx | 6 +- providers/nodeDataProvider.tsx | 18 ---- providers/nodeReleasesProvider.tsx | 16 +++ providers/withNodeRelease.tsx | 28 ++++++ public/static/js/legacyMain.js | 75 -------------- .../next-data/generateNodeReleasesJson.mjs | 51 ++++++++++ scripts/next-data/getBlogData.mjs | 4 +- scripts/next-data/getNodeVersionData.mjs | 49 ---------- scripts/next-data/index.mjs | 3 + types/index.ts | 3 - types/nodeVersions.ts | 20 ---- types/releases.ts | 34 ++++--- util/__tests__/downloadUrlByOS.test.ts | 61 +++--------- util/downloadUrlByOS.ts | 32 ++---- util/nodeRelease.ts | 39 ++++++++ 39 files changed, 674 insertions(+), 561 deletions(-) delete mode 100644 .storybook/constants.ts create mode 100644 __fixtures__/nodeReleases.tsx delete mode 100644 constants/swr.ts create mode 100644 hooks/__tests__/useDetectOS.test.tsx delete mode 100644 hooks/__tests__/useDownloadLink.test.ts create mode 100644 hooks/useDetectOS.ts delete mode 100644 hooks/useDownloadLink.ts create mode 100644 hooks/useFetchNodeReleases.ts delete mode 100644 hooks/useNodeData.ts create mode 100644 hooks/useNodeReleases.ts delete mode 100644 providers/nodeDataProvider.tsx create mode 100644 providers/nodeReleasesProvider.tsx create mode 100644 providers/withNodeRelease.tsx create mode 100644 scripts/next-data/generateNodeReleasesJson.mjs delete mode 100644 scripts/next-data/getNodeVersionData.mjs create mode 100644 scripts/next-data/index.mjs delete mode 100644 types/nodeVersions.ts create mode 100644 util/nodeRelease.ts 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'; +};