diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml index 4f672479b3586..75cfe199094b8 100644 --- a/.github/workflows/lighthouse.yml +++ b/.github/workflows/lighthouse.yml @@ -97,6 +97,7 @@ jobs: ${{ needs.get-vercel-preview.outputs.url }}/en/about ${{ needs.get-vercel-preview.outputs.url }}/en/about/previous-releases ${{ needs.get-vercel-preview.outputs.url }}/en/download + ${{ needs.get-vercel-preview.outputs.url }}/en/download/archive ${{ needs.get-vercel-preview.outputs.url }}/en/blog uploadArtifacts: true # save results as a action artifacts temporaryPublicStorage: true # upload lighthouse report to the temporary storage diff --git a/apps/site/app/[locale]/[...path]/page.tsx b/apps/site/app/[locale]/[...path]/page.tsx index 68c835fad09b6..786392e15bf14 100644 --- a/apps/site/app/[locale]/[...path]/page.tsx +++ b/apps/site/app/[locale]/[...path]/page.tsx @@ -60,8 +60,10 @@ export const generateStaticParams = async () => { // finally it returns (if the locale and route are valid) the React Component with the relevant context // and attached context providers for rendering the current page const getPage: FC = async props => { + const { path, locale: routeLocale } = await props.params; + // Gets the current full pathname for a given path - const [locale, pathname] = await basePage.getLocaleAndPath(props); + const [locale, pathname] = basePage.getLocaleAndPath(path, routeLocale); // Gets the Markdown content and context const [content, context] = await basePage.getMarkdownContext({ diff --git a/apps/site/app/[locale]/blog/[...path]/page.tsx b/apps/site/app/[locale]/blog/[...path]/page.tsx index f8fd5ea520fb5..f4ab56c6e81be 100644 --- a/apps/site/app/[locale]/blog/[...path]/page.tsx +++ b/apps/site/app/[locale]/blog/[...path]/page.tsx @@ -39,8 +39,10 @@ export const generateStaticParams = async () => { // finally it returns (if the locale and route are valid) the React Component with the relevant context // and attached context providers for rendering the current page const getPage: FC = async props => { + const { path, locale: routeLocale } = await props.params; + // Gets the current full pathname for a given path - const [locale, pathname] = await basePage.getLocaleAndPath(props); + const [locale, pathname] = basePage.getLocaleAndPath(path, routeLocale); // Verifies if the current route is a dynamic route const isDynamicRoute = BLOG_DYNAMIC_ROUTES.some(r => r.includes(pathname)); @@ -52,7 +54,7 @@ const getPage: FC = async props => { pathname: `blog/${pathname}`, }); - // If this isn't a valid dynamic route for blog post or there's no mardown file + // If this isn't a valid dynamic route for blog post or there's no markdown file // for this, then we fail as not found as there's nothing we can do. if (isDynamicRoute || context.filename) { return basePage.renderPage({ diff --git a/apps/site/app/[locale]/download/archive/[version]/page.tsx b/apps/site/app/[locale]/download/archive/[version]/page.tsx new file mode 100644 index 0000000000000..ab6a69d18f026 --- /dev/null +++ b/apps/site/app/[locale]/download/archive/[version]/page.tsx @@ -0,0 +1,79 @@ +import { notFound } from 'next/navigation'; +import type { FC } from 'react'; + +import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs'; +import { ARCHIVE_DYNAMIC_ROUTES } from '#site/next.dynamic.constants.mjs'; +import * as basePage from '#site/next.dynamic.page.mjs'; +import { defaultLocale } from '#site/next.locales.mjs'; + +type DynamicStaticPaths = { version: string; locale: string }; +type DynamicParams = { params: Promise }; + +// This is the default Viewport Metadata +// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function +export const generateViewport = basePage.generateViewport; + +// This generates each page's HTML Metadata +// @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata +export const generateMetadata = basePage.generateMetadata; + +// Generates all possible static paths based on the locales and environment configuration +// - Returns an empty array if static export is disabled (`ENABLE_STATIC_EXPORT` is false) +// - If `ENABLE_STATIC_EXPORT_LOCALE` is true, generates paths for all available locales +// - Otherwise, generates paths only for the default locale +// @see https://nextjs.org/docs/app/api-reference/functions/generate-static-params +export const generateStaticParams = async () => { + // Return an empty array if static export is disabled + if (!ENABLE_STATIC_EXPORT) { + return []; + } + + return ARCHIVE_DYNAMIC_ROUTES.map(version => ({ + locale: defaultLocale.code, + version: version, + })); +}; + +// This method parses the current pathname and does any sort of modifications needed on the route +// then it proceeds to retrieve the Markdown file and parse the MDX Content into a React Component +// finally it returns (if the locale and route are valid) the React Component with the relevant context +// and attached context providers for rendering the current page +const getPage: FC = async props => { + const { version, locale: routeLocale } = await props.params; + + // Gets the current full pathname for a given path + const [locale, pathname] = basePage.getLocaleAndPath(version, routeLocale); + + // Verifies if the current route is a dynamic route + const isDynamicRoute = ARCHIVE_DYNAMIC_ROUTES.some(r => r.includes(pathname)); + + // Gets the Markdown content and context for Download Archive pages + const [content, context] = await basePage.getMarkdownContext({ + locale: locale, + pathname: 'download/archive', + }); + + // If this isn't a valid dynamic route for archive version or there's no markdown + // file for this, then we fail as not found as there's nothing we can do. + if (isDynamicRoute && context.filename) { + return basePage.renderPage({ + content: content, + layout: context.frontmatter.layout!, + context: { ...context, pathname: `/download/archive/${pathname}` }, + }); + } + + return notFound(); +}; + +// Enforces that this route is used as static rendering +// Except whenever on the Development mode as we want instant-refresh when making changes +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic +export const dynamic = 'force-static'; + +// Ensures that this endpoint is invalidated and re-executed every X minutes +// so that when new deployments happen, the data is refreshed +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate +export const revalidate = 300; + +export default getPage; diff --git a/apps/site/app/[locale]/page.tsx b/apps/site/app/[locale]/page.tsx index 482d917122c3e..2c2dbb26b6d0b 100644 --- a/apps/site/app/[locale]/page.tsx +++ b/apps/site/app/[locale]/page.tsx @@ -50,8 +50,10 @@ export const generateStaticParams = async () => { // finally it returns (if the locale and route are valid) the React Component with the relevant context // and attached context providers for rendering the current page const getPage: FC = async props => { + const { path, locale: routeLocale } = await props.params; + // Gets the current full pathname for a given path - const [locale, pathname] = await basePage.getLocaleAndPath(props); + const [locale, pathname] = basePage.getLocaleAndPath(path, routeLocale); // Gets the Markdown content and context const [content, context] = await basePage.getMarkdownContext({ diff --git a/apps/site/components/Downloads/DownloadButton/index.tsx b/apps/site/components/Downloads/DownloadButton/index.tsx index d4f3e7a76dd4c..54d77889e85bc 100644 --- a/apps/site/components/Downloads/DownloadButton/index.tsx +++ b/apps/site/components/Downloads/DownloadButton/index.tsx @@ -21,7 +21,7 @@ const DownloadButton: FC> = ({ const { os, bitness, architecture } = useClientContext(); const platform = getUserPlatform(architecture, bitness); - const downloadLink = getNodeDownloadUrl(versionWithPrefix, os, platform); + const downloadLink = getNodeDownloadUrl({ versionWithPrefix, os, platform }); return ( <> diff --git a/apps/site/components/Downloads/DownloadLink.tsx b/apps/site/components/Downloads/DownloadLink.tsx index 905adbf352d8a..097454d2a03c2 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( + const downloadLink = getNodeDownloadUrl({ versionWithPrefix, os, platform, - kind - ); + kind, + }); return {children}; }; diff --git a/apps/site/components/Downloads/DownloadsTable/index.tsx b/apps/site/components/Downloads/DownloadsTable/index.tsx new file mode 100644 index 0000000000000..5e2c7881efd5d --- /dev/null +++ b/apps/site/components/Downloads/DownloadsTable/index.tsx @@ -0,0 +1,47 @@ +import { useTranslations } from 'next-intl'; +import type { FC } from 'react'; + +import Link from '#site/components/Link'; +import type { DownloadArtifact } from '#site/types'; +import { OperatingSystemLabel } from '#site/util/download'; + +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.fileName} + + {OperatingSystemLabel[release.os]} + + {release.architecture} +
+ ); +}; + +export default DownloadsTable; diff --git a/apps/site/components/Downloads/Release/PrebuiltDownloadButtons.tsx b/apps/site/components/Downloads/Release/PrebuiltDownloadButtons.tsx index fccff0462d69f..1939ca3adaa71 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({ + versionWithPrefix: release.versionWithPrefix, + os: os, + platform: platform, + kind: 'installer', + }) : ''; const binaryUrl = platform - ? getNodeDownloadUrl(release.versionWithPrefix, os, platform, 'binary') + ? getNodeDownloadUrl({ + versionWithPrefix: 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 1258763784fba..6928a8c3b27ab 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,29 +116,7 @@ const ReleaseCodeBox: FC = () => { - {release.status === 'End-of-life' && ( - - {t.rich('layouts.download.codeBox.unsupportedVersionWarning', { - link: text => {text}, - })} - - )} - - {release.isLts && ( - - {t.rich('layouts.download.codeBox.ltsVersionFeaturesNotice', { - link: text => {text}, - })} - - )} + {!currentPlatform || currentPlatform.recommended || ( ; }; -export const MinorReleasesTable: FC = ({ - releases, -}) => { +const MinorReleasesTable: FC = ({ releases }) => { const t = useTranslations(); return ( - - - - - - - - - - {releases.map(release => ( - - - +
+
{t('components.minorReleasesTable.version')}{t('components.minorReleasesTable.links')}
v{release.version} -
- - {t('components.minorReleasesTable.actions.release')} - - - - {t('components.minorReleasesTable.actions.changelog')} - - - - {t('components.minorReleasesTable.actions.docs')} - -
-
+ + + + + - ))} - -
{t('components.minorReleasesTable.version')}{t('components.minorReleasesTable.information')}{t('components.minorReleasesTable.links')}
+ + + {releases.map(release => ( + + + + v{release.version} + + + +
+ {release.modules && ( + <> + + + + )} + {release.npm && ( + <> + + + + )} + +
+ + +
+ + {t('components.minorReleasesTable.actions.docs')} + + + + {t('components.minorReleasesTable.actions.changelog')} + +
+ + + ))} + + + ); }; + +export default MinorReleasesTable; diff --git a/apps/site/components/Releases/PreviousReleasesTable.tsx b/apps/site/components/Releases/PreviousReleasesTable.tsx index 19705cf0958aa..674870bd66033 100644 --- a/apps/site/components/Releases/PreviousReleasesTable.tsx +++ b/apps/site/components/Releases/PreviousReleasesTable.tsx @@ -3,9 +3,10 @@ import Badge from '@node-core/ui-components/Common/Badge'; import { useTranslations } from 'next-intl'; import type { FC } from 'react'; -import { useState } from 'react'; +import { Fragment, useState } from 'react'; import FormattedTime from '#site/components/Common/FormattedTime'; +import Link from '#site/components/Link'; import LinkWithArrow from '#site/components/LinkWithArrow'; import provideReleaseData from '#site/next-data/providers/releaseData'; @@ -41,9 +42,13 @@ const PreviousReleasesTable: FC = () => { {releaseData.map(release => ( - <> + - v{release.major} + + + v{release.major} + + {release.codename || '-'} @@ -77,7 +82,7 @@ const PreviousReleasesTable: FC = () => { open={currentModal === release.version} onOpenChange={open => open || setCurrentModal(undefined)} /> - + ))} diff --git a/apps/site/components/Releases/ReleaseModal.tsx b/apps/site/components/Releases/ReleaseModal.tsx index 0c908c6c204a0..c20ea30624158 100644 --- a/apps/site/components/Releases/ReleaseModal.tsx +++ b/apps/site/components/Releases/ReleaseModal.tsx @@ -1,11 +1,10 @@ -import AlertBox from '@node-core/ui-components/Common/AlertBox'; import { Modal, Title, Content } from '@node-core/ui-components/Common/Modal'; import { useTranslations } from 'next-intl'; import type { ComponentProps, FC } from 'react'; -import Link from '#site/components/Link'; -import { MinorReleasesTable } from '#site/components/Releases/MinorReleasesTable'; -import { ReleaseOverview } from '#site/components/Releases/ReleaseOverview'; +import MinorReleasesTable from '#site/components/Releases/MinorReleasesTable'; +import ReleaseOverview from '#site/components/Releases/ReleaseOverview'; +import WithReleaseAlertBox from '#site/components/withReleaseAlertBox'; import type { NodeRelease } from '#site/types'; type ReleaseModalProps = ComponentProps & { @@ -26,33 +25,7 @@ const ReleaseModal: FC = ({ release, ...props }) => { return ( - {release.status === 'End-of-life' && ( -
- - {t.rich('components.releaseModal.unsupportedVersionWarning', { - link: text => {text}, - })} - -
- )} - - {release.isLts && ( -
- - {t.rich('components.releaseModal.ltsVersionFeaturesNotice', { - link: text => {text}, - })} - -
- )} + {modalHeading} diff --git a/apps/site/components/Releases/ReleaseOverview/index.tsx b/apps/site/components/Releases/ReleaseOverview/index.tsx index db943b583d4d0..47f6026bdcef5 100644 --- a/apps/site/components/Releases/ReleaseOverview/index.tsx +++ b/apps/site/components/Releases/ReleaseOverview/index.tsx @@ -18,7 +18,7 @@ type ReleaseOverviewProps = { release: NodeRelease; }; -export const ReleaseOverview: FC = ({ release }) => { +const ReleaseOverview: FC = ({ release }) => { const t = useTranslations(); return ( @@ -67,3 +67,5 @@ export const ReleaseOverview: FC = ({ release }) => { ); }; + +export default ReleaseOverview; diff --git a/apps/site/components/withDownloadArchive.tsx b/apps/site/components/withDownloadArchive.tsx new file mode 100644 index 0000000000000..e06c9d102e10c --- /dev/null +++ b/apps/site/components/withDownloadArchive.tsx @@ -0,0 +1,46 @@ +import { notFound } from 'next/navigation'; +import type { FC } from 'react'; + +import { getClientContext } from '#site/client-context'; +import provideReleaseData from '#site/next-data/providers/releaseData'; +import { + buildReleaseArtifacts, + extractVersionFromPath, + findReleaseByVersion, +} from '#site/util/download/archive'; + +type DownloadArchive = ReturnType; + +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 notFound(); + } + + // Find the release data for the given version + const releaseData = provideReleaseData(); + const release = findReleaseByVersion(releaseData, version); + + 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/withReleaseAlertBox.tsx b/apps/site/components/withReleaseAlertBox.tsx new file mode 100644 index 0000000000000..ccab6943e76dd --- /dev/null +++ b/apps/site/components/withReleaseAlertBox.tsx @@ -0,0 +1,46 @@ +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('layouts.download.codeBox.unsupportedVersionWarning', { + link: text => {text}, + })} + + ); + case 'Active LTS': + case 'Maintenance LTS': + return ( + + {t.rich('components.releaseModal.ltsVersionFeaturesNotice', { + link: text => {text}, + })} + + ); + default: + return null; + } +}; + +export default WithReleaseAlertBox; diff --git a/apps/site/components/withReleaseSelect.tsx b/apps/site/components/withReleaseSelect.tsx new file mode 100644 index 0000000000000..e48a94b19a98e --- /dev/null +++ b/apps/site/components/withReleaseSelect.tsx @@ -0,0 +1,63 @@ +'use client'; + +import WithNoScriptSelect from '@node-core/ui-components/Common/Select/NoScriptSelect'; +import type { ComponentProps, FC } from 'react'; + +import Link from '#site/components/Link'; +import { useRouter } from '#site/navigation.mjs'; +import provideReleaseData from '#site/next-data/providers/releaseData'; +import type { NodeRelease } from '#site/types'; +import { STATUS_ORDER } from '#site/util/download'; + +type Navigations = Record>; + +/** + * Generates the navigation links for the Node.js download archive + * It creates a list of links for each major release, grouped by status, + * formatted with the major version and codename if available. + */ +const groupReleasesByStatus = (releases: Array) => { + const groupedByStatus = releases.reduce((acc, release) => { + const { status, major, codename, versionWithPrefix } = release; + + if (!acc[status]) { + acc[status] = []; + } + + acc[status].push({ + label: `Node.js v${major}.x ${codename ? `(${codename})` : ''}`, + value: `/download/archive/${versionWithPrefix}`, + }); + + return acc; + }, {} as Navigations); + + return STATUS_ORDER.filter(status => groupedByStatus[status]).map(status => ({ + label: status, + items: groupedByStatus[status], + })); +}; + +type WithReleaseSelectProps = Omit< + ComponentProps, + 'values' | 'as' | 'onChange' +>; + +const WithReleaseSelect: FC = ({ ...props }) => { + const releaseData = provideReleaseData(); + const { push } = useRouter(); + const navigation = groupReleasesByStatus(releaseData); + + return ( + + ); +}; + +export default WithReleaseSelect; diff --git a/apps/site/layouts/DownloadArchive.tsx b/apps/site/layouts/DownloadArchive.tsx new file mode 100644 index 0000000000000..12381a6d30268 --- /dev/null +++ b/apps/site/layouts/DownloadArchive.tsx @@ -0,0 +1,20 @@ +import type { FC, PropsWithChildren } from 'react'; + +import WithFooter from '#site/components/withFooter'; +import WithNavBar from '#site/components/withNavBar'; + +import styles from './layouts.module.css'; + +const DownloadArchiveLayout: FC = ({ children }) => ( + <> + + +
+
{children}
+
+ + + +); + +export default DownloadArchiveLayout; diff --git a/apps/site/next-data/generators/majorNodeReleases.mjs b/apps/site/next-data/generators/majorNodeReleases.mjs new file mode 100644 index 0000000000000..7988067b63040 --- /dev/null +++ b/apps/site/next-data/generators/majorNodeReleases.mjs @@ -0,0 +1,30 @@ +'use strict'; + +import nodevu from '@nodevu/core'; + +const nodevuData = await nodevu({ fetch }); + +/** + * Filters Node.js release data to return only major releases with documented support. + */ +export default async function getMajorNodeReleases() { + return Object.entries(nodevuData).filter(([version, { support }]) => { + // Filter out those without documented support + // Basically those not in schedule.json + if (!support) { + return false; + } + + // 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. + if (version.startsWith('v0.') && version !== 'v0.12') { + return false; + } + + return true; + }); +} diff --git a/apps/site/next-data/generators/releaseData.mjs b/apps/site/next-data/generators/releaseData.mjs index c26ee62508982..e42a12926604b 100644 --- a/apps/site/next-data/generators/releaseData.mjs +++ b/apps/site/next-data/generators/releaseData.mjs @@ -1,6 +1,6 @@ 'use strict'; -import nodevu from '@nodevu/core'; +import getMajorNodeReleases from './majorNodeReleases.mjs'; // Gets the appropriate release status for each major release const getNodeReleaseStatus = (now, support) => { @@ -32,29 +32,7 @@ const getNodeReleaseStatus = (now, support) => { * @returns {Promise>} */ const generateReleaseData = async () => { - const nodevuOutput = await nodevu({ fetch }); - - const majors = Object.entries(nodevuOutput).filter( - ([version, { support }]) => { - // Filter out those without documented support - // Basically those not in schedule.json - if (!support) { - return false; - } - - // 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. - if (version.startsWith('v0.') && version !== 'v0.12') { - return false; - } - - return true; - } - ); + const majors = await getMajorNodeReleases(); return majors.map(([, major]) => { const [latestVersion] = Object.values(major.releases); @@ -70,8 +48,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}`, })); return { diff --git a/apps/site/next-data/generators/releaseVersions.mjs b/apps/site/next-data/generators/releaseVersions.mjs new file mode 100644 index 0000000000000..2dfbd3f3f0484 --- /dev/null +++ b/apps/site/next-data/generators/releaseVersions.mjs @@ -0,0 +1,24 @@ +'use strict'; + +import getMajorNodeReleases from './majorNodeReleases.mjs'; + +/** + * This method is used to generate all Node.js versions + * for self-consumption during RSC and Static Builds + * + * @returns {Promise>} + */ +const generateAllVersionsData = async () => { + const majors = await getMajorNodeReleases(); + + return majors.reduce( + (allVersions, [, major]) => + allVersions.concat( + Object.entries(major.releases).map( + ([, release]) => `v${release.semver.raw}` + ) + ), + [] + ); +}; +export default generateAllVersionsData; diff --git a/apps/site/next-data/providers/releaseVersions.ts b/apps/site/next-data/providers/releaseVersions.ts new file mode 100644 index 0000000000000..f7ac4d86d010d --- /dev/null +++ b/apps/site/next-data/providers/releaseVersions.ts @@ -0,0 +1,9 @@ +import { cache } from 'react'; + +import generateAllVersionsData from '#site/next-data/generators/releaseVersions.mjs'; + +const releaseVersions = await generateAllVersionsData(); + +const provideReleaseVersions = cache(() => releaseVersions); + +export default provideReleaseVersions; diff --git a/apps/site/next.dynamic.constants.mjs b/apps/site/next.dynamic.constants.mjs index ce9f3128e0077..68860ee2e2a0f 100644 --- a/apps/site/next.dynamic.constants.mjs +++ b/apps/site/next.dynamic.constants.mjs @@ -1,6 +1,7 @@ 'use strict'; import { provideBlogPosts } from '#site/next-data/providers/blogData'; +import provideReleaseVersions from '#site/next-data/providers/releaseVersions'; import { blogData } from '#site/next.json.mjs'; import { BASE_PATH, BASE_URL } from './next.constants.mjs'; @@ -27,6 +28,19 @@ export const BLOG_DYNAMIC_ROUTES = [ .flat(), ]; +/** + * This constant is used to create static routes on-the-fly that do not have a file-system + * counterpart route. This is useful for providing routes with matching Layout Names + * but that do not have Markdown content and a matching file for the route + * + * @type {Array} A Map of pathname and Layout Name + */ +export const ARCHIVE_DYNAMIC_ROUTES = [ + // Creates dynamic routes for downloads archive pages for each version + // (e.g., /download/archive/v18.20.8, /download/archive/v20.19.2) + ...provideReleaseVersions(), +]; + /** * This is the default Next.js Page Metadata for all pages * diff --git a/apps/site/next.dynamic.mjs b/apps/site/next.dynamic.mjs index 4b80852e12d9b..b1822ead633df 100644 --- a/apps/site/next.dynamic.mjs +++ b/apps/site/next.dynamic.mjs @@ -23,7 +23,8 @@ import { MDX_COMPONENTS } from './next.mdx.components.mjs'; const baseUrlAndPath = `${BASE_URL}${BASE_PATH}`; // This is a small utility that allows us to quickly separate locale from the remaining pathname -const getPathname = (path = []) => path.join('/'); +const getPathname = (path = []) => + Array.isArray(path) ? path.join('/') : path; // This maps a pathname into an actual route object that can be used // we use a platform-specific separator to split the pathname diff --git a/apps/site/next.dynamic.page.mjs b/apps/site/next.dynamic.page.mjs index 53c583ea29030..6dab59d86f17f 100644 --- a/apps/site/next.dynamic.page.mjs +++ b/apps/site/next.dynamic.page.mjs @@ -37,12 +37,11 @@ export const generateMetadata = async props => { /** * This method is used for retrieving the current locale and pathname from the request * - * @param {{ params: Promise<{ path: Array; locale: string }> }} props - * @returns {Promise<[string, string]>} the locale and pathname for the request + * @param {string|Array} path + * @param {string} locale + * @returns {[string, string]} the locale and pathname for the request */ -export const getLocaleAndPath = async props => { - const { path = [], locale = defaultLocale.code } = await props.params; - +export const getLocaleAndPath = (path = [], locale = defaultLocale.code) => { if (!availableLocaleCodes.includes(locale)) { // Forces the current locale to be the Default Locale setRequestLocale(defaultLocale.code); diff --git a/apps/site/next.mdx.use.client.mjs b/apps/site/next.mdx.use.client.mjs index 67bdc3b4b16d4..94e04232bee72 100644 --- a/apps/site/next.mdx.use.client.mjs +++ b/apps/site/next.mdx.use.client.mjs @@ -17,6 +17,7 @@ import ReleaseVersionDropdown from './components/Downloads/Release/VersionDropdo import Link from './components/Link'; import MDXCodeBox from './components/MDX/CodeBox'; import MDXImage from './components/MDX/Image'; +import WithReleaseSelect from './components/withReleaseSelect'; import { ReleaseProvider } from './providers/releaseProvider'; /** @@ -29,6 +30,8 @@ export const clientMdxComponents = { CodeTabs: MDXCodeTabs, // Renders a Download Button DownloadButton: DownloadButton, + // Renders a stateless Release Select Component + WithReleaseSelect: WithReleaseSelect, // Group of components that enable you to select versions for Node.js // releases and download selected versions. Uses `releaseProvider` as a provider Release: { diff --git a/apps/site/next.mdx.use.mjs b/apps/site/next.mdx.use.mjs index 39590e493f5b6..343980a90fae9 100644 --- a/apps/site/next.mdx.use.mjs +++ b/apps/site/next.mdx.use.mjs @@ -3,15 +3,20 @@ import BadgeGroup from '@node-core/ui-components/Common/BadgeGroup'; import Button from './components/Common/Button'; +import DownloadsTable from './components/Downloads/DownloadsTable'; import EOLAlertBox from './components/EOL/EOLAlert'; import EOLReleaseTable from './components/EOL/EOLReleaseTable'; import Link from './components/Link'; import LinkWithArrow from './components/LinkWithArrow'; import UpcomingMeetings from './components/MDX/Calendar/UpcomingMeetings'; +import MinorReleasesTable from './components/Releases/MinorReleasesTable'; import PreviousReleasesTable from './components/Releases/PreviousReleasesTable'; +import ReleaseOverview from './components/Releases/ReleaseOverview'; 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 @@ -19,15 +24,25 @@ import WithNodeRelease from './components/withNodeRelease'; * @satisfies {import('mdx/types').MDXComponents} */ export const mdxComponents = { + // HOC for providing the Download Archive Page properties + WithDownloadArchive, + // Renders a table with Node.js Releases with different platforms and architectures + DownloadsTable, PreviousReleasesTable, // 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, // Renders an EOL alert diff --git a/apps/site/pages/en/download/archive/index.mdx b/apps/site/pages/en/download/archive/index.mdx new file mode 100644 index 0000000000000..480a1564e7c5e --- /dev/null +++ b/apps/site/pages/en/download/archive/index.mdx @@ -0,0 +1,57 @@ +--- +title: Download Node.js® +layout: download-archive +--- + + + {({ binaries, installers, version, release, sources }) => ( + <> +

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

+ + +

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..9fecf27418f51 100644 --- a/apps/site/types/download.ts +++ b/apps/site/types/download.ts @@ -1,7 +1,41 @@ +import type { SelectValue } from '@node-core/ui-components/Common/Select'; + +import type { + IntlMessageKeys, + NodeReleaseStatus, + OperatingSystem, + Platform, +} from '#site/types'; + export interface DownloadSnippet { name: string; language: string; content: string; } -export type DownloadKind = 'installer' | 'binary' | 'source'; +export type DownloadKind = 'installer' | 'binary' | 'source' | 'shasum'; + +type DownloadCompatibility = { + os?: Array; + installMethod?: Array; + platform?: Array; + semver?: Array; + releases?: Array; +}; + +export type DownloadDropdownItem = { + label: IntlMessageKeys; + recommended?: boolean; + url?: string; + info?: IntlMessageKeys; + compatibility: DownloadCompatibility; +} & Omit, 'label'>; + +export type DownloadArtifact = { + fileName: string; + kind: DownloadKind; + os: OperatingSystem; + architecture: string; + url: string; + version: string; +}; 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 1c98008cec210..2e54681d132e8 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..1bf4b97b724a8 100644 --- a/apps/site/util/__tests__/url.test.mjs +++ b/apps/site/util/__tests__/url.test.mjs @@ -3,7 +3,7 @@ import { describe, it } from 'node:test'; import { getNodeDownloadUrl, getNodeApiUrl } from '../url'; -const version = 'v18.16.0'; +const versionWithPrefix = 'v18.16.0'; describe('getNodeApiUrl', () => { it('should return the correct API link for versions >=0.3.1 and <0.5.1', () => { @@ -56,48 +56,70 @@ 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({ versionWithPrefix, 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({ versionWithPrefix, 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({ versionWithPrefix, 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({ versionWithPrefix, os, platform }), + expectedUrl + ); }); describe('MAC', () => { it('should return .pkg link for installer', () => { - const url = getNodeDownloadUrl('v18.0.0', 'MAC', 'x64', 'installer'); + const url = getNodeDownloadUrl({ + versionWithPrefix: '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({ + versionWithPrefix: '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..a8cdc06c5fe03 --- /dev/null +++ b/apps/site/util/download/archive.tsx @@ -0,0 +1,198 @@ +import semVer from 'semver'; + +import type { + DownloadDropdownItem, + DownloadKind, + DownloadArtifact, + OperatingSystem, + Platform, +} from '#site/types'; +import type { NodeRelease } from '#site/types/releases'; +import { OS_NOT_SUPPORTING_INSTALLERS, PLATFORMS } from '#site/util/download'; +import { getNodeDownloadUrl } from '#site/util/url'; + +import { DIST_URL } from '#site/next.constants'; + +/** + * 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; + versionWithPrefix: string; + kind?: DownloadKind; +}; + +/** + * Creates a download artifact from platform data + */ +const createDownloadArtifact = ( + os: OperatingSystem, + platform: DownloadDropdownItem, + versionWithPrefix: string, + kind: DownloadKind +): DownloadArtifact => { + const url = getNodeDownloadUrl({ + versionWithPrefix: versionWithPrefix, + os: os, + platform: platform.value, + kind: kind, + }); + + return { + fileName: url.replace(`${DIST_URL}${versionWithPrefix}/`, ''), + kind: kind, + os: os, + architecture: platform.label, + url: url, + version: versionWithPrefix, + }; +}; + +type CompatiblePlatforms = Array<{ + os: OperatingSystem; + platform: DownloadDropdownItem; +}>; + +/** + * Filters platforms by compatibility and exclusions + */ +const getCompatiblePlatforms = ( + platforms: Record>>, + exclude: Array, + versionWithPrefix: string +): CompatiblePlatforms => { + return Object.entries(platforms).flatMap(([os, items]) => { + if (exclude.includes(os)) return []; + + return items + .filter(({ compatibility, value }) => + isCompatible( + compatibility, + os as OperatingSystem, + value, + versionWithPrefix + ) + ) + .map(platform => ({ + os: os as OperatingSystem, + platform: platform, + })); + }); +}; + +/** + * Returns a list of compatible artifacts for the given options. + */ +const getCompatibleArtifacts = ({ + platforms = PLATFORMS, + exclude = [], + versionWithPrefix, + kind = 'binary', +}: CompatibleArtifactOptions): Array => { + const compatiblePlatforms = getCompatiblePlatforms( + platforms, + exclude, + versionWithPrefix + ); + + return compatiblePlatforms.map(({ os, platform }) => + createDownloadArtifact(os, platform, versionWithPrefix, kind) + ); +}; + +/** + * 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, + versionWithPrefix: string +) => { + const minorVersion = release.minorVersions.find( + ({ versionWithPrefix: version }) => version === versionWithPrefix + ); + + const enrichedRelease = { + ...release, + ...minorVersion, + }; + + return { + binaries: getCompatibleArtifacts({ + versionWithPrefix: versionWithPrefix, + kind: 'binary', + }), + installers: getCompatibleArtifacts({ + exclude: OS_NOT_SUPPORTING_INSTALLERS, + versionWithPrefix: versionWithPrefix, + kind: 'installer', + }), + sources: { + shasum: getNodeDownloadUrl({ + versionWithPrefix: versionWithPrefix, + kind: 'shasum', + }), + tarball: getNodeDownloadUrl({ + versionWithPrefix: versionWithPrefix, + kind: 'source', + }), + }, + version: versionWithPrefix, + 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 === 'Current')!; + } + + 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 4834573ee7337..02694b9bcfe0b 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"] } } ] @@ -229,5 +232,12 @@ "platforms": ["arm64", "armv7l", "ppc64le", "ppc64", "s390x", "x64", "x86"], "bitness": ["64", "32"], "architecture": ["arm", "x86"] - } + }, + "statusOrder": [ + "Current", + "Active LTS", + "Maintenance LTS", + "End-of-life", + "Pending" + ] } diff --git a/apps/site/util/download/index.tsx b/apps/site/util/download/index.tsx index 9b31281e4c1f8..bca51484cc93a 100644 --- a/apps/site/util/download/index.tsx +++ b/apps/site/util/download/index.tsx @@ -1,4 +1,3 @@ -import type { SelectValue } from '@node-core/ui-components/Common/Select'; import * as InstallMethodIcons from '@node-core/ui-components/Icons/InstallationMethod'; import * as OSIcons from '@node-core/ui-components/Icons/OperatingSystem'; import * as PackageManagerIcons from '@node-core/ui-components/Icons/PackageManager'; @@ -6,6 +5,7 @@ import type { ElementType } from 'react'; import satisfies from 'semver/functions/satisfies'; import type { + DownloadDropdownItem, IntlMessageKeys, NodeReleaseStatus, OperatingSystem, @@ -15,7 +15,7 @@ import type * as Types from '#site/types/release'; import constants from './constants.json'; -const { systems, installMethods, packageManagers } = constants; +const { systems, installMethods, packageManagers, statusOrder } = constants; // Extract the non-installer supporting OSes export const OS_NOT_SUPPORTING_INSTALLERS = Object.entries(systems) @@ -27,23 +27,6 @@ export const OperatingSystemLabel = Object.fromEntries( Object.entries(systems).map(([key, data]) => [key, data.name]) ); -// Base types for dropdown functionality -type DownloadCompatibility = { - os?: Array; - installMethod?: Array; - platform?: Array; - semver?: Array; - releases?: Array; -}; - -type DownloadDropdownItem = { - label: IntlMessageKeys; - recommended?: boolean; - url?: string; - info?: IntlMessageKeys; - compatibility: DownloadCompatibility; -} & Omit, 'label'>; - /** * Gets the next valid item when current item is disabled/excluded */ @@ -153,3 +136,5 @@ export const PLATFORMS = Object.fromEntries( })), ]) ) as Record>>; + +export const STATUS_ORDER = statusOrder; diff --git a/apps/site/util/url.ts b/apps/site/util/url.ts index 9fde24b05294e..7f6a03057824e 100644 --- a/apps/site/util/url.ts +++ b/apps/site/util/url.ts @@ -17,18 +17,36 @@ export const getNodeApiUrl = (version: string) => { : `${DIST_URL}${version}/docs/api/`; }; -export const getNodeDownloadUrl = ( - versionWithPrefix: string, - os: OperatingSystem | 'LOADING', - platform: Platform = 'x64', - kind: DownloadKind = 'installer' -) => { +type DownloadOptions = { + /** The Node.js version string, must include the 'v' prefix (e.g., 'v20.12.2'). */ + versionWithPrefix: string; + /** The target operating system. Defaults to 'LOADING'. */ + os?: OperatingSystem | 'LOADING'; + /** The target platform/architecture (e.g., 'x64', 'arm64'). Defaults to 'x64'. */ + platform?: Platform; + /** The type of download artifact. Can be 'installer', 'binary', 'source', or 'shasum'. Defaults to 'installer'. */ + kind?: DownloadKind; +}; + +/** + * Generates a Node.js download URL for the given options + */ +export const getNodeDownloadUrl = ({ + versionWithPrefix, + os = 'LOADING', + platform = 'x64', + kind = 'installer', +}: DownloadOptions) => { const baseURL = `${DIST_URL}${versionWithPrefix}`; if (kind === 'source') { return `${baseURL}/node-${versionWithPrefix}.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 diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index ec85d0e3a0b1e..3fec6ccf78e87 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -166,6 +166,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}", @@ -212,6 +217,7 @@ "minorReleasesTable": { "version": "Version", "links": "Links", + "information": "Version Informations", "actions": { "release": "Release", "changelog": "Changelog", @@ -362,7 +368,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/Select/index.module.css b/packages/ui-components/src/Common/Select/index.module.css index 5e048f9b4be71..605b9fa866445 100644 --- a/packages/ui-components/src/Common/Select/index.module.css +++ b/packages/ui-components/src/Common/Select/index.module.css @@ -161,12 +161,17 @@ } .noscript { - @apply relative; + @apply relative + cursor-pointer; summary { @apply flex w-full justify-between; + + span { + @apply pl-0; + } } .trigger { 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