From 6df8fb5839a6259f2e8a4e92f7222056ee5eac96 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Thu, 26 Dec 2024 21:28:15 +0000 Subject: [PATCH] chore: improved types --- .../blog-data/[category]/[page]/route.ts | 7 +++- .../components/Common/BlogPostCard/index.tsx | 3 +- .../components/Containers/MetaBar/index.tsx | 4 +- .../Downloads/Release/ReleaseCodeBox.tsx | 31 +++++++------- apps/site/components/withBlogCrossLinks.tsx | 3 +- apps/site/global.d.ts | 29 +++++++++++++ .../hooks/react-generic/useSiteNavigation.ts | 2 +- apps/site/layouts/Blog.tsx | 8 +++- apps/site/next-data/blogData.ts | 7 +++- apps/site/next-data/providers/blogData.ts | 42 ++++++++++--------- apps/site/tsconfig.json | 1 + apps/site/types/blog.ts | 5 ++- apps/site/types/navigation.ts | 4 +- apps/site/util/downloadUtils.tsx | 36 ++++++++++------ packages/i18n/locales/en.json | 9 ++-- 15 files changed, 123 insertions(+), 68 deletions(-) create mode 100644 apps/site/global.d.ts diff --git a/apps/site/app/[locale]/next-data/blog-data/[category]/[page]/route.ts b/apps/site/app/[locale]/next-data/blog-data/[category]/[page]/route.ts index c0061c6a366ad..ed7977b3d6252 100644 --- a/apps/site/app/[locale]/next-data/blog-data/[category]/[page]/route.ts +++ b/apps/site/app/[locale]/next-data/blog-data/[category]/[page]/route.ts @@ -3,8 +3,13 @@ import { providePaginatedBlogPosts, } from '@/next-data/providers/blogData'; import { defaultLocale } from '@/next.locales.mjs'; +import type { BlogCategory } from '@/types'; -type DynamicStaticPaths = { locale: string; category: string; page: string }; +type DynamicStaticPaths = { + locale: string; + category: BlogCategory; + page: string; +}; type StaticParams = { params: Promise }; // This is the Route Handler for the `GET` method which handles the request diff --git a/apps/site/components/Common/BlogPostCard/index.tsx b/apps/site/components/Common/BlogPostCard/index.tsx index 4f8696dd20565..30815ad9ed62c 100644 --- a/apps/site/components/Common/BlogPostCard/index.tsx +++ b/apps/site/components/Common/BlogPostCard/index.tsx @@ -5,13 +5,14 @@ import FormattedTime from '@/components/Common/FormattedTime'; import Preview from '@/components/Common/Preview'; import Link from '@/components/Link'; import WithAvatarGroup from '@/components/withAvatarGroup'; +import type { BlogCategory } from '@/types'; import { mapBlogCategoryToPreviewType } from '@/util/blogUtils'; import styles from './index.module.css'; type BlogPostCardProps = { title: string; - category: string; + category: BlogCategory; description?: string; authors?: Array; date?: Date; diff --git a/apps/site/components/Containers/MetaBar/index.tsx b/apps/site/components/Containers/MetaBar/index.tsx index b4bc0fccf70f8..febcc88bf3e60 100644 --- a/apps/site/components/Containers/MetaBar/index.tsx +++ b/apps/site/components/Containers/MetaBar/index.tsx @@ -8,7 +8,7 @@ import Link from '@/components/Link'; import styles from './index.module.css'; type MetaBarProps = { - items: Record; + items: Partial>; headings?: { items: Array; minDepth?: number; @@ -33,7 +33,7 @@ const MetaBar: FC = ({ items, headings }) => { .filter(([, value]) => !!value) .map(([key, value]) => ( -
{t(key)}
+
{t(key as IntlMessageKeys)}
{value}
))} diff --git a/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx b/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx index 838a0657423bd..d1588ce85be86 100644 --- a/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx +++ b/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx @@ -70,6 +70,13 @@ const ReleaseCodeBox: FC = () => { // Determines if the code box should render the skeleton loader const renderSkeleton = os === 'LOADING' || platform === ''; + // Defines fallbacks for the currentPlatform object + const { + label = '', + url = '', + info = 'layouts.download.codeBox.platformInfo.default', + } = currentPlatform ?? {}; + return (
{release.status === 'End-of-life' && ( @@ -78,11 +85,11 @@ const ReleaseCodeBox: FC = () => { )} - + {!currentPlatform || currentPlatform.recommended || ( {t('layouts.download.codeBox.communityPlatformInfo')} - + )} @@ -92,22 +99,12 @@ const ReleaseCodeBox: FC = () => { - {t( - currentPlatform?.bottomInfo ?? - 'layouts.download.codeBox.platformInfo.default', - { platform: currentPlatform?.label as string } - )} - - -
- - + {t(info, { platform: label })}{' '} {t.rich('layouts.download.codeBox.externalSupportInfo', { - platform: currentPlatform?.label as string, - b: chunks => {chunks}, - link: chunks => ( - - {chunks} + platform: label, + link: text => ( + + {text} ), })} diff --git a/apps/site/components/withBlogCrossLinks.tsx b/apps/site/components/withBlogCrossLinks.tsx index ee0068d488cdc..07d05fa8900d2 100644 --- a/apps/site/components/withBlogCrossLinks.tsx +++ b/apps/site/components/withBlogCrossLinks.tsx @@ -3,6 +3,7 @@ import type { FC } from 'react'; import { getClientContext } from '@/client-context'; import CrossLink from '@/components/Common/CrossLink'; import getBlogData from '@/next-data/blogData'; +import type { BlogCategory } from '@/types'; const WithBlogCrossLinks: FC = async () => { const { pathname } = getClientContext(); @@ -10,7 +11,7 @@ const WithBlogCrossLinks: FC = async () => { // Extracts from the static URL the components used for the Blog Post slug const [, , category, postname] = pathname.split('/'); - const { posts } = await getBlogData(category); + const { posts } = await getBlogData(category as BlogCategory); const currentItem = posts.findIndex( ({ slug }) => slug === `/blog/${category}/${postname}` diff --git a/apps/site/global.d.ts b/apps/site/global.d.ts new file mode 100644 index 0000000000000..8959340122443 --- /dev/null +++ b/apps/site/global.d.ts @@ -0,0 +1,29 @@ +import type baseMessages from '@node-core/website-i18n/locales/en.json'; +import type { MessageKeys, NestedValueOf, NestedKeyOf } from 'next-intl'; + +declare global { + // Defines a type for all the IntlMessage shape (which is used internall by next-intl) + // @see https://next-intl.dev/docs/workflows/typescript + type IntlMessages = typeof baseMessages; + + // Defines a generic type for all available i18n translation keys, by default not using any namespace + type IntlMessageKeys< + NestedKey extends NamespaceKeys< + IntlMessages, + NestedKeyOf + > = never, + > = MessageKeys< + NestedValueOf< + { '!': IntlMessages }, + [NestedKey] extends [never] ? '!' : `!.${NestedKey}` + >, + NestedKeyOf< + NestedValueOf< + { '!': IntlMessages }, + [NestedKey] extends [never] ? '!' : `!.${NestedKey}` + > + > + >; +} + +export {}; diff --git a/apps/site/hooks/react-generic/useSiteNavigation.ts b/apps/site/hooks/react-generic/useSiteNavigation.ts index d3c520790158b..a6c2024f00783 100644 --- a/apps/site/hooks/react-generic/useSiteNavigation.ts +++ b/apps/site/hooks/react-generic/useSiteNavigation.ts @@ -38,7 +38,7 @@ const useSiteNavigation = () => { const t = useTranslations(); const mapNavigationEntries = (entries: Navigation, context: Context = {}) => { - const getFormattedMessage = (label: string, key: string) => + const getFormattedMessage = (label: IntlMessageKeys, key: string) => t.rich(label, context[key] || {}) as FormattedMessage; return Object.entries(entries).map( diff --git a/apps/site/layouts/Blog.tsx b/apps/site/layouts/Blog.tsx index 1b3e1084e8191..b8a57d5a9a5f3 100644 --- a/apps/site/layouts/Blog.tsx +++ b/apps/site/layouts/Blog.tsx @@ -7,6 +7,7 @@ import WithBlogCategories from '@/components/withBlogCategories'; import WithFooter from '@/components/withFooter'; import WithNavBar from '@/components/withNavBar'; import getBlogData from '@/next-data/blogData'; +import type { BlogCategory } from '@/types'; import styles from './layouts.module.css'; @@ -18,7 +19,10 @@ const getBlogCategory = async (pathname: string) => { // note that malformed routes can't happen as they are all statically generated const [, , category = 'all', , page = 1] = pathname.split('/'); - const { posts, pagination } = await getBlogData(category, Number(page)); + const { posts, pagination } = await getBlogData( + category as BlogCategory, + Number(page) + ); return { category, posts, pagination, page: Number(page) }; }; @@ -27,7 +31,7 @@ const BlogLayout: FC = async () => { const { pathname } = getClientContext(); const t = await getTranslations(); - const mapCategoriesToTabs = (categories: Array) => + const mapCategoriesToTabs = (categories: Array) => categories.map(category => ({ key: category, label: t(`layouts.blog.categories.${category}`), diff --git a/apps/site/next-data/blogData.ts b/apps/site/next-data/blogData.ts index 951cd04975245..32e2b71494bc3 100644 --- a/apps/site/next-data/blogData.ts +++ b/apps/site/next-data/blogData.ts @@ -5,9 +5,12 @@ import { VERCEL_ENV, VERCEL_REGION, } from '@/next.constants.mjs'; -import type { BlogPostsRSC } from '@/types'; +import type { BlogCategory, BlogPostsRSC } from '@/types'; -const getBlogData = (cat: string, page?: number): Promise => { +const getBlogData = ( + cat: BlogCategory, + page?: number +): Promise => { const IS_NOT_VERCEL_RUNTIME_ENV = (!IS_DEV_ENV && VERCEL_ENV && !VERCEL_REGION) || (!IS_DEV_ENV && !VERCEL_ENV); diff --git a/apps/site/next-data/providers/blogData.ts b/apps/site/next-data/providers/blogData.ts index 4f26637eac2a6..bf06a7891665e 100644 --- a/apps/site/next-data/providers/blogData.ts +++ b/apps/site/next-data/providers/blogData.ts @@ -2,35 +2,37 @@ import { cache } from 'react'; import generateBlogData from '@/next-data/generators/blogData.mjs'; import { BLOG_POSTS_PER_PAGE } from '@/next.constants.mjs'; -import type { BlogPostsRSC } from '@/types'; +import type { BlogCategory, BlogPostsRSC } from '@/types'; const { categories, posts } = await generateBlogData(); export const provideBlogCategories = cache(() => categories); -export const provideBlogPosts = cache((category: string): BlogPostsRSC => { - const categoryPosts = posts - .filter(post => post.categories.includes(category)) - .sort((a, b) => b.date.getTime() - a.date.getTime()); +export const provideBlogPosts = cache( + (category: BlogCategory): BlogPostsRSC => { + const categoryPosts = posts + .filter(post => post.categories.includes(category)) + .sort((a, b) => b.date.getTime() - a.date.getTime()); - // Total amount of possible pages given the amount of blog posts - const total = categoryPosts.length / BLOG_POSTS_PER_PAGE; + // Total amount of possible pages given the amount of blog posts + const total = categoryPosts.length / BLOG_POSTS_PER_PAGE; - return { - posts: categoryPosts, - pagination: { - prev: null, - next: null, - // In case the division results on a remainder we need - // to have an extra page containing the remainder entries - pages: Math.floor(total % 1 === 0 ? total : total + 1), - total: categoryPosts.length, - }, - }; -}); + return { + posts: categoryPosts, + pagination: { + prev: null, + next: null, + // In case the division results on a remainder we need + // to have an extra page containing the remainder entries + pages: Math.floor(total % 1 === 0 ? total : total + 1), + total: categoryPosts.length, + }, + }; + } +); export const providePaginatedBlogPosts = cache( - (category: string, page: number): BlogPostsRSC => { + (category: BlogCategory, page: number): BlogPostsRSC => { const { posts, pagination } = provideBlogPosts(category); // This autocorrects if invalid numbers are given to only allow diff --git a/apps/site/tsconfig.json b/apps/site/tsconfig.json index b9c7f4ce06c9a..3aee2c1724e0e 100644 --- a/apps/site/tsconfig.json +++ b/apps/site/tsconfig.json @@ -20,6 +20,7 @@ }, "include": [ "next-env.d.ts", + "global.d.ts", "**/*.mdx", "**/*.ts", "**/*.tsx", diff --git a/apps/site/types/blog.ts b/apps/site/types/blog.ts index d59650087c380..33e917c19a761 100644 --- a/apps/site/types/blog.ts +++ b/apps/site/types/blog.ts @@ -1,17 +1,18 @@ export type BlogPreviewType = 'announcements' | 'release' | 'vulnerability'; +export type BlogCategory = IntlMessageKeys<'layouts.blog.categories'>; export interface BlogPost { title: string; author: string; username: string; date: Date; - categories: Array; + categories: Array; slug: string; } export interface BlogData { posts: Array; - categories: Array; + categories: Array; } export interface BlogPagination { diff --git a/apps/site/types/navigation.ts b/apps/site/types/navigation.ts index 73ff3aabb562f..b45d233bbb890 100644 --- a/apps/site/types/navigation.ts +++ b/apps/site/types/navigation.ts @@ -1,7 +1,7 @@ import type { HTMLAttributeAnchorTarget } from 'react'; export interface FooterConfig { - text: string; + text: IntlMessageKeys; link: string; } @@ -21,7 +21,7 @@ export type NavigationKeys = | 'blog'; export interface NavigationEntry { - label?: string; + label?: IntlMessageKeys; link?: string; items?: Record; target?: HTMLAttributeAnchorTarget | undefined; diff --git a/apps/site/util/downloadUtils.tsx b/apps/site/util/downloadUtils.tsx index 744c37ac7bef7..64baa17f251a2 100644 --- a/apps/site/util/downloadUtils.tsx +++ b/apps/site/util/downloadUtils.tsx @@ -48,16 +48,19 @@ type DownloadCompatibility = { releases: Array; }; +// Defines the Type definition for a Release Dropdown Item type DownloadDropdownItem = { + // A label to be used within the Dropdown message + label: string; // A flag that indicates if the item is recommended or not (official or community based) recommended?: boolean; // A URL pointing to the docs or support page for the item url?: string; // A bottom info that provides additional information about the item - bottomInfo?: string; + info?: IntlMessageKeys; // A compatibility object that defines the compatibility of the item with the current Release Context compatibility: Partial; -} & SelectValue; +} & Omit, 'label'>; // This function is used to get the next item in the dropdown // when the current item is disabeld/excluded/not valid @@ -67,16 +70,16 @@ export const nextItem = ( current: T, items: Array> ): T => { - const currentItem = items.find( - ({ value }) => String(value) === String(current) - ); + const item = items.find(({ value }) => String(value) === String(current)); - const isCurrentItemDisabled = !currentItem || currentItem.disabled; + const isDisabledOrExcluded = !item || item.disabled; - const nextItem = items.find(({ disabled }) => !disabled); + if (isDisabledOrExcluded) { + const nextItem = items.find(({ disabled }) => !disabled); - if (isCurrentItemDisabled && nextItem) { - return nextItem.value; + if (nextItem) { + return nextItem.value; + } } return current; @@ -146,7 +149,12 @@ export const OPERATING_SYSTEMS: Array> = [ ]; export const INSTALL_METHODS: Array< - DownloadDropdownItem + DownloadDropdownItem & + // Since the ReleaseCodeBox requires an info key to be provided, we force this + // to be mandatory for install methods + Required< + Pick, 'info' | 'url'> + > > = [ { label: InstallationMethodLabel.NVM, @@ -155,7 +163,7 @@ export const INSTALL_METHODS: Array< iconImage: , recommended: true, url: 'https://github.com/nvm-sh/nvm', - bottomInfo: 'layouts.download.codeBox.platformInfo.nvm', + info: 'layouts.download.codeBox.platformInfo.nvm', }, { label: InstallationMethodLabel.FNM, @@ -163,7 +171,7 @@ export const INSTALL_METHODS: Array< compatibility: { os: ['MAC', 'LINUX', 'WIN'] }, iconImage: , url: 'https://github.com/Schniz/fnm', - bottomInfo: 'layouts.download.codeBox.platformInfo.fnm', + info: 'layouts.download.codeBox.platformInfo.fnm', }, { label: InstallationMethodLabel.BREW, @@ -171,7 +179,7 @@ export const INSTALL_METHODS: Array< compatibility: { os: ['MAC', 'LINUX'], releases: ['Current', 'LTS'] }, iconImage: , url: 'https://brew.sh/', - bottomInfo: 'layouts.download.codeBox.platformInfo.brew', + info: 'layouts.download.codeBox.platformInfo.brew', }, { label: InstallationMethodLabel.CHOCO, @@ -179,6 +187,7 @@ export const INSTALL_METHODS: Array< compatibility: { os: ['WIN'] }, iconImage: , url: 'https://chocolatey.org/', + info: 'layouts.download.codeBox.platformInfo.choco', }, { label: InstallationMethodLabel.DOCKER, @@ -187,6 +196,7 @@ export const INSTALL_METHODS: Array< iconImage: , recommended: true, url: 'https://docs.docker.com/get-started/get-docker/', + info: 'layouts.download.codeBox.platformInfo.docker', }, ]; diff --git a/packages/i18n/locales/en.json b/packages/i18n/locales/en.json index 5aba9ecde73b0..821666dcaa3e7 100644 --- a/packages/i18n/locales/en.json +++ b/packages/i18n/locales/en.json @@ -298,13 +298,14 @@ "codeBox": { "unsupportedVersionWarning": "This version is out of maintenance. Please use a currently supported version.", "communityPlatformInfo": "Installation methods that involve community software are supported by the teams maintaining that software.", - "externalSupportInfo": "If you encounter any issues with {platform} please reach out to that project, here.", + "externalSupportInfo": "If you encounter any issues please visit {platform}'s website", "platformInfo": { "default": "{platform} and their installation scripts are not maintained by the Node.js project.", - "nvm": "\"nvm\" is a Node.js version manager, it allows you to install and manage multiple versions of Node.js.", - "fnm": "\"fnm\" is a Node.js version manager, it allows you to install and manage multiple versions of Node.js.", + "nvm": "\"nvm\" is a cross-platform Node.js version manager.", + "fnm": "\"fnm\" is a cross-platform Node.js version manager.", "brew": "Homebrew is a package manager for macOS and Linux.", - "choco": "Chocolatey is a package manager for Windows." + "choco": "Chocolatey is a package manager for Windows.", + "docker": "Docker is a containerization platform." } } }