diff --git a/app/[locale]/next-data/changelog-data/[version]/route.ts b/app/[locale]/next-data/changelog-data/[version]/route.ts new file mode 100644 index 0000000000000..9184ee68af578 --- /dev/null +++ b/app/[locale]/next-data/changelog-data/[version]/route.ts @@ -0,0 +1,44 @@ +import { provideChangelogData } from '@/next-data/providers/changelogData'; +import provideReleaseData from '@/next-data/providers/releaseData'; +import { VERCEL_REVALIDATE } from '@/next.constants.mjs'; +import { defaultLocale } from '@/next.locales.mjs'; + +type StaticParams = { + params: { version: string }; +}; + +// This is the Route Handler for the `GET` method which handles the request +// for generating static data related to the Node.js Changelog Data +// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers +export const GET = async (_: Request, { params }: StaticParams) => { + const changelogData = await provideChangelogData(params.version); + + return Response.json(changelogData); +}; + +// This function generates the static paths that come from the dynamic segments +// `[locale]/next-data/changelog-data/[version]` and returns an array of all available static paths +// This is used for ISR static validation and generation +export const generateStaticParams = async () => { + const releases = provideReleaseData(); + + const mappedParams = releases.map(release => ({ + locale: defaultLocale.code, + version: String(release.versionWithPrefix), + })); + + return mappedParams; +}; + +// Enforces that only the paths from `generateStaticParams` are allowed, giving 404 on the contrary +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams +export const dynamicParams = false; + +// Enforces that this route is used as static rendering +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic +export const dynamic = 'error'; + +// 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 = VERCEL_REVALIDATE; diff --git a/components/Downloads/ChangelogModal/index.module.css b/components/Downloads/ChangelogModal/index.module.css index acc48d1e2807a..3f4f43dc8732d 100644 --- a/components/Downloads/ChangelogModal/index.module.css +++ b/components/Downloads/ChangelogModal/index.module.css @@ -3,11 +3,9 @@ inset-0 flex justify-center - bg-white - bg-opacity-90 - backdrop-blur-lg - dark:bg-neutral-950 - dark:bg-opacity-80; + bg-white/40 + backdrop-blur-[2px] + dark:bg-neutral-950/40; .content { @apply relative @@ -16,7 +14,7 @@ inline-flex w-full flex-col - overflow-y-scroll + overflow-y-auto rounded border border-neutral-200 @@ -24,9 +22,8 @@ p-8 focus:outline-none dark:bg-neutral-950 - sm:mt-20 - lg:w-2/3 - xl:w-3/5 + sm:my-20 + lg:max-w-[900px] xl:p-12 xs:p-6; } @@ -38,7 +35,11 @@ block size-6 cursor-pointer - sm:hidden; + rounded + focus:outline-none + focus:ring-2 + focus:ring-neutral-200 + dark:focus:ring-neutral-900; } .title { @@ -85,10 +86,6 @@ flex-col gap-4; - a { - @apply underline; - } - pre { @apply overflow-auto; } diff --git a/components/Downloads/ChangelogModal/index.stories.tsx b/components/Downloads/ChangelogModal/index.stories.tsx index 8fc593f93954d..a08e8bd299ee4 100644 --- a/components/Downloads/ChangelogModal/index.stories.tsx +++ b/components/Downloads/ChangelogModal/index.stories.tsx @@ -1,7 +1,6 @@ import type { Meta as MetaObj, StoryObj } from '@storybook/react'; import { VFile } from 'vfile'; -import Button from '@/components/Common/Button'; import ChangelogModal from '@/components/Downloads/ChangelogModal'; import { MDXRenderer } from '@/components/mdxRenderer'; import { compileMDX } from '@/next.mdx.compiler.mjs'; @@ -178,7 +177,7 @@ ZCVKLyezajjko28SugXGjegEjcY4o7v23XghhW6RAbEB6R8TZDo= export const Default: Story = { args: { - trigger: , + open: false, heading: 'Node v18.17.0', subheading: "2023-07-18, Version 18.17.0 'Hydrogen' (LTS), @danielleadams", avatars: names.map(name => ({ @@ -189,15 +188,15 @@ export const Default: Story = { }, render: (_, { loaded: { Content } }) => Content, loaders: [ - async ({ args }) => { + async ({ args: { children, ...props } }) => { const { MDXContent } = await compileMDX( - new VFile(args.children?.toString()), + new VFile(children?.toString()), 'md' ); return { Content: ( - +
diff --git a/components/Downloads/ChangelogModal/index.tsx b/components/Downloads/ChangelogModal/index.tsx index 939eded316523..3c2fe26a7cbec 100644 --- a/components/Downloads/ChangelogModal/index.tsx +++ b/components/Downloads/ChangelogModal/index.tsx @@ -1,51 +1,59 @@ +'use client'; + import { ArrowUpRightIcon, XMarkIcon } from '@heroicons/react/24/outline'; import * as Dialog from '@radix-ui/react-dialog'; import { useTranslations } from 'next-intl'; -import type { FC, PropsWithChildren, ReactNode, ComponentProps } from 'react'; +import type { FC, PropsWithChildren, ComponentProps } from 'react'; import AvatarGroup from '@/components/Common/AvatarGroup'; import Link from '@/components/Link'; import styles from './index.module.css'; -type ChangelogModalProps = { +type ChangelogModalProps = PropsWithChildren<{ heading: string; subheading: string; avatars: ComponentProps['avatars']; - trigger: ReactNode; - children: ReactNode; -}; + open?: boolean; + onOpenChange?: (open: boolean) => void; +}>; -const ChangelogModal: FC> = ({ +const ChangelogModal: FC = ({ heading, subheading, avatars, - trigger, children, + open = false, + onOpenChange = () => {}, }) => { const t = useTranslations(); return ( - - {trigger} + + {heading} + {subheading} +
+ {t('components.downloads.changelogModal.startContributing')}
+
{children}
+
diff --git a/components/Downloads/DownloadReleasesTable.tsx b/components/Downloads/DownloadReleasesTable.tsx index 9c6d775623c0f..306b6ab29df49 100644 --- a/components/Downloads/DownloadReleasesTable.tsx +++ b/components/Downloads/DownloadReleasesTable.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react'; import getReleaseData from '@/next-data/releaseData'; import { getNodeApiLink } from '@/util/getNodeApiLink'; -import { getNodejsChangelog } from '@/util/getNodeJsChangelog'; +import { getNodeJsChangelog } from '@/util/getNodeJsChangelog'; // This is a React Async Server Component // Note that Hooks cannot be used in a RSC async component @@ -38,7 +38,7 @@ const DownloadReleasesTable: FC = async () => { > {t('components.downloadReleasesTable.releases')} - + {t('components.downloadReleasesTable.changelog')} diff --git a/components/Downloads/Release/ChangelogLink.tsx b/components/Downloads/Release/ChangelogLink.tsx new file mode 100644 index 0000000000000..50dcaccd564a7 --- /dev/null +++ b/components/Downloads/Release/ChangelogLink.tsx @@ -0,0 +1,19 @@ +'use client'; + +import type { FC, PropsWithChildren } from 'react'; +import { useContext } from 'react'; + +import LinkWithArrow from '@/components/Downloads/Release/LinkWithArrow'; +import { ReleaseContext } from '@/providers/releaseProvider'; + +const ChangelogLink: FC = ({ children }) => { + const { modalOpen, setModalOpen } = useContext(ReleaseContext); + + return ( + + ); +}; + +export default ChangelogLink; diff --git a/components/Downloads/Release/LinkWithArrow.tsx b/components/Downloads/Release/LinkWithArrow.tsx index 46a8ad69604e2..ca99c630dea26 100644 --- a/components/Downloads/Release/LinkWithArrow.tsx +++ b/components/Downloads/Release/LinkWithArrow.tsx @@ -1,11 +1,9 @@ import { ArrowUpRightIcon } from '@heroicons/react/24/solid'; -import type { FC, PropsWithChildren } from 'react'; +import type { ComponentProps, FC } from 'react'; import Link from '@/components/Link'; -type AccessibleAnchorProps = { href?: string }; - -const LinkWithArrow: FC> = ({ +const LinkWithArrow: FC> = ({ children, ...props }) => ( @@ -14,4 +12,5 @@ const LinkWithArrow: FC> = ({ ); + export default LinkWithArrow; diff --git a/components/mdxRenderer.tsx b/components/mdxRenderer.tsx index 79d312270c087..abf9b4315e97e 100644 --- a/components/mdxRenderer.tsx +++ b/components/mdxRenderer.tsx @@ -1,11 +1,13 @@ import type { MDXComponents, MDXContent } from 'mdx/types'; import type { FC } from 'react'; -import { htmlComponents, mdxComponents } from '@/next.mdx.use.mjs'; +import { htmlComponents, clientMdxComponents } from '@/next.mdx.use.client.mjs'; +import { mdxComponents } from '@/next.mdx.use.mjs'; // Combine all MDX Components to be used const combinedComponents: MDXComponents = { ...htmlComponents, + ...clientMdxComponents, ...mdxComponents, }; diff --git a/components/withChangelogModal.tsx b/components/withChangelogModal.tsx new file mode 100644 index 0000000000000..8c6700bdbbd28 --- /dev/null +++ b/components/withChangelogModal.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import type { FC, ReactElement } from 'react'; +import { VFile } from 'vfile'; + +import ChangelogModal from '@/components/Downloads/ChangelogModal'; +import changelogData from '@/next-data/changelogData'; +import { compileMDX } from '@/next.mdx.compiler.mjs'; +import { clientMdxComponents, htmlComponents } from '@/next.mdx.use.client.mjs'; +import type { NodeRelease } from '@/types'; +import { + getNodeJsChangelogAuthor, + getNodeJsChangelogSlug, +} from '@/util/getNodeJsChangelog'; +import { getGitHubAvatarUrl } from '@/util/gitHubUtils'; + +type WithChangelogModalProps = { + release: NodeRelease; + modalOpen: boolean; + setModalOpen: (open: boolean) => void; +}; + +// We only need the base components for our ChangelogModal, this avoids/eliminates +// the need of Next.js bundling on the client-side all our MDX components +// Note that this already might increase the client-side bundle due to Shiki +const clientComponents = { ...clientMdxComponents, ...htmlComponents }; + +const WithChangelogModal: FC = ({ + release: { versionWithPrefix }, + modalOpen, + setModalOpen, +}) => { + const [ChangelogMDX, setChangelogMDX] = useState(); + const [changelog, setChangelog] = useState(''); + + useEffect(() => { + let isCancelled = false; + + const fetchChangelog = async () => { + try { + const data = await changelogData(versionWithPrefix); + + // We need to check if the component is still mounted before setting the state + if (!isCancelled) { + setChangelog(data); + + // This removes the

header from the changelog content, as we already + // render the changelog heading as the "ChangelogModal" subheading + const changelogWithoutHeader = data.split('\n').slice(2).join('\n'); + + compileMDX(new VFile(changelogWithoutHeader), 'md').then( + ({ MDXContent }) => { + // This is a tricky one. React states does not allow you to actually store React components + // hence we need to render the component within an Effect and set the state as a ReactElement + // which is a function that can be eval'd by React during runtime. + const renderedElement = ( + + ); + + setChangelogMDX(renderedElement); + } + ); + } + } catch (_) { + throw new Error(`Failed to fetch changelog for, ${versionWithPrefix}`); + } + }; + + if (modalOpen && versionWithPrefix) { + fetchChangelog(); + } + + return () => { + isCancelled = true; + }; + }, [modalOpen, versionWithPrefix]); + + const author = getNodeJsChangelogAuthor(changelog); + const slug = getNodeJsChangelogSlug(changelog); + + const modalProps = { + heading: `Node.js ${versionWithPrefix}`, + avatars: [{ src: getGitHubAvatarUrl(author), alt: author }], + subheading: slug, + open: modalOpen && typeof ChangelogMDX !== 'undefined', + onOpenChange: setModalOpen, + }; + + return ( + +
{ChangelogMDX}
+
+ ); +}; + +export default WithChangelogModal; diff --git a/components/withDownloadCategories.tsx b/components/withDownloadCategories.tsx index 331a508649f16..f6047ea20bdd9 100644 --- a/components/withDownloadCategories.tsx +++ b/components/withDownloadCategories.tsx @@ -1,15 +1,14 @@ import { getTranslations } from 'next-intl/server'; import type { FC, PropsWithChildren } from 'react'; +import LinkTabs from '@/components/Common/LinkTabs'; +import WithNodeRelease from '@/components/withNodeRelease'; import { useClientContext } from '@/hooks/react-server'; import getReleaseData from '@/next-data/releaseData'; import { ReleaseProvider } from '@/providers/releaseProvider'; import type { NodeReleaseStatus } from '@/types'; import { getDownloadCategory, mapCategoriesToTabs } from '@/util/downloadUtils'; -import LinkTabs from './Common/LinkTabs'; -import WithNodeRelease from './withNodeRelease'; - const WithDownloadCategories: FC = async ({ children }) => { const t = await getTranslations(); const releases = await getReleaseData(); diff --git a/global.ts b/global.ts index f413cd85f5e22..58a0dbb28cd11 100644 --- a/global.ts +++ b/global.ts @@ -1,5 +1,7 @@ +import type { clientMdxComponents } from '@/next.mdx.use.client.mjs'; import type { mdxComponents } from '@/next.mdx.use.mjs'; declare global { - type MDXProvidedComponents = typeof mdxComponents; + type MDXProvidedComponents = typeof mdxComponents & + typeof clientMdxComponents; } diff --git a/next-data/changelogData.ts b/next-data/changelogData.ts new file mode 100644 index 0000000000000..1d3db6f8f18bf --- /dev/null +++ b/next-data/changelogData.ts @@ -0,0 +1,25 @@ +import { + ENABLE_STATIC_EXPORT, + IS_DEVELOPMENT, + NEXT_DATA_URL, + VERCEL_ENV, +} from '@/next.constants.mjs'; + +const getChangelogData = (version: string): Promise => { + // When we're using Static Exports the Next.js Server is not running (during build-time) + // hence the self-ingestion APIs will not be available. In this case we want to load + // the data directly within the current thread, which will anyways be loaded only once + // We use lazy-imports to prevent `provideChangelogData` from executing on import + if (ENABLE_STATIC_EXPORT || (!IS_DEVELOPMENT && !VERCEL_ENV)) { + return import('@/next-data/providers/changelogData').then( + ({ provideChangelogData }) => provideChangelogData(version) + ); + } + + // When we're on RSC with Server capabilities we prefer using Next.js Data Fetching + // as this will load cached data from the server instead of generating data on the fly + // this is extremely useful for ISR and SSG as it will not generate this data on every request + return fetch(`${NEXT_DATA_URL}changelog-data/${version}`).then(r => r.json()); +}; + +export default getChangelogData; diff --git a/next-data/generators/changelogData.mjs b/next-data/generators/changelogData.mjs new file mode 100644 index 0000000000000..f0cb12f3dac75 --- /dev/null +++ b/next-data/generators/changelogData.mjs @@ -0,0 +1,18 @@ +'use strict'; + +import { fetchNodeJsChangelog } from '@/util/fetchNodeJsChangelog'; + +/** + * This method is used to generate the Node.js Changelog Data + * for self-consumption during RSC and Static Builds + * + * @returns {Promise} + */ +const generateChangelogData = async version => { + // Get the raw changelog for the latest minor for a given major + const changelog = await fetchNodeJsChangelog(version); + + return changelog; +}; + +export default generateChangelogData; diff --git a/next-data/generators/releaseData.mjs b/next-data/generators/releaseData.mjs index 8162fbdcdf983..cd34ecf3e4ef1 100644 --- a/next-data/generators/releaseData.mjs +++ b/next-data/generators/releaseData.mjs @@ -37,32 +37,35 @@ const generateReleaseData = () => { // Basically those not in schedule.json const majors = Object.values(nodevuOutput).filter(major => !!major.support); - const nodeReleases = majors.map(major => { - const [latestVersion] = Object.values(major.releases); + const nodeReleases = Promise.all( + majors.map(async major => { + const [latestVersion] = Object.values(major.releases); - const support = { - currentStart: major.support.phases.dates.start, - ltsStart: major.support.phases.dates.lts, - maintenanceStart: major.support.phases.dates.maintenance, - endOfLife: major.support.phases.dates.end, - }; + const support = { + currentStart: major.support.phases.dates.start, + ltsStart: major.support.phases.dates.lts, + maintenanceStart: major.support.phases.dates.maintenance, + endOfLife: major.support.phases.dates.end, + }; - const status = getNodeReleaseStatus(new Date(), support); + // Get the major release status based on our Release Schedule + const status = getNodeReleaseStatus(new Date(), support); - return { - ...support, - status, - major: latestVersion.semver.major, - version: latestVersion.semver.raw, - versionWithPrefix: `v${latestVersion.semver.raw}`, - codename: major.support.codename || '', - isLts: status === 'Active LTS' || status === 'Maintenance LTS', - npm: latestVersion.dependencies.npm || '', - v8: latestVersion.dependencies.v8 || '', - releaseDate: latestVersion.releaseDate || '', - modules: latestVersion.modules.version || '', - }; - }); + return { + ...support, + status, + major: latestVersion.semver.major, + version: latestVersion.semver.raw, + versionWithPrefix: `v${latestVersion.semver.raw}`, + codename: major.support.codename || '', + isLts: status === 'Active LTS' || status === 'Maintenance LTS', + npm: latestVersion.dependencies.npm || '', + v8: latestVersion.dependencies.v8 || '', + releaseDate: latestVersion.releaseDate || '', + modules: latestVersion.modules.version || '', + }; + }) + ); // nodevu returns duplicated v0.x versions (v0.12, v0.10, ...). // This behavior seems intentional as the case is hardcoded in nodevu, @@ -70,7 +73,9 @@ const generateReleaseData = () => { // 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. - return nodeReleases.filter(r => r.major !== 0 || r.version === '0.12.18'); + return nodeReleases.then(releases => + releases.filter(r => r.major !== 0 || r.version === '0.12.18') + ); }); }; diff --git a/next-data/providers/changelogData.ts b/next-data/providers/changelogData.ts new file mode 100644 index 0000000000000..8a07bab07fd2e --- /dev/null +++ b/next-data/providers/changelogData.ts @@ -0,0 +1,9 @@ +import { cache } from 'react'; + +import generateChangelogData from '@/next-data/generators/changelogData.mjs'; + +export const provideChangelogData = cache((version: string) => { + const changelog = generateChangelogData(version); + + return changelog; +}); diff --git a/next.mdx.compiler.mjs b/next.mdx.compiler.mjs index 1bc2ebc96f83f..6fcdf963cdd39 100644 --- a/next.mdx.compiler.mjs +++ b/next.mdx.compiler.mjs @@ -35,7 +35,6 @@ export async function compileMDX(source, fileExtension) { rehypePlugins: NEXT_REHYPE_PLUGINS, remarkPlugins: NEXT_REMARK_PLUGINS, format: fileExtension, - baseUrl: import.meta.url, ...reactRuntime, }); diff --git a/next.mdx.shiki.mjs b/next.mdx.shiki.mjs index aeeb67254bebf..b3253f6646cfc 100644 --- a/next.mdx.shiki.mjs +++ b/next.mdx.shiki.mjs @@ -10,11 +10,6 @@ import { getShiki, highlightToHast } from './util/getHighlighter'; // to attribute the current language of the
 element
 const languagePrefix = 'language-';
 
-// We do a top-level await, since the Unist-tree visitor
-// is synchronous, and it makes more sense to do a top-level
-// await, rather than an await inside the visitor function
-const memoizedShiki = await getShiki();
-
 /**
  * Retrieve the value for the given meta key.
  *
@@ -60,6 +55,11 @@ function isCodeBlock(node) {
 
 export default function rehypeShikiji() {
   return async function (tree) {
+    // We do a top-level await, since the Unist-tree visitor
+    // is synchronous, and it makes more sense to do a top-level
+    // await, rather than an await inside the visitor function
+    const memoizedShiki = await getShiki();
+
     visit(tree, 'element', (_, index, parent) => {
       const languages = [];
       const displayNames = [];
diff --git a/next.mdx.use.client.mjs b/next.mdx.use.client.mjs
new file mode 100644
index 0000000000000..9c75bf92355ff
--- /dev/null
+++ b/next.mdx.use.client.mjs
@@ -0,0 +1,39 @@
+'use strict';
+
+import Blockquote from './components/Common/Blockquote';
+import Button from './components/Common/Button';
+import LinkWithArrow from './components/Downloads/Release/LinkWithArrow';
+import Link from './components/Link';
+import MDXCodeBox from './components/MDX/CodeBox';
+import MDXCodeTabs from './components/MDX/CodeTabs';
+import MDXImage from './components/MDX/Image';
+
+/**
+ * A full list of React Components that we want to pass through to MDX
+ *
+ * @satisfies {import('mdx/types').MDXComponents}
+ */
+export const clientMdxComponents = {
+  // Renders MDX CodeTabs
+  CodeTabs: MDXCodeTabs,
+  // Renders a Button Component for `button` tags
+  Button: Button,
+  // Links with External Arrow
+  LinkWithArrow: LinkWithArrow,
+};
+
+/**
+ * A full list of wired HTML elements into custom React Components
+ *
+ * @type {import('mdx/types').MDXComponents}
+ */
+export const htmlComponents = {
+  // Renders a Link Component for `a` tags
+  a: Link,
+  // Renders a Blockquote Component for `blockquote` tags
+  blockquote: Blockquote,
+  // Renders a CodeBox Component for `pre` tags
+  pre: MDXCodeBox,
+  // Renders an Image Component for `img` tags
+  img: MDXImage,
+};
diff --git a/next.mdx.use.mjs b/next.mdx.use.mjs
index cc27b12efffbd..54191ff55b952 100644
--- a/next.mdx.use.mjs
+++ b/next.mdx.use.mjs
@@ -1,14 +1,12 @@
 'use strict';
 
-import Blockquote from './components/Common/Blockquote';
-import Button from './components/Common/Button';
 import DownloadButton from './components/Downloads/DownloadButton';
 import DownloadLink from './components/Downloads/DownloadLink';
 import DownloadReleasesTable from './components/Downloads/DownloadReleasesTable';
 import BitnessDropdown from './components/Downloads/Release/BitnessDropdown';
 import BlogPostLink from './components/Downloads/Release/BlogPostLink';
+import ChangelogLink from './components/Downloads/Release/ChangelogLink';
 import ReleaseDownloadButton from './components/Downloads/Release/DownloadButton';
-import LinkWithArrow from './components/Downloads/Release/LinkWithArrow';
 import NpmLink from './components/Downloads/Release/NpmLink';
 import OperatingSystemDropdown from './components/Downloads/Release/OperatingSystemDropdown';
 import PlatformDropdown from './components/Downloads/Release/PlatformDropdown';
@@ -18,11 +16,7 @@ import ReleaseVersion from './components/Downloads/Release/ReleaseVersion';
 import SourceButton from './components/Downloads/Release/SourceButton';
 import VerifyingBinariesLink from './components/Downloads/Release/VerifyingBinariesLink';
 import VersionDropdown from './components/Downloads/Release/VersionDropdown';
-import Link from './components/Link';
 import UpcomingMeetings from './components/MDX/Calendar/UpcomingMeetings';
-import MDXCodeBox from './components/MDX/CodeBox';
-import MDXCodeTabs from './components/MDX/CodeTabs';
-import MDXImage from './components/MDX/Image';
 import SearchPage from './components/MDX/SearchPage';
 import WithBadge from './components/withBadge';
 import WithBanner from './components/withBanner';
@@ -41,20 +35,14 @@ export const mdxComponents = {
   WithBanner: WithBanner,
   // HOC for providing Badge Data
   WithBadge: WithBadge,
-  // Renders MDX CodeTabs
-  CodeTabs: MDXCodeTabs,
   // Renders a Download Button
   DownloadButton: DownloadButton,
   // Renders a Download Link
   DownloadLink: DownloadLink,
-  // Renders a Button Component for `button` tags
-  Button: Button,
   // Renders a Search Page
   SearchPage: SearchPage,
   // Renders an container for Upcoming Node.js Meetings
   UpcomingMeetings: UpcomingMeetings,
-  // Links with External Arrow
-  LinkWithArrow: LinkWithArrow,
   // Group of components that enable you to select versions for Node.js
   // releases and download selected versions. Uses `releaseProvider` as a provider
   Release: {
@@ -82,21 +70,7 @@ export const mdxComponents = {
     SourceButton: SourceButton,
     // Renders a Release CodeBox
     ReleaseCodeBox: ReleaseCodeBox,
+    // Renders a Changelog Modal Link Button
+    ChangelogLink: ChangelogLink,
   },
 };
-
-/**
- * A full list of wired HTML elements into custom React Components
- *
- * @type {import('mdx/types').MDXComponents}
- */
-export const htmlComponents = {
-  // Renders a Link Component for `a` tags
-  a: Link,
-  // Renders a Blockquote Component for `blockquote` tags
-  blockquote: Blockquote,
-  // Renders a CodeBox Component for `pre` tags
-  pre: MDXCodeBox,
-  // Renders an Image Component for `img` tags
-  img: MDXImage,
-};
diff --git a/pages/en/download/current.mdx b/pages/en/download/current.mdx
index 637327b30099e..764ed0c3ee56e 100644
--- a/pages/en/download/current.mdx
+++ b/pages/en/download/current.mdx
@@ -13,6 +13,8 @@ I want the  version of Node.js for 
 Node.js includes .
 
+Read the changelog for this version
+
 Read the blog post for this version
 
 Learn how to verify signed SHASUMS
diff --git a/pages/en/download/index.mdx b/pages/en/download/index.mdx
index 637327b30099e..764ed0c3ee56e 100644
--- a/pages/en/download/index.mdx
+++ b/pages/en/download/index.mdx
@@ -13,6 +13,8 @@ I want the  version of Node.js for 
 Node.js includes .
 
+Read the changelog for this version
+
 Read the blog post for this version
 
 Learn how to verify signed SHASUMS
diff --git a/pages/en/download/package-manager/current.mdx b/pages/en/download/package-manager/current.mdx
index 7b970af146d64..b1df735f28000 100644
--- a/pages/en/download/package-manager/current.mdx
+++ b/pages/en/download/package-manager/current.mdx
@@ -13,6 +13,8 @@ Install Node.js  on  
 Node.js includes .
 
+Read the changelog for this version
+
 Read the blog post for this version
 
 Learn how to verify signed SHASUMS
diff --git a/pages/en/download/package-manager/index.mdx b/pages/en/download/package-manager/index.mdx
index 87f714dffbbba..53cef9690e467 100644
--- a/pages/en/download/package-manager/index.mdx
+++ b/pages/en/download/package-manager/index.mdx
@@ -13,6 +13,8 @@ Install Node.js  on 
 Node.js includes .
 
+Read the changelog for this version
+
 Read the blog post for this version
 
 Learn how to verify signed SHASUMS
diff --git a/pages/en/download/prebuilt-binaries/current.mdx b/pages/en/download/prebuilt-binaries/current.mdx
index c3138a51457ac..c101cf05ac441 100644
--- a/pages/en/download/prebuilt-binaries/current.mdx
+++ b/pages/en/download/prebuilt-binaries/current.mdx
@@ -13,6 +13,8 @@ I want the  version of Node.js for 
 Node.js includes .
 
+Read the changelog for this version
+
 Read the blog post for this version
 
 Learn how to verify signed SHASUMS
diff --git a/pages/en/download/prebuilt-binaries/index.mdx b/pages/en/download/prebuilt-binaries/index.mdx
index c3138a51457ac..c101cf05ac441 100644
--- a/pages/en/download/prebuilt-binaries/index.mdx
+++ b/pages/en/download/prebuilt-binaries/index.mdx
@@ -13,6 +13,8 @@ I want the  version of Node.js for 
 Node.js includes .
 
+Read the changelog for this version
+
 Read the blog post for this version
 
 Learn how to verify signed SHASUMS
diff --git a/pages/en/download/source-code/current.mdx b/pages/en/download/source-code/current.mdx
index 75dd8002f3a7f..0732ee94ce4fb 100644
--- a/pages/en/download/source-code/current.mdx
+++ b/pages/en/download/source-code/current.mdx
@@ -13,6 +13,8 @@ I want the  version of the Node.js source code.
 
Node.js includes . +Read the changelog for this version + Read the blog post for this version Learn how to verify signed SHASUMS diff --git a/pages/en/download/source-code/index.mdx b/pages/en/download/source-code/index.mdx index 75dd8002f3a7f..0732ee94ce4fb 100644 --- a/pages/en/download/source-code/index.mdx +++ b/pages/en/download/source-code/index.mdx @@ -13,6 +13,8 @@ I want the version of the Node.js source code.
Node.js includes . +Read the changelog for this version + Read the blog post for this version Learn how to verify signed SHASUMS diff --git a/providers/releaseProvider.tsx b/providers/releaseProvider.tsx index c17e8748af4cd..10668654937eb 100644 --- a/providers/releaseProvider.tsx +++ b/providers/releaseProvider.tsx @@ -3,6 +3,7 @@ import type { Dispatch, PropsWithChildren, FC } from 'react'; import { createContext, useMemo, useReducer } from 'react'; +import WithChangelogModal from '@/components/withChangelogModal'; import type { NodeRelease } from '@/types'; import type { ReleaseDispatchActions, @@ -18,6 +19,7 @@ const initialState: ReleaseState = { os: 'OTHER', bitness: '', platform: 'NVM', + modalOpen: false, }; const createDispatchActions = ( @@ -27,6 +29,7 @@ const createDispatchActions = ( setOS: payload => dispatch({ type: 'SET_OS', payload }), setBitness: payload => dispatch({ type: 'SET_BITNESS', payload }), setPlatform: payload => dispatch({ type: 'SET_PLATFORM', payload }), + setModalOpen: payload => dispatch({ type: 'SET_MODAL_OPEN', payload }), }); export const ReleaseContext = createContext({ @@ -53,6 +56,8 @@ export const ReleaseProvider: FC> = ({ return { ...state, bitness: action.payload }; case 'SET_PLATFORM': return { ...state, platform: action.payload }; + case 'SET_MODAL_OPEN': + return { ...state, modalOpen: action.payload }; default: return state; } @@ -69,6 +74,12 @@ export const ReleaseProvider: FC> = ({ return ( {children} + + ); }; diff --git a/types/release.ts b/types/release.ts index 4cbe96bc8fd04..e231e55a921e7 100644 --- a/types/release.ts +++ b/types/release.ts @@ -11,19 +11,22 @@ export interface ReleaseState { releases: Array; bitness: string | number; platform: PackageManager; + modalOpen: boolean; } export type ReleaseAction = | { type: 'SET_OS'; payload: UserOS } | { type: 'SET_VERSION'; payload: string } | { type: 'SET_BITNESS'; payload: string | number } - | { type: 'SET_PLATFORM'; payload: PackageManager }; + | { type: 'SET_PLATFORM'; payload: PackageManager } + | { type: 'SET_MODAL_OPEN'; payload: boolean }; export interface ReleaseDispatchActions { setVersion: (version: string) => void; setOS: (os: UserOS) => void; setBitness: (bitness: string | number) => void; setPlatform: (platform: PackageManager) => void; + setModalOpen: (open: boolean) => void; } export interface ReleaseContextType diff --git a/util/__tests__/getNodeJsChangelog.test.mjs b/util/__tests__/getNodeJsChangelog.test.mjs index a5f76e633b140..5cc56521750b2 100644 --- a/util/__tests__/getNodeJsChangelog.test.mjs +++ b/util/__tests__/getNodeJsChangelog.test.mjs @@ -1,12 +1,12 @@ -import { getNodejsChangelog } from '../getNodeJsChangelog'; +import { getNodeJsChangelog } from '../getNodeJsChangelog'; -describe('getNodejsChangelog', () => { +describe('getNodeJsChangelog', () => { it('returns the correct changelog URL for major version >= 4', () => { const version = '14.2.0'; const expectedUrl = 'https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V14.md#14.2.0'; - const result = getNodejsChangelog(version); + const result = getNodeJsChangelog(version); expect(result).toBe(expectedUrl); }); @@ -16,7 +16,7 @@ describe('getNodejsChangelog', () => { const expectedUrl = 'https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_IOJS.md#1.8.3'; - const result = getNodejsChangelog(version); + const result = getNodeJsChangelog(version); expect(result).toBe(expectedUrl); }); @@ -26,7 +26,7 @@ describe('getNodejsChangelog', () => { const expectedUrl1 = 'https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V6.md#6.12.0'; - const result1 = getNodejsChangelog(version1); + const result1 = getNodeJsChangelog(version1); expect(result1).toBe(expectedUrl1); @@ -34,7 +34,7 @@ describe('getNodejsChangelog', () => { const expectedUrl2 = 'https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V8.md#8.10.0'; - const result2 = getNodejsChangelog(version2); + const result2 = getNodeJsChangelog(version2); expect(result2).toBe(expectedUrl2); }); @@ -44,7 +44,7 @@ describe('getNodejsChangelog', () => { const expectedUrl = 'https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V012.md#0.12.7'; - const result = getNodejsChangelog(version); + const result = getNodeJsChangelog(version); expect(result).toBe(expectedUrl); }); diff --git a/util/fetchNodeJsChangelog.ts b/util/fetchNodeJsChangelog.ts new file mode 100644 index 0000000000000..eea20bb3ab878 --- /dev/null +++ b/util/fetchNodeJsChangelog.ts @@ -0,0 +1,24 @@ +import { getNodeJsChangelog } from './getNodeJsChangelog'; + +const getChangelogSectionRegex = (sectionId: string) => + new RegExp(`\\n([\\s\\S]+?)(?:\\n