From 02db91b92defd39f1388b5403b022bc185741298 Mon Sep 17 00:00:00 2001 From: Brian Muenzenmeyer Date: Wed, 16 Jul 2025 18:30:23 -0500 Subject: [PATCH 01/52] feat: add eol page Co-Authored-By: Aviv Keller --- apps/site/app/[locale]/page.tsx | 4 +- .../DownloadReleasesTable/DetailsButton.tsx | 14 +- .../Downloads/DownloadReleasesTable/index.tsx | 2 +- .../Downloads/Release/ReleaseCodeBox.tsx | 2 +- .../Downloads/ReleaseModal/index.tsx | 21 +-- apps/site/components/EOL/Alert.tsx | 18 ++ apps/site/components/EOL/Modal.tsx | 156 ++++++++++++++++++ apps/site/components/EOL/Table.tsx | 63 +++++++ .../VulnerabilityChips/Chip/index.module.css | 8 + .../EOL/VulnerabilityChips/Chip/index.tsx | 36 ++++ .../EOL/VulnerabilityChips/index.tsx | 41 +++++ apps/site/components/withLayout.tsx | 20 ++- apps/site/layouts/About.tsx | 5 +- apps/site/layouts/Post.tsx | 3 + .../next-data/generators/vulnerabilities.mjs | 40 +++++ .../next-data/providers/vulnerabilities.ts | 9 + apps/site/next.mdx.use.mjs | 6 + .../site/pages/en/about/previous-releases.mdx | 7 +- apps/site/pages/en/eol.mdx | 46 ++++++ apps/site/pages/en/index.mdx | 2 +- apps/site/providers/modalProvider.tsx | 56 +++++++ apps/site/providers/releaseModalProvider.tsx | 45 ----- apps/site/types/vulnerabilities.ts | 14 ++ packages/i18n/src/locales/en.json | 24 +++ .../src/Common/AlertBox/index.stories.tsx | 15 ++ .../src/Common/AlertBox/index.tsx | 2 +- 26 files changed, 575 insertions(+), 84 deletions(-) create mode 100644 apps/site/components/EOL/Alert.tsx create mode 100644 apps/site/components/EOL/Modal.tsx create mode 100644 apps/site/components/EOL/Table.tsx create mode 100644 apps/site/components/EOL/VulnerabilityChips/Chip/index.module.css create mode 100644 apps/site/components/EOL/VulnerabilityChips/Chip/index.tsx create mode 100644 apps/site/components/EOL/VulnerabilityChips/index.tsx create mode 100644 apps/site/next-data/generators/vulnerabilities.mjs create mode 100644 apps/site/next-data/providers/vulnerabilities.ts create mode 100644 apps/site/pages/en/eol.mdx create mode 100644 apps/site/providers/modalProvider.tsx delete mode 100644 apps/site/providers/releaseModalProvider.tsx create mode 100644 apps/site/types/vulnerabilities.ts diff --git a/apps/site/app/[locale]/page.tsx b/apps/site/app/[locale]/page.tsx index cf9d20c1de3b3..a82b5e711c507 100644 --- a/apps/site/app/[locale]/page.tsx +++ b/apps/site/app/[locale]/page.tsx @@ -150,7 +150,9 @@ const getPage: FC = async props => { // within a server-side context return ( - {content} + + {content} + ); } diff --git a/apps/site/components/Downloads/DownloadReleasesTable/DetailsButton.tsx b/apps/site/components/Downloads/DownloadReleasesTable/DetailsButton.tsx index b8fbeb6f160b9..b656b8f379ac4 100644 --- a/apps/site/components/Downloads/DownloadReleasesTable/DetailsButton.tsx +++ b/apps/site/components/Downloads/DownloadReleasesTable/DetailsButton.tsx @@ -5,23 +5,19 @@ import type { FC } from 'react'; import { use } from 'react'; import LinkWithArrow from '#site/components/LinkWithArrow'; -import { ReleaseModalContext } from '#site/providers/releaseModalProvider'; -import type { NodeRelease } from '#site/types'; +import { ModalContext } from '#site/providers/modalProvider'; type DetailsButtonProps = { - versionData: NodeRelease; + data: unknown; }; -const DetailsButton: FC = ({ versionData }) => { +const DetailsButton: FC = ({ data }) => { const t = useTranslations('components.downloadReleasesTable'); - const { openModal } = use(ReleaseModalContext); + const { openModal } = use(ModalContext); return ( - openModal(versionData)} - > + openModal(data)}> {t('details')} ); diff --git a/apps/site/components/Downloads/DownloadReleasesTable/index.tsx b/apps/site/components/Downloads/DownloadReleasesTable/index.tsx index 557d6bd86e039..4cd2a8072c952 100644 --- a/apps/site/components/Downloads/DownloadReleasesTable/index.tsx +++ b/apps/site/components/Downloads/DownloadReleasesTable/index.tsx @@ -48,7 +48,7 @@ const DownloadReleasesTable: FC = () => { - + ))} diff --git a/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx b/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx index ccd9073412f41..1258763784fba 100644 --- a/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx +++ b/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx @@ -122,7 +122,7 @@ const ReleaseCodeBox: FC = () => { size="small" > {t.rich('layouts.download.codeBox.unsupportedVersionWarning', { - link: text => {text}, + link: text => {text}, })} )} diff --git a/apps/site/components/Downloads/ReleaseModal/index.tsx b/apps/site/components/Downloads/ReleaseModal/index.tsx index f5a89e8a8a776..8f1869a65e474 100644 --- a/apps/site/components/Downloads/ReleaseModal/index.tsx +++ b/apps/site/components/Downloads/ReleaseModal/index.tsx @@ -6,19 +6,11 @@ import type { FC } from 'react'; import { MinorReleasesTable } from '#site/components/Downloads/MinorReleasesTable'; import { ReleaseOverview } from '#site/components/Downloads/ReleaseOverview'; import Link from '#site/components/Link'; +import type { ModalProps } from '#site/providers/modalProvider'; import type { NodeRelease } from '#site/types'; -type ReleaseModalProps = { - isOpen: boolean; - closeModal: () => void; - release: NodeRelease; -}; - -const ReleaseModal: FC = ({ - isOpen, - closeModal, - release, -}) => { +const ReleaseModal: FC = ({ open, closeModal, data }) => { + const release = data as NodeRelease; const t = useTranslations(); const modalHeadingKey = release.codename @@ -31,7 +23,7 @@ const ReleaseModal: FC = ({ }); return ( - + {release.status === 'End-of-life' && (
= ({ > {t.rich('components.releaseModal.unsupportedVersionWarning', { link: text => ( - + {text} ), diff --git a/apps/site/components/EOL/Alert.tsx b/apps/site/components/EOL/Alert.tsx new file mode 100644 index 0000000000000..b44dc6c923f2c --- /dev/null +++ b/apps/site/components/EOL/Alert.tsx @@ -0,0 +1,18 @@ +import AlertBox from '@node-core/ui-components/Common/AlertBox'; +import { useTranslations } from 'next-intl'; + +import Link from '#site/components/Link'; + +const EOLAlert = () => { + const t = useTranslations('components.endOfLife'); + return ( + + {t('intro')}{' '} + + OpenJS Ecosystem Sustainability Program partner HeroDevs + + + ); +}; + +export default EOLAlert; diff --git a/apps/site/components/EOL/Modal.tsx b/apps/site/components/EOL/Modal.tsx new file mode 100644 index 0000000000000..ed1c2d8e3262b --- /dev/null +++ b/apps/site/components/EOL/Modal.tsx @@ -0,0 +1,156 @@ +import { Modal, Title, Content } from '@node-core/ui-components/Common/Modal'; +import classNames from 'classnames'; +import { useTranslations } from 'next-intl'; +import type { FC } from 'react'; + +import VulnerabilityChip from '#site/components/EOL/VulnerabilityChips/Chip'; +import LinkWithArrow from '#site/components/LinkWithArrow'; +import type { ModalProps } from '#site/providers/modalProvider'; +import type { NodeRelease } from '#site/types'; +import type { Vulnerability } from '#site/types/vulnerabilities'; + +import { SEVERITY_ORDER } from './VulnerabilityChips'; + +type EOLModalData = { + release: NodeRelease; + vulnerabilities: Array; +}; + +type KnownVulnerability = Vulnerability & { + severity: (typeof SEVERITY_ORDER)[number]; +}; + +const VulnerabilitiesTable: FC<{ + vulnerabilities: Array; + maxWidth?: string; +}> = ({ vulnerabilities, maxWidth = 'max-w-2xs' }) => { + const t = useTranslations('components.eolModal'); + + return ( + + + + + + + + + + + {vulnerabilities.map((vuln, i) => ( + + + + + + + ))} + +
{t('table.cves')}{t('table.severity')}{t('table.overview')}{t('table.details')}
+ {vuln.cve.length + ? vuln.cve.map(cveId => ( +
+ + {cveId} + +
+ )) + : '-'} +
+ + + {vuln.description || vuln.overview || '-'} + + {vuln.ref ? ( + + {t('blogLinkText')} + + ) : ( + '—' + )} +
+ ); +}; + +const UnknownSeveritySection: FC<{ + vulnerabilities: Array; + hasKnownVulns: boolean; +}> = ({ vulnerabilities, hasKnownVulns }) => { + const t = useTranslations('components.eolModal'); + + if (!vulnerabilities.length) { + return null; + } + + return ( +
+ + {t('showUnknownSeverities')} ({vulnerabilities.length}) + +
+ +
+
+ ); +}; + +const EOLModal: FC = ({ open, closeModal, data }) => { + const { release, vulnerabilities } = data as EOLModalData; + const t = useTranslations('components.eolModal'); + + const modalHeading = t(release.codename ? 'title' : 'titleWithoutCodename', { + version: release.major, + codename: release.codename ?? '', + }); + + const [knownVulns, unknownVulns] = vulnerabilities.reduce( + (acc, vuln) => { + acc[vuln.severity === 'unknown' ? 1 : 0].push(vuln as KnownVulnerability); + return acc; + }, + [[], []] as [Array, Array] + ); + + knownVulns.sort( + (a, b) => + SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity) + ); + + const hasKnownVulns = knownVulns.length > 0; + const hasAnyVulns = hasKnownVulns || unknownVulns.length > 0; + + return ( + + {modalHeading} + + {vulnerabilities.length > 0 && ( +

+ {t('vulnerabilitiesMessage', { count: vulnerabilities.length })} +

+ )} + + {hasKnownVulns && } + + + + {!hasAnyVulns &&

{t('noVulnerabilitiesMessage')}

} +
+
+ ); +}; + +export default EOLModal; diff --git a/apps/site/components/EOL/Table.tsx b/apps/site/components/EOL/Table.tsx new file mode 100644 index 0000000000000..1c770007bf38d --- /dev/null +++ b/apps/site/components/EOL/Table.tsx @@ -0,0 +1,63 @@ +import { getTranslations } from 'next-intl/server'; +import type { FC } from 'react'; + +import FormattedTime from '#site/components/Common/FormattedTime'; +import DetailsButton from '#site/components/Downloads/DownloadReleasesTable/DetailsButton'; +import provideReleaseData from '#site/next-data/providers/releaseData'; +import provideVulnerabilities from '#site/next-data/providers/vulnerabilities'; + +import VulnerabilityChips from './VulnerabilityChips'; + +const EOLTable: FC = async () => { + const releaseData = provideReleaseData(); + const vulnerabilities = await provideVulnerabilities(); + const EOLReleases = releaseData.filter( + release => release.status === 'End-of-life' + ); + + const t = await getTranslations(); + + return ( + + + + {/* TODO @bmuenzenmeyer change these to new i18n keys */} + + + + + + + + {EOLReleases.map(release => ( + + + + + + + ))} + +
+ {t('components.downloadReleasesTable.version')} ( + {t('components.downloadReleasesTable.codename')}) + {t('components.downloadReleasesTable.lastUpdated')}VulnerabilitiesDetails
+ v{release.major} {release.codename ? `(${release.codename})` : ''} + + + + + + +
+ ); +}; + +export default EOLTable; diff --git a/apps/site/components/EOL/VulnerabilityChips/Chip/index.module.css b/apps/site/components/EOL/VulnerabilityChips/Chip/index.module.css new file mode 100644 index 0000000000000..0dc0a5ebc7337 --- /dev/null +++ b/apps/site/components/EOL/VulnerabilityChips/Chip/index.module.css @@ -0,0 +1,8 @@ +@reference "../../../../styles/index.css"; + +.chipCount { + @apply mr-1 + rounded-sm + bg-gray-800/20 + px-1.5; +} diff --git a/apps/site/components/EOL/VulnerabilityChips/Chip/index.tsx b/apps/site/components/EOL/VulnerabilityChips/Chip/index.tsx new file mode 100644 index 0000000000000..ce7e7af33ad93 --- /dev/null +++ b/apps/site/components/EOL/VulnerabilityChips/Chip/index.tsx @@ -0,0 +1,36 @@ +import Badge from '@node-core/ui-components/Common/Badge'; +import { useTranslations } from 'next-intl'; +import type { FC } from 'react'; + +import styles from './index.module.css'; + +export const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low'] as const; + +const SEVERITY_KIND_MAP = { + unknown: 'neutral', + low: 'default', + medium: 'info', + high: 'warning', + critical: 'error', +} as const; + +type VulnerabilityChipProps = { + severity: keyof typeof SEVERITY_KIND_MAP; + count?: number; +}; + +const VulnerabilityChip: FC = ({ + severity, + count = 0, +}) => { + const t = useTranslations('components.endOfLife'); + + return ( + + {count > 0 ? {count} : null} + {t(`severity.${severity}`)} + + ); +}; + +export default VulnerabilityChip; diff --git a/apps/site/components/EOL/VulnerabilityChips/index.tsx b/apps/site/components/EOL/VulnerabilityChips/index.tsx new file mode 100644 index 0000000000000..fcd73e6b15930 --- /dev/null +++ b/apps/site/components/EOL/VulnerabilityChips/index.tsx @@ -0,0 +1,41 @@ +import type { FC } from 'react'; + +import type { Vulnerability } from '#site/types/vulnerabilities'; + +import VulnerabilityChip from './Chip'; + +export const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low'] as const; + +type VulnerabilityChipsProps = { + vulnerabilities: Array; +}; + +const VulnerabilityChips: FC = ({ + vulnerabilities, +}) => { + // Group vulnerabilities by severity + const groupedBySeverity = vulnerabilities.reduce>( + (acc, vuln) => { + const severity = vuln.severity.toLowerCase(); + acc[severity] = (acc[severity] || 0) + 1; + return acc; + }, + {} + ); + + return ( +
+ {SEVERITY_ORDER.filter(severity => groupedBySeverity[severity] > 0).map( + severity => ( + + ) + )} +
+ ); +}; + +export default VulnerabilityChips; diff --git a/apps/site/components/withLayout.tsx b/apps/site/components/withLayout.tsx index ec553190e621d..44c00b7ea220f 100644 --- a/apps/site/components/withLayout.tsx +++ b/apps/site/components/withLayout.tsx @@ -8,6 +8,7 @@ import DownloadLayout from '#site/layouts/Download'; import GlowingBackdropLayout from '#site/layouts/GlowingBackdrop'; import LearnLayout from '#site/layouts/Learn'; import PostLayout from '#site/layouts/Post'; +import { ModalProvider } from '#site/providers/modalProvider'; import type { Layouts } from '#site/types'; const layouts = { @@ -21,11 +22,26 @@ const layouts = { article: ArticlePageLayout, } satisfies Record; -type WithLayoutProps = PropsWithChildren<{ layout: L }>; +type WithLayoutProps = PropsWithChildren<{ + layout: L; + modal?: string; +}>; -const WithLayout: FC> = ({ layout, children }) => { +const WithLayout: FC> = ({ + layout, + children, + modal, +}) => { const LayoutComponent = layouts[layout] ?? DefaultLayout; + if (modal) { + return ( + + {children} + + ); + } + return {children}; }; diff --git a/apps/site/layouts/About.tsx b/apps/site/layouts/About.tsx index b6f2fecae5419..cf616baf892aa 100644 --- a/apps/site/layouts/About.tsx +++ b/apps/site/layouts/About.tsx @@ -6,10 +6,9 @@ import WithFooter from '#site/components/withFooter'; import WithMetaBar from '#site/components/withMetaBar'; import WithNavBar from '#site/components/withNavBar'; import WithSidebar from '#site/components/withSidebar'; -import { ReleaseModalProvider } from '#site/providers/releaseModalProvider'; const AboutLayout: FC = ({ children }) => ( - + <>
@@ -25,7 +24,7 @@ const AboutLayout: FC = ({ children }) => (
-
+ ); export default AboutLayout; diff --git a/apps/site/layouts/Post.tsx b/apps/site/layouts/Post.tsx index 9b5234b727757..9d356ce55ab83 100644 --- a/apps/site/layouts/Post.tsx +++ b/apps/site/layouts/Post.tsx @@ -1,6 +1,7 @@ import Preview from '@node-core/ui-components/Common/Preview'; import type { FC, PropsWithChildren } from 'react'; +import EOLAlert from '#site/components/EOL/Alert'; import WithAvatarGroup from '#site/components/withAvatarGroup'; import WithBlogCrossLinks from '#site/components/withBlogCrossLinks'; import WithFooter from '#site/components/withFooter'; @@ -26,6 +27,8 @@ const PostLayout: FC = ({ children }) => {
+ {type === 'vulnerability' && } +

{frontmatter.title}

diff --git a/apps/site/next-data/generators/vulnerabilities.mjs b/apps/site/next-data/generators/vulnerabilities.mjs new file mode 100644 index 0000000000000..1aabfc9201967 --- /dev/null +++ b/apps/site/next-data/generators/vulnerabilities.mjs @@ -0,0 +1,40 @@ +/** + * Groups vulnerabilities by major version number extracted from the `vulnerable` string. + * + * @param {Array>} vulnerabilities Array of Vulnerability objects + */ +function groupVulnerabilitiesByMajor(vulnerabilities) { + const grouped = {}; + + for (const vulnerability of vulnerabilities) { + const majorVersions = + vulnerability.vulnerable + .match(/\b\d+\b/g) + ?.map(Number) + .filter(major => !isNaN(major)) ?? []; + + for (const majorVersion of majorVersions) { + const key = majorVersion.toString(); + if (!grouped[key]) grouped[key] = []; + grouped[key].push(vulnerability); + } + } + + return grouped; +} + +/** + * Fetches vulnerability data from the Node.js Security Working Group repository, + * and returns it grouped by major version. + * + * @returns {Promise} Grouped vulnerabilities + */ +export default async function generateVulnerabilityData() { + const response = await fetch( + 'https://raw.githubusercontent.com/nodejs/security-wg/main/vuln/core/index.json' + ); + + const data = await response.json(); + + return groupVulnerabilitiesByMajor(Object.values(data)); +} diff --git a/apps/site/next-data/providers/vulnerabilities.ts b/apps/site/next-data/providers/vulnerabilities.ts new file mode 100644 index 0000000000000..cfc3cce008ba0 --- /dev/null +++ b/apps/site/next-data/providers/vulnerabilities.ts @@ -0,0 +1,9 @@ +import { cache } from 'react'; + +import generateVulnerabilities from '#site/next-data/generators/vulnerabilities.mjs'; + +const vulnerabilities = await generateVulnerabilities(); + +const provideVulnerabilities = cache(() => vulnerabilities); + +export default provideVulnerabilities; diff --git a/apps/site/next.mdx.use.mjs b/apps/site/next.mdx.use.mjs index 9942ac7f35e3e..15c37390ad5a9 100644 --- a/apps/site/next.mdx.use.mjs +++ b/apps/site/next.mdx.use.mjs @@ -3,6 +3,8 @@ import BadgeGroup from '@node-core/ui-components/Common/BadgeGroup'; import DownloadReleasesTable from './components/Downloads/DownloadReleasesTable'; +import EOLAlertBox from './components/EOL/Alert'; +import EOLTable from './components/EOL/Table'; import UpcomingMeetings from './components/MDX/Calendar/UpcomingMeetings'; import WithBadgeGroup from './components/withBadgeGroup'; import WithBanner from './components/withBanner'; @@ -25,4 +27,8 @@ export const mdxComponents = { BadgeGroup, // Renders an container for Upcoming Node.js Meetings UpcomingMeetings, + // Renders an EOL alert + EOLAlertBox, + // Renders the EOL Table + EOLTable, }; diff --git a/apps/site/pages/en/about/previous-releases.mdx b/apps/site/pages/en/about/previous-releases.mdx index 8ef856f11261a..909a5bd4cfbc3 100644 --- a/apps/site/pages/en/about/previous-releases.mdx +++ b/apps/site/pages/en/about/previous-releases.mdx @@ -1,10 +1,13 @@ --- title: Node.js Releases layout: about +modal: release --- # Node.js Releases + + Major Node.js versions enter _Current_ release status for six months, which gives library authors time to add support for them. After six months, odd-numbered releases (9, 11, etc.) become unsupported, and even-numbered releases (10, 12, etc.) move to _Active LTS_ status and are ready for general use. _LTS_ release status is "long-term support", which typically guarantees that critical bugs will be fixed for a total of 30 months. @@ -16,10 +19,6 @@ Production applications should only use _Active LTS_ or _Maintenance LTS_ releas Full details regarding the Node.js release schedule are available [on GitHub](https://github.com/nodejs/release#release-schedule). -### Commercial Support - -Commercial support for versions past the Maintenance phase is available through our OpenJS Ecosystem Sustainability Program partner [HeroDevs](https://www.herodevs.com/support/node-nes?utm_source=NodeJS+&utm_medium=Link&utm_campaign=Version_support_page). - ## Looking for the latest release of a version branch? diff --git a/apps/site/pages/en/eol.mdx b/apps/site/pages/en/eol.mdx new file mode 100644 index 0000000000000..27ace7d1637d4 --- /dev/null +++ b/apps/site/pages/en/eol.mdx @@ -0,0 +1,46 @@ +--- +title: End-Of-Life +layout: article +modal: eol +--- + +# End-Of-Life (EOL) + +## What is EOL Software? + +End-Of-Life software is software that is no longer maintained by its creators. Node.js has releases going back to 2015, and it's simply not feasible to maintain all release lines in perpetuity. Major versions are released, patched, and designated End-Of-Life on a schedule. + +[View the Node.js release schedule](/about/releases/). + +## Why Using EOL Software is Dangerous + +When a version reaches End-Of-Life, it means that it will no longer receive updates, including security patches. This can leave applications running on these versions vulnerable to security issues and bugs that will never be fixed. + +**End-Of-Life versions are dangerous. They are now completely unsupported**, meaning they receive no updates, including security patches. + +The security implications are immediate and serious. For example, when new security releases reveal issues and patches against major lines, the security advisory notes, "End-of-Life versions are always affected when a security release occurs", meaning **all earlier versions have these same vulnerabilities but will never receive patches**. + +## EOL Versions + + + +## Commercial Support + +We understand that some organizations face constraints that prevent immediate upgrades, such as legacy codebases, compliance requirements, or complex dependency chains. If your company cannot upgrade immediately but needs continued security support for End-Of-Life versions of Node.js, [**commercial support**](https://www.herodevs.com/support/node-nes?utm_source=NodeJS+&utm_medium=Link&utm_campaign=Nodejs_eol_support) **is available through HeroDevs**. + +As part of the [OpenJS Ecosystem Sustainability Program](https://openjsf.org/partners) partnership, HeroDevs provides Never-Ending Support (NES) for Node.js versions past their official maintenance phase. This includes security patches, compliance assistance, and technical support to help bridge the gap while you plan your upgrade strategy. + +However, this should be viewed as a temporary solution—the goal should always be to upgrade to actively supported versions. + +## Upgrade Today + +
+ + + + +
diff --git a/apps/site/pages/en/index.mdx b/apps/site/pages/en/index.mdx index 02f5c43bd78a2..14abd8323a71e 100644 --- a/apps/site/pages/en/index.mdx +++ b/apps/site/pages/en/index.mdx @@ -21,7 +21,7 @@ layout: home -