Skip to content

feat: add eol page #7990

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 55 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
02db91b
feat: add eol page
bmuenzenmeyer Jul 16, 2025
113334f
Update Modal.tsx
ovflowd Jul 21, 2025
6bc1393
expand and do not reuse i18n keys
bmuenzenmeyer Jul 22, 2025
967676d
add one more i18n key
bmuenzenmeyer Jul 22, 2025
5cd1b33
add footer to article layout
bmuenzenmeyer Jul 22, 2025
d2b7826
Update apps/site/components/EOL/Table.tsx
bmuenzenmeyer Jul 22, 2025
b294b44
cleanup unused classnames
bmuenzenmeyer Jul 22, 2025
4204159
simplify Table
bmuenzenmeyer Jul 22, 2025
2cadb0c
normalize endOfLife vs eol
bmuenzenmeyer Jul 22, 2025
55abd69
Apply suggestions from code review
bmuenzenmeyer Jul 22, 2025
1a4a53a
add back in the release schedule link
bmuenzenmeyer Jul 22, 2025
51c5fb0
small grammar / tense / capitalization changes
bmuenzenmeyer Jul 22, 2025
ea43096
Update apps/site/pages/en/eol.mdx
ovflowd Jul 23, 2025
d94d5ce
increase gap between vulnerability chips
bmuenzenmeyer Jul 24, 2025
1dccbd5
Merge branch 'main' into eol
ovflowd Jul 25, 2025
21014bd
make all translation strings long-form
bmuenzenmeyer Jul 28, 2025
ff6ddec
document translation key retrieval
bmuenzenmeyer Jul 28, 2025
9c68dd0
rename variable
bmuenzenmeyer Jul 28, 2025
f8cab5e
move CTAs up
bmuenzenmeyer Jul 28, 2025
2fbd30a
Merge branch 'main' into eol
ovflowd Jul 28, 2025
7babcc8
chore: button variants, and updated eol page; removed translated prev…
ovflowd Jul 28, 2025
07befbb
rename EOLModal/index per standard
bmuenzenmeyer Jul 29, 2025
594531f
rename components per docs and patterns
bmuenzenmeyer Jul 29, 2025
c4abb0e
chore: design improvements
ovflowd Jul 29, 2025
e471cb0
chore: tiny mobile improvement
ovflowd Jul 29, 2025
6b7bde7
chore: apply suggestions
ovflowd Jul 29, 2025
2a748fd
chore: apply text suggestions
ovflowd Jul 29, 2025
e0f7c44
chore: balance the buttons
ovflowd Jul 29, 2025
5ab2cbb
fix a11y issue on mdx rendering
bmuenzenmeyer Jul 29, 2025
26f5b81
chore: make it rain tm
ovflowd Jul 29, 2025
16f5992
Update vulnerabilities.mjs
avivkeller Jul 29, 2025
ccbba0f
apply aviv"s suggestions - manually added as the redirect also needed…
bmuenzenmeyer Jul 30, 2025
40df1ab
types and constants cleanup
bmuenzenmeyer Jul 30, 2025
f424d53
fix import
bmuenzenmeyer Jul 30, 2025
7724afb
more types cleanup
bmuenzenmeyer Jul 30, 2025
669d21d
one final type lint
bmuenzenmeyer Jul 30, 2025
04e4f5c
Merge branch 'main' into eol
bmuenzenmeyer Aug 1, 2025
89e5c92
move link below buttons
bmuenzenmeyer Aug 1, 2025
748e116
move URL to constants
bmuenzenmeyer Aug 2, 2025
9fe21cc
rename variable
bmuenzenmeyer Aug 2, 2025
4836bef
tighten up UnknownSeverity types
bmuenzenmeyer Aug 2, 2025
c147efc
format after refactor
bmuenzenmeyer Aug 2, 2025
847897b
memoize calls
bmuenzenmeyer Aug 2, 2025
d61bd30
simplify translation call
bmuenzenmeyer Aug 2, 2025
1a522c8
apply suggestion
bmuenzenmeyer Aug 2, 2025
dd10604
apply linter
bmuenzenmeyer Aug 2, 2025
fb31b90
refator vulnerability grouping, add unit tests
bmuenzenmeyer Aug 5, 2025
6957f21
avoid passing modal via frontmatter
avivkeller Aug 8, 2025
1c499dd
generify modal props
avivkeller Aug 9, 2025
1a7a80a
move checks into children components
avivkeller Aug 9, 2025
029ee7d
pass all vulns to children
avivkeller Aug 9, 2025
54fe024
fixup!
avivkeller Aug 9, 2025
9a8c6c7
no modal provider
avivkeller Aug 10, 2025
92cdd9a
rename ref to url
avivkeller Aug 10, 2025
f579c6f
fix type
avivkeller Aug 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions apps/site/components/Common/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import BaseButton, {
type ButtonProps,
} from '@node-core/ui-components/Common/BaseButton';
import BaseButton from '@node-core/ui-components/Common/BaseButton';
import type { ButtonProps } from '@node-core/ui-components/Common/BaseButton';
import type { FC, ComponentProps } from 'react';

