diff --git a/apps/site/components/Downloads/DownloadButton/index.module.css b/apps/site/components/Downloads/DownloadButton/index.module.css deleted file mode 100644 index 7cc12d7f700e3..0000000000000 --- a/apps/site/components/Downloads/DownloadButton/index.module.css +++ /dev/null @@ -1,19 +0,0 @@ -@reference "../../../styles/index.css"; - -.downloadButton { - @apply justify-center; - - &.primary { - @apply inline-flex - dark:hidden; - } - - &.special { - @apply hidden - dark:inline-flex; - } - - svg { - @apply dark:opacity-50; - } -} diff --git a/apps/site/components/Downloads/DownloadButton/index.tsx b/apps/site/components/Downloads/DownloadButton/index.tsx deleted file mode 100644 index d4f3e7a76dd4c..0000000000000 --- a/apps/site/components/Downloads/DownloadButton/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -'use client'; - -import { CloudArrowDownIcon } from '@heroicons/react/24/outline'; -import classNames from 'classnames'; -import type { FC, PropsWithChildren } from 'react'; - -import Button from '#site/components/Common/Button'; -import { useClientContext } from '#site/hooks'; -import type { NodeRelease } from '#site/types'; -import { getNodeDownloadUrl } from '#site/util/url'; -import { getUserPlatform } from '#site/util/userAgent'; - -import styles from './index.module.css'; - -type DownloadButtonProps = { release: NodeRelease }; - -const DownloadButton: FC> = ({ - release: { versionWithPrefix }, - children, -}) => { - const { os, bitness, architecture } = useClientContext(); - - const platform = getUserPlatform(architecture, bitness); - const downloadLink = getNodeDownloadUrl(versionWithPrefix, os, platform); - - return ( - <> - - - - - ); -}; - -export default DownloadButton; diff --git a/apps/site/components/Downloads/DownloadLink.tsx b/apps/site/components/Downloads/DownloadLink.tsx index 905adbf352d8a..bdcc68e31b9d2 100644 --- a/apps/site/components/Downloads/DownloadLink.tsx +++ b/apps/site/components/Downloads/DownloadLink.tsx @@ -19,12 +19,12 @@ const DownloadLink: FC> = ({ const platform = getUserPlatform(architecture, bitness); - const downloadLink = getNodeDownloadUrl( - versionWithPrefix, - os, - platform, - kind - ); + const downloadLink = getNodeDownloadUrl({ + version: versionWithPrefix, + os: os, + platform: platform, + kind: kind, + }); return {children}; }; diff --git a/apps/site/components/Downloads/DownloadReleasesTable/index.tsx b/apps/site/components/Downloads/DownloadReleasesTable/index.tsx index 462221477b8e3..ced375c1ac29a 100644 --- a/apps/site/components/Downloads/DownloadReleasesTable/index.tsx +++ b/apps/site/components/Downloads/DownloadReleasesTable/index.tsx @@ -4,6 +4,7 @@ import type { FC } from 'react'; import FormattedTime from '#site/components/Common/FormattedTime'; import DetailsButton from '#site/components/Downloads/DownloadReleasesTable/DetailsButton'; +import Link from '#site/components/Link'; import getReleaseData from '#site/next-data/releaseData'; const BADGE_KIND_MAP = { @@ -42,7 +43,11 @@ const DownloadReleasesTable: FC = async () => { {releaseData.map(release => ( - v{release.major} + + + v{release.major} + + {release.codename || '-'} diff --git a/apps/site/components/Downloads/DownloadsTable/index.tsx b/apps/site/components/Downloads/DownloadsTable/index.tsx new file mode 100644 index 0000000000000..fc5c9b97b4fd8 --- /dev/null +++ b/apps/site/components/Downloads/DownloadsTable/index.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useTranslations } from 'next-intl'; +import type { FC } from 'react'; + +import Link from '#site/components/Link'; +import { OperatingSystemLabel } from '#site/util/download'; +import type { NodeDownloadArtifact } from '#site/util/download/archive'; + +type DownloadsTableProps = { + source: Array; +}; + +const DownloadsTable: FC = ({ source }) => { + const t = useTranslations(); + + return ( + + + + + + + + + + {source.map(release => ( + + + + + + ))} + +
{t('components.downloadsTable.fileName')} + {t('components.downloadsTable.operatingSystem')} + + {t('components.downloadsTable.architecture')} +
+ {release.file} + + {OperatingSystemLabel[release.os]} + + {release.architecture} +
+ ); +}; + +export default DownloadsTable; diff --git a/apps/site/components/Downloads/MinorReleasesTable/index.module.css b/apps/site/components/Downloads/MinorReleasesTable/index.module.css index 5740edadd7de7..645f033173934 100644 --- a/apps/site/components/Downloads/MinorReleasesTable/index.module.css +++ b/apps/site/components/Downloads/MinorReleasesTable/index.module.css @@ -1,8 +1,29 @@ @reference "../../../styles/index.css"; -.links { +.additionalLinks { @apply flex h-4 items-center gap-2; } + +.items { + @apply flex + h-9 + gap-2; +} + +.scrollable { + @apply scrollbar-thin + flex + max-h-[29rem] + overflow-y-auto; +} + +.information { + @apply md:w-96; +} + +.links { + @apply md:w-44; +} diff --git a/apps/site/components/Downloads/MinorReleasesTable/index.tsx b/apps/site/components/Downloads/MinorReleasesTable/index.tsx index 641af7fec0cb8..aad76ab83b417 100644 --- a/apps/site/components/Downloads/MinorReleasesTable/index.tsx +++ b/apps/site/components/Downloads/MinorReleasesTable/index.tsx @@ -1,10 +1,14 @@ 'use client'; +import { CodeBracketSquareIcon } from '@heroicons/react/24/outline'; import Separator from '@node-core/ui-components/Common/Separator'; +import NpmIcon from '@node-core/ui-components/Icons/PackageManager/Npm'; import { useTranslations } from 'next-intl'; import type { FC } from 'react'; +import { ReleaseOverviewItem } from '#site/components/Downloads/ReleaseOverview'; import Link from '#site/components/Link'; +import LinkWithArrow from '#site/components/LinkWithArrow'; import { BASE_CHANGELOG_URL } from '#site/next.constants.mjs'; import type { MinorVersion } from '#site/types'; import { getNodeApiUrl } from '#site/util/url'; @@ -18,47 +22,78 @@ type MinorReleasesTableProps = { export const MinorReleasesTable: FC = ({ releases, }) => { - const t = useTranslations('components.minorReleasesTable'); + const t = useTranslations('components'); return ( - - - - - - - - - {releases.map(release => ( - - - +
+
{t('version')}{t('links')}
v{release.version} -
- - {t('actions.release')} - - - - {t('actions.changelog')} - - - - {t('actions.docs')} - -
-
+ + + + + - ))} - -
{t('minorReleasesTable.version')} + {t('minorReleasesTable.information')} + {t('minorReleasesTable.links')}
+ + + {releases.map(release => ( + + + + v{release.version} + + + +
+ {release.modules && ( + <> + + + + )} + {release.npm && ( + <> + + + + )} + +
+ + +
+ + {t('minorReleasesTable.actions.docs')} + + + + {t('minorReleasesTable.actions.changelog')} + +
+ + + ))} + + + ); }; diff --git a/apps/site/components/Downloads/Release/PrebuiltDownloadButtons.tsx b/apps/site/components/Downloads/Release/PrebuiltDownloadButtons.tsx index fccff0462d69f..92f6fb2615a69 100644 --- a/apps/site/components/Downloads/Release/PrebuiltDownloadButtons.tsx +++ b/apps/site/components/Downloads/Release/PrebuiltDownloadButtons.tsx @@ -22,11 +22,21 @@ const PrebuiltDownloadButtons: FC = () => { const { release, os, platform } = useContext(ReleaseContext); const installerUrl = platform - ? getNodeDownloadUrl(release.versionWithPrefix, os, platform, 'installer') + ? getNodeDownloadUrl({ + version: release.versionWithPrefix, + os: os, + platform: platform, + kind: 'installer', + }) : ''; const binaryUrl = platform - ? getNodeDownloadUrl(release.versionWithPrefix, os, platform, 'binary') + ? getNodeDownloadUrl({ + version: release.versionWithPrefix, + os: os, + platform: platform, + kind: 'binary', + }) : ''; return ( diff --git a/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx b/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx index 54ef178b43672..a639b82f03022 100644 --- a/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx +++ b/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx @@ -10,6 +10,7 @@ import { useContext, useMemo } from 'react'; import CodeBox from '#site/components/Common/CodeBox'; import Link from '#site/components/Link'; import LinkWithArrow from '#site/components/LinkWithArrow'; +import WithReleaseAlertBox from '#site/components/withReleaseAlertBox'; import { createSval } from '#site/next.jsx.compiler.mjs'; import { ReleaseContext, @@ -107,7 +108,7 @@ const ReleaseCodeBox: FC = () => { > {t.rich('layouts.download.codeBox.noScriptDetected', { link: text => ( - + {text} ), @@ -115,17 +116,7 @@ const ReleaseCodeBox: FC = () => { - {release.status === 'End-of-life' && ( - - {t.rich('layouts.download.codeBox.unsupportedVersionWarning', { - link: text => {text}, - })} - - )} + {release.status === 'LTS' && ( = ({ : 'components.releaseModal.titleWithoutCodename'; const modalHeading = t(modalHeadingKey, { - version: release.major, + version: `v${release.major}`, codename: release.codename ?? '', }); return ( - {release.status === 'End-of-life' && ( -
- - {t.rich('components.releaseModal.unsupportedVersionWarning', { - link: text => ( - - {text} - - ), - })} - -
- )} - - {release.status === 'LTS' && ( -
- - {t.rich('components.releaseModal.ltsVersionFeaturesNotice', { - link: text => {text}, - })} - -
- )} + {modalHeading} diff --git a/apps/site/components/Downloads/ReleaseOverview/index.module.css b/apps/site/components/Downloads/ReleaseOverview/index.module.css index 5043967ac27f1..da1541398edbc 100644 --- a/apps/site/components/Downloads/ReleaseOverview/index.module.css +++ b/apps/site/components/Downloads/ReleaseOverview/index.module.css @@ -15,24 +15,24 @@ gap-4 lg:grid-cols-3; } +} - .item { - @apply flex - items-center - gap-2; +.item { + @apply flex + items-center + gap-2; - h1 { - @apply text-sm - font-semibold; - } + dt { + @apply text-sm + font-semibold; + } - h2 { - @apply text-xs - font-normal; - } + dd { + @apply text-xs + font-normal; + } - svg { - @apply size-4; - } + svg { + @apply size-4; } } diff --git a/apps/site/components/Downloads/ReleaseOverview/index.tsx b/apps/site/components/Downloads/ReleaseOverview/index.tsx index a29118a8e400f..af38c606d5cfc 100644 --- a/apps/site/components/Downloads/ReleaseOverview/index.tsx +++ b/apps/site/components/Downloads/ReleaseOverview/index.tsx @@ -13,23 +13,25 @@ import type { NodeRelease } from '#site/types'; import styles from './index.module.css'; -type ItemProps = { +type ReleaseOverviewItemProps = { Icon: FC>; title: ReactNode; subtitle: ReactNode; }; -const Item: FC = ({ Icon, title, subtitle }) => { - return ( -
- -
-

{subtitle}

-

{title}

-
-
- ); -}; +export const ReleaseOverviewItem: FC = ({ + Icon, + title, + subtitle, +}) => ( +
+ +
+
{subtitle}
+
{title}
+
+
+); type ReleaseOverviewProps = { release: NodeRelease; @@ -41,36 +43,36 @@ export const ReleaseOverview: FC = ({ release }) => { return (
- } subtitle={t('components.releaseOverview.firstReleased')} /> - } subtitle={t('components.releaseOverview.lastUpdated')} /> - {release.modules && ( - )} {release.npm && ( - )} - ; + +type Navigation = { + label: string; + href: string; +}; + +type WithDownloadArchiveProps = { + children: FC }>; +}; + +/** + * Higher-order component that extracts version from pathname, + * fetches release data, and provides download artifacts to child component + */ +const WithDownloadArchive: FC = async ({ + children: Component, +}) => { + const { pathname } = getClientContext(); + + // Extract version from pathname + const version = extractVersionFromPath(pathname); + + if (version == null) { + return null; + } + + // Find the release data for the given version + const releaseData = await getReleaseData(); + const release = findReleaseByVersion(releaseData, version); + + if (!release) { + return null; + } + + const navigation = getDownloadArchiveNavigation(releaseData); + const releaseArtifacts = buildReleaseArtifacts( + release, + version === 'archive' ? release.versionWithPrefix : version + ); + + return ; +}; + +export default WithDownloadArchive; diff --git a/apps/site/components/withLayout.tsx b/apps/site/components/withLayout.tsx index ec553190e621d..c7f948a5946dc 100644 --- a/apps/site/components/withLayout.tsx +++ b/apps/site/components/withLayout.tsx @@ -5,6 +5,7 @@ import ArticlePageLayout from '#site/layouts/ArticlePage'; import BlogLayout from '#site/layouts/Blog'; import DefaultLayout from '#site/layouts/Default'; import DownloadLayout from '#site/layouts/Download'; +import DownloadArchiveLayout from '#site/layouts/DownloadArchive'; import GlowingBackdropLayout from '#site/layouts/GlowingBackdrop'; import LearnLayout from '#site/layouts/Learn'; import PostLayout from '#site/layouts/Post'; @@ -18,6 +19,7 @@ const layouts = { 'blog-post': PostLayout, 'blog-category': BlogLayout, download: DownloadLayout, + 'download-archive': DownloadArchiveLayout, article: ArticlePageLayout, } satisfies Record; diff --git a/apps/site/components/withMarkdownContent.tsx b/apps/site/components/withMarkdownContent.tsx new file mode 100644 index 0000000000000..9747729c3e3c3 --- /dev/null +++ b/apps/site/components/withMarkdownContent.tsx @@ -0,0 +1,35 @@ +import { getLocale } from 'next-intl/server'; +import type { FC } from 'react'; + +import { dynamicRouter } from '#site/next.dynamic'; + +const getMarkdownContent = async (locale: string, file: Array) => { + const filePathname = dynamicRouter.getPathname(file); + + // Retrieves the Markdown file source content based on the file path and locale + // Uses dynamic routing to locate and load the appropriate markdown file + // for the given locale and file path segments + const { source, filename } = await dynamicRouter.getMarkdownFile( + locale, + filePathname + ); + + // Parses the Markdown/MDX source content and transforms it into a React component + // Handles both standard Markdown and MDX files + const { content } = await dynamicRouter.getMDXContent(source, filename); + + return content; +}; + +type WithMarkdownContentProps = { + file: Array; +}; + +const WithMarkdownContent: FC = async ({ file }) => { + const locale = await getLocale(); + const content = await getMarkdownContent(locale, file); + + return content; +}; + +export default WithMarkdownContent; diff --git a/apps/site/components/withReleaseAlertBox.tsx b/apps/site/components/withReleaseAlertBox.tsx new file mode 100644 index 0000000000000..7cc95db1661a8 --- /dev/null +++ b/apps/site/components/withReleaseAlertBox.tsx @@ -0,0 +1,49 @@ +import AlertBox from '@node-core/ui-components/Common/AlertBox'; +import { useTranslations } from 'next-intl'; +import type { FC } from 'react'; + +import Link from '#site/components/Link'; +import type { NodeReleaseStatus } from '#site/types'; + +type WithReleaseAlertBoxProps = { + status: NodeReleaseStatus; +}; + +const WithReleaseAlertBox: FC = ({ status }) => { + const t = useTranslations(); + + switch (status) { + case 'End-of-life': + return ( + + {t.rich('components.releaseModal.unsupportedVersionWarning', { + link: text => ( + + {text} + + ), + })} + + ); + case 'LTS': + return ( + + {t.rich('components.releaseModal.ltsVersionFeaturesNotice', { + link: text => {text}, + })} + + ); + default: + return null; + } +}; + +export default WithReleaseAlertBox; diff --git a/apps/site/layouts/DownloadArchive.tsx b/apps/site/layouts/DownloadArchive.tsx new file mode 100644 index 0000000000000..7050c2ce4af36 --- /dev/null +++ b/apps/site/layouts/DownloadArchive.tsx @@ -0,0 +1,23 @@ +import type { FC } from 'react'; + +import WithFooter from '#site/components/withFooter'; +import WithMarkdownContent from '#site/components/withMarkdownContent'; +import WithNavBar from '#site/components/withNavBar'; + +import styles from './layouts.module.css'; + +const DownloadArchiveLayout: FC = () => ( + <> + + +
+
+ +
+
+ + + +); + +export default DownloadArchiveLayout; diff --git a/apps/site/next-data/generators/releaseData.mjs b/apps/site/next-data/generators/releaseData.mjs index 03800543b882f..b5143f6338343 100644 --- a/apps/site/next-data/generators/releaseData.mjs +++ b/apps/site/next-data/generators/releaseData.mjs @@ -77,8 +77,12 @@ const generateReleaseData = async () => { const status = getNodeReleaseStatus(new Date(), support); const minorVersions = Object.entries(major.releases).map(([, release]) => ({ - version: release.semver.raw, + modules: release.modules.version || '', + npm: release.dependencies.npm || '', releaseDate: release.releaseDate, + v8: release.dependencies.v8, + version: release.semver.raw, + versionWithPrefix: `v${release.semver.raw}`, })); const majorVersion = latestVersion.semver.major; diff --git a/apps/site/next.dynamic.constants.mjs b/apps/site/next.dynamic.constants.mjs index 294344b09ea15..8c84f1b566081 100644 --- a/apps/site/next.dynamic.constants.mjs +++ b/apps/site/next.dynamic.constants.mjs @@ -1,5 +1,6 @@ 'use strict'; +import provideReleaseData from '#site/next-data/providers/releaseData'; import { blogData } from '#site/next.json.mjs'; import { provideBlogPosts } from './next-data/providers/blogData'; @@ -19,6 +20,8 @@ export const IGNORED_ROUTES = [ locale !== defaultLocale.code && /^blog/.test(pathname), // This is used to ignore all pathnames that are empty ({ locale, pathname }) => locale.length && !pathname.length, + // This is used to ignore download routes for Node.js versions and downloads archive page + ({ pathname }) => /^download\/(v\d+(\.\d+)*|archive)$/.test(pathname), ]; /** @@ -29,6 +32,14 @@ export const IGNORED_ROUTES = [ * @type {Map} A Map of pathname and Layout Name */ export const DYNAMIC_ROUTES = new Map([ + // Creates dynamic routes for downloads archive pages for each version + // (e.g., /download/v18.20.8, /download/v20.19.2) + ...provideReleaseData() + .flatMap(({ minorVersions, versionWithPrefix }) => [ + `download/${versionWithPrefix}`, + ...minorVersions.map(minor => `download/${minor.versionWithPrefix}`), + ]) + .map(version => [version, 'download-archive']), // Provides Routes for all Blog Categories ...blogData.categories.map(c => [`blog/${c}`, 'blog-category']), // Provides Routes for all Blog Categories w/ Pagination diff --git a/apps/site/next.mdx.use.client.mjs b/apps/site/next.mdx.use.client.mjs index b9e0f64d0daa1..e466a1d1a9a3b 100644 --- a/apps/site/next.mdx.use.client.mjs +++ b/apps/site/next.mdx.use.client.mjs @@ -4,7 +4,6 @@ import Blockquote from '@node-core/ui-components/Common/Blockquote'; import MDXCodeTabs from '@node-core/ui-components/MDX/CodeTabs'; import Button from './components/Common/Button'; -import DownloadButton from './components/Downloads/DownloadButton'; import DownloadLink from './components/Downloads/DownloadLink'; import BlogPostLink from './components/Downloads/Release/BlogPostLink'; import ChangelogLink from './components/Downloads/Release/ChangelogLink'; @@ -36,8 +35,6 @@ export const clientMdxComponents = { LinkWithArrow: LinkWithArrow, // Regular links (without arrow) Link: Link, - // Renders a Download Button - DownloadButton: DownloadButton, // Renders a Download Link DownloadLink: DownloadLink, // Group of components that enable you to select versions for Node.js diff --git a/apps/site/next.mdx.use.mjs b/apps/site/next.mdx.use.mjs index 9942ac7f35e3e..e47916b913916 100644 --- a/apps/site/next.mdx.use.mjs +++ b/apps/site/next.mdx.use.mjs @@ -3,10 +3,15 @@ import BadgeGroup from '@node-core/ui-components/Common/BadgeGroup'; import DownloadReleasesTable from './components/Downloads/DownloadReleasesTable'; +import DownloadsTable from './components/Downloads/DownloadsTable'; +import { MinorReleasesTable } from './components/Downloads/MinorReleasesTable'; +import { ReleaseOverview } from './components/Downloads/ReleaseOverview'; import UpcomingMeetings from './components/MDX/Calendar/UpcomingMeetings'; import WithBadgeGroup from './components/withBadgeGroup'; import WithBanner from './components/withBanner'; +import WithDownloadArchive from './components/withDownloadArchive'; import WithNodeRelease from './components/withNodeRelease'; +import WithReleaseAlertBox from './components/withReleaseAlertBox'; /** * A full list of React Components that we want to pass through to MDX @@ -15,14 +20,24 @@ import WithNodeRelease from './components/withNodeRelease'; */ export const mdxComponents = { DownloadReleasesTable, + // HOC for providing the Download Archive Page properties + WithDownloadArchive, + // Renders a table with Node.js Releases with different platforms and architectures + DownloadsTable, // HOC for getting Node.js Release Metadata WithNodeRelease, + // Renders an alert box with the given release status + WithReleaseAlertBox, // HOC for providing Banner Data WithBanner, // HOC for providing Badge Data WithBadgeGroup, // Standalone Badge Group BadgeGroup, + // Renders the Release Overview for a specified version + ReleaseOverview, + // Renders a table with all the Minor Releases for a Major Version + MinorReleasesTable, // Renders an container for Upcoming Node.js Meetings UpcomingMeetings, }; diff --git a/apps/site/pages/en/download/archive.mdx b/apps/site/pages/en/download/archive.mdx new file mode 100644 index 0000000000000..d2ea3805467f4 --- /dev/null +++ b/apps/site/pages/en/download/archive.mdx @@ -0,0 +1,69 @@ +--- +title: Download Node.js® +layout: download-archive +--- + + + {({ binaries, installers, version, release, sources, navigation }) => ( + <> +

Node.js® Download Archive

+ +

+ Node.js Logo + {version} + {release.codename && ` (${release.codename})`} +

+ + + + + +
    + +
  • + Learn more about Node.js releases, including the release schedule and LTS status. +
  • + +
  • + Signed SHASUMS for release files. How to verify signed SHASUMS. +
  • + +
  • + Download a signed Node.js {version} source tarball. +
  • + +
+ +
+ +

Other releases

+
+
    + {navigation.map(({ href, label }) => ( +
  • + +

    {label}

    + +
  • + ))} +
+
+ +

Binary Downloads

+ + +

Installer Packages

+ + +

Minor versions

+ + + +)} + +
diff --git a/apps/site/pages/en/download/current.mdx b/apps/site/pages/en/download/current.mdx index 16e78d6a8e1d7..a3c838e5f70e3 100644 --- a/apps/site/pages/en/download/current.mdx +++ b/apps/site/pages/en/download/current.mdx @@ -30,7 +30,7 @@ Learn how to Node.js source tarball. Check out our nightly binaries or -all previous releases +all previous releases or the unofficial binaries for other platforms. diff --git a/apps/site/pages/en/download/index.mdx b/apps/site/pages/en/download/index.mdx index 16e78d6a8e1d7..a3c838e5f70e3 100644 --- a/apps/site/pages/en/download/index.mdx +++ b/apps/site/pages/en/download/index.mdx @@ -30,7 +30,7 @@ Learn how to Node.js source tarball. Check out our nightly binaries or -all previous releases +all previous releases or the unofficial binaries for other platforms. diff --git a/apps/site/types/download.ts b/apps/site/types/download.ts index 3be983a9466f2..a7f86c049a849 100644 --- a/apps/site/types/download.ts +++ b/apps/site/types/download.ts @@ -4,4 +4,4 @@ export interface DownloadSnippet { content: string; } -export type DownloadKind = 'installer' | 'binary' | 'source'; +export type DownloadKind = 'installer' | 'binary' | 'source' | 'shasum'; diff --git a/apps/site/types/layouts.ts b/apps/site/types/layouts.ts index a3e81f69132f7..ff6ad599eca79 100644 --- a/apps/site/types/layouts.ts +++ b/apps/site/types/layouts.ts @@ -6,4 +6,5 @@ export type Layouts = | 'blog-category' | 'blog-post' | 'download' + | 'download-archive' | 'article'; diff --git a/apps/site/types/releases.ts b/apps/site/types/releases.ts index cf71f7bea406d..ac9732d86d803 100644 --- a/apps/site/types/releases.ts +++ b/apps/site/types/releases.ts @@ -20,8 +20,12 @@ export interface NodeReleaseSource { } export interface MinorVersion { - version: string; + npm?: string; + modules?: string; releaseDate: string; + v8: string; + version: string; + versionWithPrefix: string; } export interface NodeRelease extends NodeReleaseSource { diff --git a/apps/site/util/__tests__/url.test.mjs b/apps/site/util/__tests__/url.test.mjs index 8f6cd5be3f302..4f24164e6b680 100644 --- a/apps/site/util/__tests__/url.test.mjs +++ b/apps/site/util/__tests__/url.test.mjs @@ -56,48 +56,58 @@ describe('getNodeApiUrl', () => { describe('getNodeDownloadUrl', () => { it('should return the correct download URL for Mac', () => { const os = 'MAC'; - const bitness = 86; + const platform = 86; const expectedUrl = 'https://nodejs.org/dist/v18.16.0/node-v18.16.0.pkg'; - assert.equal(getNodeDownloadUrl(version, os, bitness), expectedUrl); + assert.equal(getNodeDownloadUrl({ version, os, platform }), expectedUrl); }); it('should return the correct download URL for Windows (32-bit)', () => { const os = 'WIN'; - const bitness = 86; + const platform = 86; const expectedUrl = 'https://nodejs.org/dist/v18.16.0/node-v18.16.0-x86.msi'; - assert.equal(getNodeDownloadUrl(version, os, bitness), expectedUrl); + assert.equal(getNodeDownloadUrl({ version, os, platform }), expectedUrl); }); it('should return the correct download URL for Windows (64-bit)', () => { const os = 'WIN'; - const bitness = 64; + const platform = 64; const expectedUrl = 'https://nodejs.org/dist/v18.16.0/node-v18.16.0-x64.msi'; - assert.equal(getNodeDownloadUrl(version, os, bitness), expectedUrl); + assert.equal(getNodeDownloadUrl({ version, os, platform }), expectedUrl); }); it('should return the default download URL for other operating systems', () => { const os = 'OTHER'; - const bitness = 86; + const platform = 86; const expectedUrl = 'https://nodejs.org/dist/v18.16.0/node-v18.16.0.tar.gz'; - assert.equal(getNodeDownloadUrl(version, os, bitness), expectedUrl); + assert.equal(getNodeDownloadUrl({ version, os, platform }), expectedUrl); }); describe('MAC', () => { it('should return .pkg link for installer', () => { - const url = getNodeDownloadUrl('v18.0.0', 'MAC', 'x64', 'installer'); + const url = getNodeDownloadUrl({ + version: 'v18.0.0', + os: 'MAC', + platform: 'x64', + kind: 'installer', + }); assert.ok(url.includes('.pkg')); }); }); describe('WIN', () => { it('should return an MSI link for installer', () => { - const url = getNodeDownloadUrl('v18.0.0', 'WIN', 'x64', 'installer'); + const url = getNodeDownloadUrl({ + version: 'v18.0.0', + os: 'WIN', + platform: 'x64', + kind: 'installer', + }); assert.ok(url.includes('.msi')); }); }); diff --git a/apps/site/util/download/archive.tsx b/apps/site/util/download/archive.tsx new file mode 100644 index 0000000000000..fa066e42773af --- /dev/null +++ b/apps/site/util/download/archive.tsx @@ -0,0 +1,173 @@ +import semVer from 'semver'; + +import type { DownloadKind, OperatingSystem, Platform } from '#site/types'; +import type { NodeRelease } from '#site/types/releases'; +import type { DownloadDropdownItem } from '#site/util/download'; +import { OS_NOT_SUPPORTING_INSTALLERS, PLATFORMS } from '#site/util/download'; +import { getNodeDownloadUrl } from '#site/util/url'; + +import { DIST_URL } from '#site/next.constants'; + +export type NodeDownloadArtifact = { + file: string; + kind: DownloadKind; + os: OperatingSystem; + architecture: string; + url: string; + version: string; +}; + +/** + * Checks if a download item is compatible with the given OS, platform, and version. + */ +function isCompatible( + compatibility: DownloadDropdownItem['compatibility'], + os: OperatingSystem, + platform: Platform, + version: string +): boolean { + const { + os: osList, + platform: platformList, + semver: versions, + } = compatibility; + + return ( + (osList?.includes(os) ?? true) && + (platformList?.includes(platform) ?? true) && + (versions?.every(r => semVer.satisfies(version, r)) ?? true) + ); +} + +type CompatibleArtifactOptions = { + platforms?: Record>>; + exclude?: Array; + version: string; + kind?: DownloadKind; +}; + +/** + * Returns a list of compatible artifacts for the given options. + */ +const getCompatibleArtifacts = ({ + platforms = PLATFORMS, + exclude = [], + version, + kind = 'binary', +}: CompatibleArtifactOptions): Array => { + return Object.entries(platforms).flatMap(([os, items]) => { + if (exclude.includes(os)) return []; + + const operatingSystem = os as OperatingSystem; + + return items + .filter(({ compatibility, value }) => + isCompatible(compatibility, operatingSystem, value, version) + ) + .map(({ value, label }) => { + const url = getNodeDownloadUrl({ + version: version, + os: operatingSystem, + platform: value, + kind: kind, + }); + + return { + file: url.replace(`${DIST_URL}${version}/`, ''), + kind: kind, + os: operatingSystem, + architecture: label, + url: url, + version: version, + }; + }); + }); +}; + +/** + * Generates the navigation links for the Node.js download archive + * It creates a list of links for each major release, formatted with the major + * version and codename if available. + */ +export const getDownloadArchiveNavigation = (releases: Array) => + releases.map(({ major, codename, versionWithPrefix }) => ({ + label: `Node.js v${major} ${codename ? `(${codename})` : ''}`, + href: `/download/${versionWithPrefix}`, + })); + +/** + * Builds the release artifacts for a given Node.js release and version. + * It retrieves binaries, installers, and source files based on the version. + */ +export const buildReleaseArtifacts = ( + release: NodeRelease, + version: string +) => { + const minorVersion = release.minorVersions.find( + ({ versionWithPrefix }) => versionWithPrefix === version + ); + + const enrichedRelease = { + ...release, + ...minorVersion, + }; + + return { + binaries: getCompatibleArtifacts({ + version: version, + kind: 'binary', + }), + installers: getCompatibleArtifacts({ + exclude: OS_NOT_SUPPORTING_INSTALLERS, + version: version, + kind: 'installer', + }), + sources: { + shasum: getNodeDownloadUrl({ + version: version, + kind: 'shasum', + }), + tarball: getNodeDownloadUrl({ + version: version, + kind: 'source', + }), + }, + version: version, + release: enrichedRelease, + }; +}; + +/** + * Extracts the version from the pathname. + * It expects the version to be in the format 'v22.0.4' or 'archive'. + */ +export const extractVersionFromPath = (pathname: string | undefined) => { + if (!pathname) { + return null; + } + + const segments = pathname.split('/').filter(Boolean); + const version = segments.pop(); + + // Check version format like (v22.0.4 or 'archive') + if (!version || !version.match(/^v\d+(\.\d+)*|archive$/)) { + return null; + } + + return version; +}; + +/** + * Finds the appropriate release based on version, if 'archive' is passed, + * it returns the latest LTS release. + */ +export const findReleaseByVersion = ( + releaseData: Array, + version: string | 'archive' +) => { + if (version === 'archive') { + return releaseData.find(release => release.status === 'LTS'); + } + + return releaseData.find(release => semVer.major(version) === release.major); +}; diff --git a/apps/site/util/download/constants.json b/apps/site/util/download/constants.json index a04695ee3dd40..fc6d5d45b80b3 100644 --- a/apps/site/util/download/constants.json +++ b/apps/site/util/download/constants.json @@ -8,7 +8,10 @@ "platforms": [ { "label": "x64", - "value": "x64" + "value": "x64", + "compatibility": { + "semver": [">= 4.0.0"] + } }, { "label": "x86", @@ -40,7 +43,7 @@ "label": "ARM64", "value": "arm64", "compatibility": { - "semver": [">= 19.9.0"] + "semver": [">= 16.0.0"] } } ] diff --git a/apps/site/util/download/index.tsx b/apps/site/util/download/index.tsx index 9b31281e4c1f8..0f0579e49c8c9 100644 --- a/apps/site/util/download/index.tsx +++ b/apps/site/util/download/index.tsx @@ -36,7 +36,7 @@ type DownloadCompatibility = { releases?: Array; }; -type DownloadDropdownItem = { +export type DownloadDropdownItem = { label: IntlMessageKeys; recommended?: boolean; url?: string; diff --git a/apps/site/util/url.ts b/apps/site/util/url.ts index 9fde24b05294e..0bf3c48822a73 100644 --- a/apps/site/util/url.ts +++ b/apps/site/util/url.ts @@ -17,73 +17,98 @@ export const getNodeApiUrl = (version: string) => { : `${DIST_URL}${version}/docs/api/`; }; -export const getNodeDownloadUrl = ( - versionWithPrefix: string, - os: OperatingSystem | 'LOADING', - platform: Platform = 'x64', - kind: DownloadKind = 'installer' -) => { - const baseURL = `${DIST_URL}${versionWithPrefix}`; +type DownloadOptions = { + version: string; + os?: OperatingSystem | 'LOADING'; + platform?: Platform; + kind?: DownloadKind; +}; + +/** + * Generates a Node.js download URL for the given options. + * + * @param options - The download options. + * @param options.version - The Node.js version string, must include the 'v' prefix (e.g., 'v20.12.2'). + * @param options.os - The target operating system. Defaults to 'LOADING'. + * @param options.platform - The target platform/architecture (e.g., 'x64', 'arm64'). Defaults to 'x64'. + * @param options.kind - The type of download artifact. Can be 'installer', 'binary', 'source', or 'shasum'. Defaults to 'installer'. + * @returns The fully qualified URL to the requested Node.js artifact. + * + * @example + * getNodeDownloadUrl({ version: 'v20.12.2', os: 'MAC', platform: 'arm64', kind: 'binary' }); + * // => 'https://nodejs.org/dist/v20.12.2/node-v20.12.2-darwin-arm64.tar.gz' + */ +export const getNodeDownloadUrl = ({ + version, + os = 'LOADING', + platform = 'x64', + kind = 'installer', +}: DownloadOptions) => { + const baseURL = `${DIST_URL}${version}`; if (kind === 'source') { - return `${baseURL}/node-${versionWithPrefix}.tar.gz`; + return `${baseURL}/node-${version}.tar.gz`; + } + + if (kind === 'shasum') { + return `${baseURL}/SHASUMS256.txt.asc`; } switch (os) { case 'MAC': // Prepares a downloadable Node.js installer link for the x64, ARM64 platforms if (kind === 'installer') { - return `${baseURL}/node-${versionWithPrefix}.pkg`; + return `${baseURL}/node-${version}.pkg`; } // Prepares a downloadable Node.js link for the ARM64 platform if (typeof platform === 'string') { - return `${baseURL}/node-${versionWithPrefix}-darwin-${platform}.tar.gz`; + return `${baseURL}/node-${version}-darwin-${platform}.tar.gz`; } // Prepares a downloadable Node.js link for the x64 platform. // Since the x86 platform is not officially supported, returns the x64 // link as the default value. - return `${baseURL}/node-${versionWithPrefix}-darwin-x64.tar.gz`; + return `${baseURL}/node-${version}-darwin-x64.tar.gz`; case 'WIN': { if (kind === 'installer') { // Prepares a downloadable Node.js installer link for the ARM platforms if (typeof platform === 'string') { - return `${baseURL}/node-${versionWithPrefix}-${platform}.msi`; + return `${baseURL}/node-${version}-${platform}.msi`; } // Prepares a downloadable Node.js installer link for the x64 and x86 platforms - return `${baseURL}/node-${versionWithPrefix}-x${platform}.msi`; + return `${baseURL}/node-${version}-x${platform}.msi`; } // Prepares a downloadable Node.js link for the ARM64 platform if (typeof platform === 'string') { - return `${baseURL}/node-${versionWithPrefix}-win-${platform}.zip`; + return `${baseURL}/node-${version}-win-${platform}.zip`; } // Prepares a downloadable Node.js link for the x64 and x86 platforms - return `${baseURL}/node-${versionWithPrefix}-win-x${platform}.zip`; + return `${baseURL}/node-${version}-win-x${platform}.zip`; } case 'LINUX': // Prepares a downloadable Node.js link for the ARM platforms such as // ARMv7 and ARMv8 if (typeof platform === 'string') { - return `${baseURL}/node-${versionWithPrefix}-linux-${platform}.tar.xz`; + return `${baseURL}/node-${version}-linux-${platform}.tar.xz`; } // Prepares a downloadable Node.js link for the x64 platform. // Since the x86 platform is not officially supported, returns the x64 // link as the default value. - return `${baseURL}/node-${versionWithPrefix}-linux-x64.tar.xz`; + return `${baseURL}/node-${version}-linux-x64.tar.xz`; case 'AIX': // Prepares a downloadable Node.js link for AIX if (typeof platform === 'string') { - return `${baseURL}/node-${versionWithPrefix}-aix-${platform}.tar.gz`; + return `${baseURL}/node-${version}-aix-${platform}.tar.gz`; } - return `${baseURL}/node-${versionWithPrefix}-aix-ppc64.tar.gz`; + return `${baseURL}/node-${version}-aix-ppc64.tar.gz`; default: // Prepares a downloadable Node.js source code link - return `${baseURL}/node-${versionWithPrefix}.tar.gz`; + return `${baseURL}/node-${version}.tar.gz`; } }; diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index 064b82e74f790..8d13dc1e33bdd 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -162,6 +162,11 @@ "status": "Status", "details": "Details" }, + "downloadsTable": { + "fileName": "File Name", + "operatingSystem": "OS", + "architecture": "Architecture" + }, "releaseModal": { "title": "Node.js v{version} ({codename})", "titleWithoutCodename": "Node.js v{version}", @@ -174,6 +179,8 @@ "minorReleasesTable": { "version": "Version", "links": "Links", + "showMore": "Show more", + "information": "Version Informations", "actions": { "release": "Release", "changelog": "Changelog", @@ -323,7 +330,7 @@ "ltsVersionFeaturesNotice": "Want new features sooner? Get the latest Node.js version instead and try the latest improvements!", "communityPlatformInfo": "Installation methods that involve community software are supported by the teams maintaining that software.", "externalSupportInfo": "If you encounter any issues please visit {platform}'s website", - "noScriptDetected": "This page requires JavaScript. You can download Node.js without JavaScript by visiting the releases page directly.", + "noScriptDetected": "This page requires JavaScript. You can download Node.js without JavaScript by visiting the downloads archive page directly.", "platformInfo": { "default": "{platform} and their installation scripts are not maintained by the Node.js project.", "nvm": "\"nvm\" is a cross-platform Node.js version manager.", diff --git a/packages/ui-components/src/Common/Modal/index.module.css b/packages/ui-components/src/Common/Modal/index.module.css index d5e385f7228bd..c9da0809358e1 100644 --- a/packages/ui-components/src/Common/Modal/index.module.css +++ b/packages/ui-components/src/Common/Modal/index.module.css @@ -27,6 +27,7 @@ focus:outline-none sm:my-20 xl:p-12 + dark:border-neutral-800 dark:bg-neutral-950; } diff --git a/packages/ui-components/src/Common/Separator/index.module.css b/packages/ui-components/src/Common/Separator/index.module.css index 61d7dc140faa0..49ef6d6896fec 100644 --- a/packages/ui-components/src/Common/Separator/index.module.css +++ b/packages/ui-components/src/Common/Separator/index.module.css @@ -2,7 +2,8 @@ .root { @apply shrink-0 - bg-neutral-800; + bg-neutral-200 + dark:bg-neutral-800; &.horizontal { @apply h-px diff --git a/packages/ui-components/src/styles/markdown.css b/packages/ui-components/src/styles/markdown.css index b4f9a2cc73cde..bfa26fbd03dfc 100644 --- a/packages/ui-components/src/styles/markdown.css +++ b/packages/ui-components/src/styles/markdown.css @@ -170,4 +170,24 @@ main { @apply sm:border-l-0; } } + + details { + summary * { + @apply inline + pl-2; + } + + a { + h1, + h2, + h3, + h4, + h5, + h6 { + @apply inline + text-green-600 + dark:text-green-400; + } + } + } }