import Link from '#site/components/Link';
Expand Down

This file was deleted.

61 changes: 40 additions & 21 deletions apps/site/components/Downloads/DownloadReleasesTable/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
'use client';

import Badge from '@node-core/ui-components/Common/Badge';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import type { FC } from 'react';

import FormattedTime from '#site/components/Common/FormattedTime';
import DetailsButton from '#site/components/Downloads/DownloadReleasesTable/DetailsButton';
import LinkWithArrow from '#site/components/LinkWithArrow';
import provideReleaseData from '#site/next-data/providers/releaseData';

import ReleaseModal from '../ReleaseModal';

const BADGE_KIND_MAP = {
'End-of-life': 'warning',
'Maintenance LTS': 'neutral',
Expand All @@ -18,8 +23,10 @@ const DownloadReleasesTable: FC = () => {
const releaseData = provideReleaseData();
const t = useTranslations();

const [currentModal, setCurrentModal] = useState<string | undefined>();

return (
<table id="tbVersions" className="download-table full-width">
<table id="tbVersions">
<thead>
<tr>
<th>{t('components.downloadReleasesTable.version')}</th>
Expand All @@ -32,25 +39,37 @@ const DownloadReleasesTable: FC = () => {
</thead>
<tbody>
{releaseData.map(release => (
<tr key={release.major}>
<td data-label="Version">v{release.major}</td>
<td data-label="LTS">{release.codename || '-'}</td>
<td data-label="Date">
<FormattedTime date={release.currentStart} />
</td>
<td data-label="Date">
<FormattedTime date={release.releaseDate} />
</td>
<td data-label="Status">
<Badge kind={BADGE_KIND_MAP[release.status]} size="small">
{release.status}
{release.status === 'End-of-life' ? ' (EoL)' : ''}
</Badge>
</td>
<td className="download-table-last">
<DetailsButton versionData={release} />
</td>
</tr>
<>
<tr key={release.major}>
<td data-label="Version">v{release.major}</td>
<td data-label="LTS">{release.codename || '-'}</td>
<td data-label="Date">
<FormattedTime date={release.currentStart} />
</td>
<td data-label="Date">
<FormattedTime date={release.releaseDate} />
</td>
<td data-label="Status">
<Badge kind={BADGE_KIND_MAP[release.status]} size="small">
{release.status}
{release.status === 'End-of-life' ? ' (EoL)' : ''}
</Badge>
</td>
<td>
<LinkWithArrow
className="cursor-pointer"
onClick={() => setCurrentModal(release.version)}
>
{t('components.downloadReleasesTable.details')}
</LinkWithArrow>
</td>
</tr>
<ReleaseModal
release={release}
open={currentModal === release.version}
onOpenChange={open => open || setCurrentModal(undefined)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the modalProps could just be onClose since we know when it is open or not ;)

So tha onClose becomes just a fn

/>
</>
))}
</tbody>
</table>
Expand Down
12 changes: 6 additions & 6 deletions apps/site/components/Downloads/MinorReleasesTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ type MinorReleasesTableProps = {
export const MinorReleasesTable: FC<MinorReleasesTableProps> = ({
releases,
}) => {
const t = useTranslations('components.minorReleasesTable');
const t = useTranslations();

return (
<table>
<thead>
<tr>
<th>{t('version')}</th>
<th>{t('links')}</th>
<th>{t('components.minorReleasesTable.version')}</th>
<th>{t('components.minorReleasesTable.links')}</th>
</tr>
</thead>
<tbody>
Expand All @@ -38,21 +38,21 @@ export const MinorReleasesTable: FC<MinorReleasesTableProps> = ({
kind="neutral"
href={`https://nodejs.org/download/release/v${release.version}/`}
>
{t('actions.release')}
{t('components.minorReleasesTable.actions.release')}
</Link>
<Separator orientation="vertical" />
<Link
kind="neutral"
href={`${BASE_CHANGELOG_URL}${release.version}`}
>
{t('actions.changelog')}
{t('components.minorReleasesTable.actions.changelog')}
</Link>
<Separator orientation="vertical" />
<Link
kind="neutral"
href={getNodeApiUrl(`v${release.version}`)}
>
{t('actions.docs')}
{t('components.minorReleasesTable.actions.docs')}
</Link>
</div>
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ const ReleaseCodeBox: FC = () => {
size="small"
>
{t.rich('layouts.download.codeBox.unsupportedVersionWarning', {
link: text => <Link href="/about/previous-releases/">{text}</Link>,
link: text => <Link href="/eol">{text}</Link>,
})}
</AlertBox>
)}
Expand Down
21 changes: 6 additions & 15 deletions apps/site/components/Downloads/ReleaseModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
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 { FC } from 'react';
import type { ComponentProps, 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 { NodeRelease } from '#site/types';

type ReleaseModalProps = {
isOpen: boolean;
closeModal: () => void;
type ReleaseModalProps = ComponentProps<typeof Modal> & {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still can have a shared prop for ModalProps fyi that accepts a generic ✨

release: NodeRelease;
};

const ReleaseModal: FC<ReleaseModalProps> = ({
isOpen,
closeModal,
release,
onOpenChange,
...props
}) => {
const t = useTranslations();

Expand All @@ -31,7 +29,7 @@ const ReleaseModal: FC<ReleaseModalProps> = ({
});

return (
<Modal open={isOpen} onOpenChange={closeModal}>
<Modal onOpenChange={onOpenChange} {...props}>
{release.status === 'End-of-life' && (
<div className="mb-4">
<AlertBox
Expand All @@ -40,14 +38,7 @@ const ReleaseModal: FC<ReleaseModalProps> = ({
size="small"
>
{t.rich('components.releaseModal.unsupportedVersionWarning', {
link: text => (
<Link
onClick={closeModal}
href="/about/previous-releases#release-schedule"
>
{text}
</Link>
),
link: text => <Link href="/eol">{text}</Link>,
})}
</AlertBox>
</div>
Expand Down
19 changes: 19 additions & 0 deletions apps/site/components/EOL/EOLAlert/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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();
return (
<AlertBox level="warning">
{t('components.eolAlert.intro')}{' '}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this whole thing could be an i18n string. And you pass the Link as a reference. Like we do with our rich text i18n strings ;)

<Link href="/eol">
OpenJS Ecosystem Sustainability Program{' '}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole thing should be an i18n string. Even more due to RTL languages.

{t('components.eolAlert.partner')} HeroDevs
</Link>
</AlertBox>
);
};

export default EOLAlert;
66 changes: 66 additions & 0 deletions apps/site/components/EOL/EOLModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Modal, Title, Content } from '@node-core/ui-components/Common/Modal';
import { useTranslations } from 'next-intl';
import { useMemo } from 'react';
import type { FC, ComponentProps } from 'react';

import KnownSeveritySection from '#site/components/EOL/KnownSeveritySection';
import UnknownSeveritySection from '#site/components/EOL/UnknownSeveritySection';
import { SEVERITY_ORDER } from '#site/next.constants.mjs';
import type { NodeRelease } from '#site/types/releases';
import type { Vulnerability } from '#site/types/vulnerabilities';

type EOLModalProps = ComponentProps<typeof Modal> & {
release: NodeRelease;
vulnerabilities: Array<Vulnerability>;
};

const EOLModal: FC<EOLModalProps> = ({
release,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we destruct this to:

Suggested change
release,
{ major, codename },

vulnerabilities,
...props
}) => {
const t = useTranslations();

const modalHeading = release.codename
? t('components.eolModal.title', {
Copy link
Member

@ovflowd ovflowd Aug 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which would allow us to do:

t('components.eolModal.title', { major, codename })

version: release.major,
codename: release.codename,
})
: t('components.eolModal.titleWithoutCodename', { version: release.major });

useMemo(
() =>
vulnerabilities.sort(
(a, b) =>
SEVERITY_ORDER.indexOf(a.severity) -
SEVERITY_ORDER.indexOf(b.severity)
),
[vulnerabilities]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[vulnerabilities]
[vulnerabilities.length]

Should be enough and less expensive

);

return (
<Modal {...props}>
<Title>{modalHeading}</Title>
<Content>
{vulnerabilities.length > 0 && (
<p className="m-1">
{t('components.eolModal.vulnerabilitiesMessage', {
count: vulnerabilities.length,
})}
</p>
)}

<KnownSeveritySection vulnerabilities={vulnerabilities} />
<UnknownSeveritySection vulnerabilities={vulnerabilities} />

{!vulnerabilities.length && (
<p className="m-1">
{t('components.eolModal.noVulnerabilitiesMessage')}
</p>
)}
</Content>
</Modal>
);
};

export default EOLModal;
78 changes: 78 additions & 0 deletions apps/site/components/EOL/EOLReleaseTable/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use client';
Copy link
Member

@ovflowd ovflowd Aug 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be a client component, does it really need to be? It forces the provdeVulnerabilities and provideReleaseData to run on client-side too.

+ Even if EOLModal is a client component, that's completely fine, as it will just slot in, afaik.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, that's due to useState.... 🤔

In this case, I wonder how this will behave. It will probably run these fetch() requests on the client-side, which we want to avoid.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we move'em to another sub-component, we'll have the same situation as of: React splitting the prop data into the client-side.

At the very least Next.js will do an initial hydration, so there won't be load times for users to see this. But these requests will definitely run every time they open this page....

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Yeah it is requesting on every load, although it does then cache and load from disk cache. My wonder is:

  • Knowing all is behind CDN, it shouldn't be an issue, and it fetches just in case the state is different, which would trigger a re-render
    • This could though create a situation of React complaining server content !== client content
    • But this should be fine due to skew protection and worst case, just re-renders.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thought here is, do we have other pages that are also running these fetch requests on client-side? During the development of this website, we made a conscious effort for all these requests to only be server-side, should we be fine with them being client-side? Instead of having mixed JSONs within the bundle?


import { useTranslations } from 'next-intl';
import { useState } from 'react';
import type { FC } from 'react';

import FormattedTime from '#site/components/Common/FormattedTime';
import VulnerabilityChips from '#site/components/EOL/VulnerabilityChips';
import LinkWithArrow from '#site/components/LinkWithArrow';
import provideReleaseData from '#site/next-data/providers/releaseData';
import provideVulnerabilities from '#site/next-data/providers/vulnerabilities';
import { EOL_VERSION_IDENTIFIER } from '#site/next.constants.mjs';

import EOLModal from '../EOLModal';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full import descriptors please 🙇


const EOLReleaseTable: FC = () => {
const releaseData = provideReleaseData();
const vulnerabilities = provideVulnerabilities();
const eolReleases = releaseData.filter(
release => release.status === EOL_VERSION_IDENTIFIER
);

const t = useTranslations();

const [currentModal, setCurrentModal] = useState<string | undefined>();

return (
<table id="tbVulnerabilities">
<thead>
<tr>
<th>
{t('components.eolTable.version')} (
{t('components.eolTable.codename')})
</th>
<th>{t('components.eolTable.lastUpdated')}</th>
<th>{t('components.eolTable.vulnerabilities')}</th>
<th>{t('components.eolTable.details')}</th>
</tr>
</thead>
<tbody>
{eolReleases.map(release => (
<>
<tr key={release.major}>
<td data-label="Version">
v{release.major}{' '}
{release.codename ? `(${release.codename})` : ''}
</td>
<td data-label="Date">
<FormattedTime date={release.releaseDate} />
</td>
<td>
<VulnerabilityChips
vulnerabilities={vulnerabilities[release.major]}
/>
</td>
<td>
<LinkWithArrow
className="cursor-pointer"
onClick={() => setCurrentModal(release.version)}
>
{t('components.downloadReleasesTable.details')}
</LinkWithArrow>
</td>
</tr>
<EOLModal
release={release}
vulnerabilities={vulnerabilities[release.major]}
open={currentModal === release.version}
onOpenChange={open => open || setCurrentModal(undefined)}
/>
</>
))}
</tbody>
</table>
);
};

export default EOLReleaseTable;
Loading
Loading