diff --git a/apps/site/app/[locale]/next-data/api-data/route.ts b/apps/site/app/[locale]/next-data/api-data/route.ts index de008a59739b0..29fedbdf8393e 100644 --- a/apps/site/app/[locale]/next-data/api-data/route.ts +++ b/apps/site/app/[locale]/next-data/api-data/route.ts @@ -4,8 +4,8 @@ import provideReleaseData from '#site/next-data/providers/releaseData'; import { GITHUB_API_KEY } from '#site/next.constants.mjs'; import { defaultLocale } from '#site/next.locales.mjs'; import type { GitHubApiFile } from '#site/types'; -import { getGitHubApiDocsUrl } from '#site/util/gitHubUtils'; -import { parseRichTextIntoPlainText } from '#site/util/stringUtils'; +import { getGitHubApiDocsUrl } from '#site/util/github'; +import { parseRichTextIntoPlainText } from '#site/util/string'; // Defines if we should use the GitHub API Key for the request // based on the environment variable `GITHUB_API_KEY` diff --git a/apps/site/app/[locale]/next-data/og/[category]/[title]/route.tsx b/apps/site/app/[locale]/next-data/og/[category]/[title]/route.tsx index 05b304e5721ed..a521b6d686360 100644 --- a/apps/site/app/[locale]/next-data/og/[category]/[title]/route.tsx +++ b/apps/site/app/[locale]/next-data/og/[category]/[title]/route.tsx @@ -4,19 +4,21 @@ import { ImageResponse } from 'next/og'; import { DEFAULT_CATEGORY_OG_TYPE } from '#site/next.constants.mjs'; import { defaultLocale } from '#site/next.locales.mjs'; -import { hexToRGBA } from '#site/util/hexToRGBA'; // TODO: use CSS variables instead of absolute values const CATEGORY_TO_THEME_COLOUR_MAP = { - announcement: '#1a3f1d', - release: '#0c7bb3', - vulnerability: '#ae5f00', + announcement: 'rgb(26, 63, 29)', + release: 'rgb(12, 123, 179)', + vulnerability: 'rgb(174, 95, 0)', }; -type Category = keyof typeof CATEGORY_TO_THEME_COLOUR_MAP; - -type DynamicStaticPaths = { locale: string; category: Category; title: string }; -type StaticParams = { params: Promise }; +type StaticParams = { + params: Promise<{ + locale: string; + category: keyof typeof CATEGORY_TO_THEME_COLOUR_MAP; + title: string; + }>; +}; // This is the Route Handler for the `GET` method which handles the request // for generating OpenGraph images for Blog Posts and Pages @@ -29,7 +31,7 @@ export const GET = async (_: Request, props: StaticParams) => { ? CATEGORY_TO_THEME_COLOUR_MAP[params.category] : CATEGORY_TO_THEME_COLOUR_MAP[DEFAULT_CATEGORY_OG_TYPE]; - const gridBackground = `radial-gradient(circle, ${hexToRGBA(categoryColour)}, transparent)`; + const gridBackground = `radial-gradient(circle, ${categoryColour}, transparent)`; return new ImageResponse( ( diff --git a/apps/site/app/[locale]/next-data/page-data/route.ts b/apps/site/app/[locale]/next-data/page-data/route.ts index d7e9fdd09efd4..35e64530caef0 100644 --- a/apps/site/app/[locale]/next-data/page-data/route.ts +++ b/apps/site/app/[locale]/next-data/page-data/route.ts @@ -4,7 +4,7 @@ import matter from 'gray-matter'; import { dynamicRouter } from '#site/next.dynamic.mjs'; import { defaultLocale } from '#site/next.locales.mjs'; -import { parseRichTextIntoPlainText } from '#site/util/stringUtils'; +import { parseRichTextIntoPlainText } from '#site/util/string'; // This is the Route Handler for the `GET` method which handles the request // for a digest and metadata of all existing pages on Node.js Website diff --git a/apps/site/client-context.ts b/apps/site/client-context.ts index 8ed30d8fb353b..3b900b02a8123 100644 --- a/apps/site/client-context.ts +++ b/apps/site/client-context.ts @@ -1,7 +1,7 @@ import { cache } from 'react'; import type { ClientSharedServerContext } from '#site/types'; -import { assignClientContext } from '#site/util/assignClientContext'; +import { assignClientContext } from '#site/util/context'; // This allows us to have Server-Side Context's of the shared "contextual" data // which includes the frontmatter, the current pathname from the dynamic segments diff --git a/apps/site/components/Blog/BlogPostCard/index.tsx b/apps/site/components/Blog/BlogPostCard/index.tsx index 4f7487b86de75..9f8c7442685b3 100644 --- a/apps/site/components/Blog/BlogPostCard/index.tsx +++ b/apps/site/components/Blog/BlogPostCard/index.tsx @@ -6,7 +6,7 @@ import FormattedTime from '#site/components/Common/FormattedTime'; import Link from '#site/components/Link'; import WithAvatarGroup from '#site/components/withAvatarGroup'; import type { BlogCategory } from '#site/types'; -import { mapBlogCategoryToPreviewType } from '#site/util/blogUtils'; +import { mapBlogCategoryToPreviewType } from '#site/util/blog'; import styles from './index.module.css'; diff --git a/apps/site/components/Downloads/DownloadButton/index.tsx b/apps/site/components/Downloads/DownloadButton/index.tsx index f238ce4546505..d4f3e7a76dd4c 100644 --- a/apps/site/components/Downloads/DownloadButton/index.tsx +++ b/apps/site/components/Downloads/DownloadButton/index.tsx @@ -7,8 +7,8 @@ import type { FC, PropsWithChildren } from 'react'; import Button from '#site/components/Common/Button'; import { useClientContext } from '#site/hooks'; import type { NodeRelease } from '#site/types'; -import { getNodeDownloadUrl } from '#site/util/getNodeDownloadUrl'; -import { getUserPlatform } from '#site/util/getUserPlatform'; +import { getNodeDownloadUrl } from '#site/util/url'; +import { getUserPlatform } from '#site/util/userAgent'; import styles from './index.module.css'; diff --git a/apps/site/components/Downloads/DownloadLink.tsx b/apps/site/components/Downloads/DownloadLink.tsx index e639a83d2ac7c..905adbf352d8a 100644 --- a/apps/site/components/Downloads/DownloadLink.tsx +++ b/apps/site/components/Downloads/DownloadLink.tsx @@ -4,10 +4,9 @@ import type { FC, PropsWithChildren } from 'react'; import LinkWithArrow from '#site/components/LinkWithArrow'; import { useClientContext } from '#site/hooks'; -import type { NodeRelease } from '#site/types'; -import type { DownloadKind } from '#site/util/getNodeDownloadUrl'; -import { getNodeDownloadUrl } from '#site/util/getNodeDownloadUrl'; -import { getUserPlatform } from '#site/util/getUserPlatform'; +import type { DownloadKind, NodeRelease } from '#site/types'; +import { getNodeDownloadUrl } from '#site/util/url'; +import { getUserPlatform } from '#site/util/userAgent'; type DownloadLinkProps = { release: NodeRelease; kind?: DownloadKind }; diff --git a/apps/site/components/Downloads/MinorReleasesTable/index.tsx b/apps/site/components/Downloads/MinorReleasesTable/index.tsx index c2cf17ec975a5..641af7fec0cb8 100644 --- a/apps/site/components/Downloads/MinorReleasesTable/index.tsx +++ b/apps/site/components/Downloads/MinorReleasesTable/index.tsx @@ -7,7 +7,7 @@ import type { FC } from 'react'; import Link from '#site/components/Link'; import { BASE_CHANGELOG_URL } from '#site/next.constants.mjs'; import type { MinorVersion } from '#site/types'; -import { getNodeApiLink } from '#site/util/getNodeApiLink'; +import { getNodeApiUrl } from '#site/util/url'; import styles from './index.module.css'; @@ -50,7 +50,7 @@ export const MinorReleasesTable: FC = ({ {t('actions.docs')} diff --git a/apps/site/components/Downloads/Release/DownloadLink.tsx b/apps/site/components/Downloads/Release/DownloadLink.tsx index dc685ccedda79..6d9e20179f7f1 100644 --- a/apps/site/components/Downloads/Release/DownloadLink.tsx +++ b/apps/site/components/Downloads/Release/DownloadLink.tsx @@ -5,7 +5,7 @@ import { useContext } from 'react'; import DownloadLinkBase from '#site/components/Downloads/DownloadLink'; import { ReleaseContext } from '#site/providers/releaseProvider'; -import type { DownloadKind } from '#site/util/getNodeDownloadUrl'; +import type { DownloadKind } from '#site/types/download'; type DownloadLinkProps = { kind?: DownloadKind }; diff --git a/apps/site/components/Downloads/Release/InstallationMethodDropdown.tsx b/apps/site/components/Downloads/Release/InstallationMethodDropdown.tsx index b050b156f18c4..831b5b6e480ee 100644 --- a/apps/site/components/Downloads/Release/InstallationMethodDropdown.tsx +++ b/apps/site/components/Downloads/Release/InstallationMethodDropdown.tsx @@ -7,11 +7,7 @@ import type { FC } from 'react'; import { ReleaseContext } from '#site/providers/releaseProvider'; import type { InstallationMethod } from '#site/types/release'; -import { - nextItem, - INSTALL_METHODS, - parseCompat, -} from '#site/util/downloadUtils'; +import { nextItem, INSTALL_METHODS, parseCompat } from '#site/util/download'; const InstallationMethodDropdown: FC = () => { const release = useContext(ReleaseContext); diff --git a/apps/site/components/Downloads/Release/OperatingSystemDropdown.tsx b/apps/site/components/Downloads/Release/OperatingSystemDropdown.tsx index e5ba1e404d141..1e096adf3ffcd 100644 --- a/apps/site/components/Downloads/Release/OperatingSystemDropdown.tsx +++ b/apps/site/components/Downloads/Release/OperatingSystemDropdown.tsx @@ -7,14 +7,10 @@ import type { FC } from 'react'; import { useClientContext } from '#site/hooks'; import { ReleaseContext } from '#site/providers/releaseProvider'; -import type { UserOS } from '#site/types/userOS'; -import { - nextItem, - OPERATING_SYSTEMS, - parseCompat, -} from '#site/util/downloadUtils'; +import type { OperatingSystem } from '#site/types/userAgent'; +import { nextItem, OPERATING_SYSTEMS, parseCompat } from '#site/util/download'; -type OperatingSystemDropdownProps = { exclude?: Array }; +type OperatingSystemDropdownProps = { exclude?: Array }; const OperatingSystemDropdown: FC = () => { const { os } = useClientContext(); @@ -53,7 +49,7 @@ const OperatingSystemDropdown: FC = () => { ); return ( - + values={parsedOperatingSystems} defaultValue={release.os !== 'LOADING' ? release.os : undefined} loading={release.os === 'LOADING'} diff --git a/apps/site/components/Downloads/Release/PackageManagerDropdown.tsx b/apps/site/components/Downloads/Release/PackageManagerDropdown.tsx index 28a48d178339d..defee5799e364 100644 --- a/apps/site/components/Downloads/Release/PackageManagerDropdown.tsx +++ b/apps/site/components/Downloads/Release/PackageManagerDropdown.tsx @@ -7,11 +7,7 @@ import type { FC } from 'react'; import { ReleaseContext } from '#site/providers/releaseProvider'; import type { PackageManager } from '#site/types/release'; -import { - nextItem, - PACKAGE_MANAGERS, - parseCompat, -} from '#site/util/downloadUtils'; +import { nextItem, PACKAGE_MANAGERS, parseCompat } from '#site/util/download'; const PackageManagerDropdown: FC = () => { const release = useContext(ReleaseContext); diff --git a/apps/site/components/Downloads/Release/PlatformDropdown.tsx b/apps/site/components/Downloads/Release/PlatformDropdown.tsx index 7e5405e612155..57a73d1d1a710 100644 --- a/apps/site/components/Downloads/Release/PlatformDropdown.tsx +++ b/apps/site/components/Downloads/Release/PlatformDropdown.tsx @@ -7,9 +7,9 @@ import { useEffect, useContext, useMemo } from 'react'; import { useClientContext } from '#site/hooks'; import { ReleaseContext } from '#site/providers/releaseProvider'; -import type { UserPlatform } from '#site/types/userOS'; -import { PLATFORMS, nextItem, parseCompat } from '#site/util/downloadUtils'; -import { getUserPlatform } from '#site/util/getUserPlatform'; +import type { Platform } from '#site/types/userAgent'; +import { PLATFORMS, nextItem, parseCompat } from '#site/util/download'; +import { getUserPlatform } from '#site/util/userAgent'; const PlatformDropdown: FC = () => { const { architecture, bitness } = useClientContext(); @@ -57,7 +57,7 @@ const PlatformDropdown: FC = () => { ); return ( - + values={parsedPlatforms} defaultValue={release.platform !== '' ? release.platform : undefined} loading={release.os === 'LOADING' || release.platform === ''} diff --git a/apps/site/components/Downloads/Release/PrebuiltDownloadButtons.tsx b/apps/site/components/Downloads/Release/PrebuiltDownloadButtons.tsx index ca037f6a443c2..fccff0462d69f 100644 --- a/apps/site/components/Downloads/Release/PrebuiltDownloadButtons.tsx +++ b/apps/site/components/Downloads/Release/PrebuiltDownloadButtons.tsx @@ -11,8 +11,8 @@ import { ReleaseContext } from '#site/providers/releaseProvider'; import { OS_NOT_SUPPORTING_INSTALLERS, OperatingSystemLabel, -} from '#site/util/downloadUtils'; -import { getNodeDownloadUrl } from '#site/util/getNodeDownloadUrl'; +} from '#site/util/download'; +import { getNodeDownloadUrl } from '#site/util/url'; // Retrieves the pure extension piece from the input string const getExtension = (input: string) => String(input.split('.').slice(-1)); diff --git a/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx b/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx index 7e18a7bea4b9b..b3d13a9f9df4b 100644 --- a/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx +++ b/apps/site/components/Downloads/Release/ReleaseCodeBox.tsx @@ -16,7 +16,7 @@ import { ReleasesContext, } from '#site/providers/releaseProvider'; import type { ReleaseContextType } from '#site/types/release'; -import { INSTALL_METHODS } from '#site/util/downloadUtils'; +import { INSTALL_METHODS } from '#site/util/download'; // Creates a minimal JavaScript interpreter for parsing the JavaScript code from the snippets // Note: that the code runs inside a sandboxed environment and cannot interact with any code outside of the sandbox diff --git a/apps/site/components/withAvatarGroup.tsx b/apps/site/components/withAvatarGroup.tsx index fc0569877c50f..0e2731c72e218 100644 --- a/apps/site/components/withAvatarGroup.tsx +++ b/apps/site/components/withAvatarGroup.tsx @@ -5,7 +5,7 @@ import type { ComponentProps, FC } from 'react'; import Link from '#site/components/Link'; import type { AuthorProps } from '#site/types'; -import { getAuthors } from '#site/util/authorUtils'; +import { getAuthors } from '#site/util/author'; type WithAvatarGroupProps = Omit< ComponentProps, diff --git a/apps/site/components/withBadgeGroup.tsx b/apps/site/components/withBadgeGroup.tsx index ceda7771adff4..069e762bf48bc 100644 --- a/apps/site/components/withBadgeGroup.tsx +++ b/apps/site/components/withBadgeGroup.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react'; import Link from '#site/components/Link'; import { siteConfig } from '#site/next.json.mjs'; -import { dateIsBetween } from '#site/util/dateUtils'; +import { dateIsBetween } from '#site/util/date'; const WithBadgeGroup: FC<{ section: string }> = ({ section }) => { const badge = siteConfig.websiteBadges[section]; diff --git a/apps/site/components/withBanner.tsx b/apps/site/components/withBanner.tsx index 09f05065959d4..169ddd5598f4f 100644 --- a/apps/site/components/withBanner.tsx +++ b/apps/site/components/withBanner.tsx @@ -4,7 +4,7 @@ import type { FC } from 'react'; import Link from '#site/components/Link'; import { siteConfig } from '#site/next.json.mjs'; -import { dateIsBetween } from '#site/util/dateUtils'; +import { dateIsBetween } from '#site/util/date'; const WithBanner: FC<{ section: string }> = ({ section }) => { const banner = siteConfig.websiteBanners[section]; diff --git a/apps/site/components/withBlogCategories.tsx b/apps/site/components/withBlogCategories.tsx index c011a6b60b618..d007d1673893c 100644 --- a/apps/site/components/withBlogCategories.tsx +++ b/apps/site/components/withBlogCategories.tsx @@ -5,7 +5,7 @@ import BlogPostCard from '#site/components/Blog/BlogPostCard'; import LinkTabs from '#site/components/Common/LinkTabs'; import Pagination from '#site/components/Common/Pagination'; import type { BlogPostsRSC } from '#site/types'; -import { mapAuthorToCardAuthors } from '#site/util/authorUtils'; +import { mapAuthorToCardAuthors } from '#site/util/author'; type WithBlogCategoriesProps = { categories: ComponentProps['tabs']; diff --git a/apps/site/components/withBreadcrumbs.tsx b/apps/site/components/withBreadcrumbs.tsx index a0fcc4f28792a..2d342a2b6803a 100644 --- a/apps/site/components/withBreadcrumbs.tsx +++ b/apps/site/components/withBreadcrumbs.tsx @@ -12,7 +12,7 @@ import { useSiteNavigation, } from '#site/hooks'; import type { NavigationKeys } from '#site/types'; -import { dashToCamelCase } from '#site/util/stringUtils'; +import { dashToCamelCase } from '#site/util/string'; type WithBreadcrumbsProps = { navKeys?: Array; diff --git a/apps/site/components/withMetaBar.tsx b/apps/site/components/withMetaBar.tsx index a7dd313ad8167..b7c5348112beb 100644 --- a/apps/site/components/withMetaBar.tsx +++ b/apps/site/components/withMetaBar.tsx @@ -12,7 +12,7 @@ import useMediaQuery from '#site/hooks/react-client/useMediaQuery'; import { DEFAULT_DATE_FORMAT } from '#site/next.calendar.constants.mjs'; import { TRANSLATION_URL } from '#site/next.constants.mjs'; import { defaultLocale } from '#site/next.locales.mjs'; -import { getGitHubBlobUrl } from '#site/util/gitHubUtils'; +import { getGitHubBlobUrl } from '#site/util/github'; const WithMetaBar: FC = () => { const { headings, readingTime, frontmatter, filename } = useClientContext(); diff --git a/apps/site/hooks/react-client/useDetectOS.ts b/apps/site/hooks/react-client/useDetectOS.ts index e751738bd1c61..dc2d2cfcdf244 100644 --- a/apps/site/hooks/react-client/useDetectOS.ts +++ b/apps/site/hooks/react-client/useDetectOS.ts @@ -2,14 +2,17 @@ import { useEffect, useState } from 'react'; -import type { UserArchitecture, UserBitness, UserOS } from '#site/types/userOS'; -import { detectOS } from '#site/util/detectOS'; -import { getHighEntropyValues } from '#site/util/getHighEntropyValues'; +import type { + Architecture, + Bitness, + OperatingSystem, +} from '#site/types/userAgent'; +import { getHighEntropyValues, detectOS } from '#site/util/userAgent'; type UserOSState = { - os: UserOS | 'LOADING'; - bitness: UserBitness | ''; - architecture: UserArchitecture | ''; + os: OperatingSystem | 'LOADING'; + bitness: Bitness | ''; + architecture: Architecture | ''; }; const useDetectOS = () => { @@ -42,8 +45,8 @@ const useDetectOS = () => { }) => { setUserOSState(current => ({ ...current, - bitness: bitness as UserBitness, - architecture: architecture as UserArchitecture, + bitness: bitness as Bitness, + architecture: architecture as Architecture, })); } ); diff --git a/apps/site/hooks/react-client/useNavigationState.ts b/apps/site/hooks/react-client/useNavigationState.ts index ab3e420d6e668..dbdb093ac6e25 100644 --- a/apps/site/hooks/react-client/useNavigationState.ts +++ b/apps/site/hooks/react-client/useNavigationState.ts @@ -4,7 +4,7 @@ import type { RefObject } from 'react'; import { useContext, useEffect } from 'react'; import { NavigationStateContext } from '#site/providers/navigationStateProvider'; -import { debounce } from '#site/util/debounce'; +import { debounce } from '#site/util/objects'; const useNavigationState = ( id: string, diff --git a/apps/site/i18n.tsx b/apps/site/i18n.tsx index 3d899b06f6755..7abd8682746a9 100644 --- a/apps/site/i18n.tsx +++ b/apps/site/i18n.tsx @@ -4,7 +4,7 @@ import { getRequestConfig } from 'next-intl/server'; import { availableLocaleCodes, defaultLocale } from '#site/next.locales.mjs'; -import deepMerge from './util/deepMerge'; +import { deepMerge } from './util/objects'; // Loads the Application Locales/Translations Dynamically const loadLocaleDictionary = async (locale: string) => { diff --git a/apps/site/layouts/Post.tsx b/apps/site/layouts/Post.tsx index a614fc27a36bd..9b5234b727757 100644 --- a/apps/site/layouts/Post.tsx +++ b/apps/site/layouts/Post.tsx @@ -7,8 +7,8 @@ import WithFooter from '#site/components/withFooter'; import WithMetaBar from '#site/components/withMetaBar'; import WithNavBar from '#site/components/withNavBar'; import { useClientContext } from '#site/hooks/react-server'; -import { mapAuthorToCardAuthors } from '#site/util/authorUtils'; -import { mapBlogCategoryToPreviewType } from '#site/util/blogUtils'; +import { mapAuthorToCardAuthors } from '#site/util/author'; +import { mapBlogCategoryToPreviewType } from '#site/util/blog'; import styles from './layouts.module.css'; diff --git a/apps/site/next.mdx.compiler.mjs b/apps/site/next.mdx.compiler.mjs index 009c25f067e74..987e6c8803c7d 100644 --- a/apps/site/next.mdx.compiler.mjs +++ b/apps/site/next.mdx.compiler.mjs @@ -6,7 +6,7 @@ import { matter } from 'vfile-matter'; import { createSval } from './next.jsx.compiler.mjs'; import { REHYPE_PLUGINS, REMARK_PLUGINS } from './next.mdx.plugins.mjs'; -import { createGitHubSlugger } from './util/gitHubUtils'; +import { createGitHubSlugger } from './util/github'; // Defines a JSX Fragment and JSX Runtime for the MDX Compiler export const reactRuntime = { Fragment, jsx, jsxs }; diff --git a/apps/site/providers/matterProvider.tsx b/apps/site/providers/matterProvider.tsx index 1e683d3e9d974..38b032c2ca3a1 100644 --- a/apps/site/providers/matterProvider.tsx +++ b/apps/site/providers/matterProvider.tsx @@ -5,7 +5,7 @@ import type { FC, PropsWithChildren } from 'react'; import { useDetectOS } from '#site/hooks'; import type { ClientSharedServerContext } from '#site/types'; -import { assignClientContext } from '#site/util/assignClientContext'; +import { assignClientContext } from '#site/util/context'; export const MatterContext = createContext( assignClientContext({}) diff --git a/apps/site/types/downloads.ts b/apps/site/types/download.ts similarity index 59% rename from apps/site/types/downloads.ts rename to apps/site/types/download.ts index 91f4366c26a62..3be983a9466f2 100644 --- a/apps/site/types/downloads.ts +++ b/apps/site/types/download.ts @@ -3,3 +3,5 @@ export interface DownloadSnippet { language: string; content: string; } + +export type DownloadKind = 'installer' | 'binary' | 'source'; diff --git a/apps/site/types/index.ts b/apps/site/types/index.ts index 53eb2fe7fc6fa..38f5767b732db 100644 --- a/apps/site/types/index.ts +++ b/apps/site/types/index.ts @@ -11,4 +11,5 @@ export * from './server'; export * from './github'; export * from './calendar'; export * from './author'; -export * from './downloads'; +export * from './download'; +export * from './userAgent'; diff --git a/apps/site/types/release.ts b/apps/site/types/release.ts index 28f945f6cbd3a..fc150fec22d68 100644 --- a/apps/site/types/release.ts +++ b/apps/site/types/release.ts @@ -1,6 +1,6 @@ -import type { DownloadSnippet } from '#site/types/downloads'; +import type { DownloadSnippet } from '#site/types/download'; import type { NodeRelease } from '#site/types/releases'; -import type { UserOS, UserPlatform } from '#site/types/userOS'; +import type { OperatingSystem, Platform } from '#site/types/userAgent'; export type InstallationMethod = | 'NVM' @@ -17,23 +17,23 @@ export type PackageManager = 'NPM' | 'YARN' | 'PNPM'; // during runtime and do not have necessarily a consistent initial value export interface ReleaseState { version: string; - os: UserOS | 'LOADING'; - platform: UserPlatform | ''; + os: OperatingSystem | 'LOADING'; + platform: Platform | ''; installMethod: InstallationMethod | ''; packageManager: PackageManager; } export type ReleaseAction = | { type: 'SET_VERSION'; payload: string } - | { type: 'SET_OS'; payload: UserOS } - | { type: 'SET_PLATFORM'; payload: UserPlatform } + | { type: 'SET_OS'; payload: OperatingSystem } + | { type: 'SET_PLATFORM'; payload: Platform } | { type: 'SET_INSTALL_METHOD'; payload: InstallationMethod } | { type: 'SET_MANAGER'; payload: PackageManager }; export interface ReleaseDispatchActions { setVersion: (version: string) => void; - setOS: (os: UserOS) => void; - setPlatform: (bitness: UserPlatform) => void; + setOS: (os: OperatingSystem) => void; + setPlatform: (bitness: Platform) => void; setInstallMethod: (installMethod: InstallationMethod) => void; setPackageManager: (packageManager: PackageManager) => void; } diff --git a/apps/site/types/userAgent.ts b/apps/site/types/userAgent.ts new file mode 100644 index 0000000000000..55d55aee03b3f --- /dev/null +++ b/apps/site/types/userAgent.ts @@ -0,0 +1,13 @@ +import type constants from '../util/download/constants.json'; + +// Extract OS key type from the systems object +export type OperatingSystem = keyof typeof constants.systems; + +// Derive the union type of UserPlatform from the userOptions +export type Platform = (typeof constants.userOptions.platforms)[number]; + +// Derive the union type of UserBitness from the userOptions +export type Bitness = (typeof constants.userOptions.bitness)[number]; + +// Derive the union type of UserArchitecture from the userOptions +export type Architecture = (typeof constants.userOptions.architecture)[number]; diff --git a/apps/site/types/userOS.ts b/apps/site/types/userOS.ts deleted file mode 100644 index 76bd462f345e5..0000000000000 --- a/apps/site/types/userOS.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type constants from '../util/downloadUtils/constants.json'; - -// Extract OS key type from the systems object -export type UserOS = keyof typeof constants.systems; - -// Derive the union type of UserPlatform from the userOptions -export type UserPlatform = (typeof constants.userOptions.platforms)[number]; - -// Derive the union type of UserBitness from the userOptions -export type UserBitness = (typeof constants.userOptions.bitness)[number]; - -// Derive the union type of UserArchitecture from the userOptions -export type UserArchitecture = - (typeof constants.userOptions.architecture)[number]; diff --git a/apps/site/util/__tests__/authorUtils.test.mjs b/apps/site/util/__tests__/author.test.mjs similarity index 99% rename from apps/site/util/__tests__/authorUtils.test.mjs rename to apps/site/util/__tests__/author.test.mjs index 6a26617258a4d..04497a32ef3f2 100644 --- a/apps/site/util/__tests__/authorUtils.test.mjs +++ b/apps/site/util/__tests__/author.test.mjs @@ -6,7 +6,7 @@ import { getAuthorWithId, getAuthorWithName, getAuthors, -} from '#site/util/authorUtils'; +} from '#site/util/author'; describe('mapAuthorToCardAuthors', () => { it('maps authors to card authors with default avatar source', () => { diff --git a/apps/site/util/__tests__/blogUtils.test.mjs b/apps/site/util/__tests__/blog.test.mjs similarity index 88% rename from apps/site/util/__tests__/blogUtils.test.mjs rename to apps/site/util/__tests__/blog.test.mjs index 7142de43505fd..b68301c3ce300 100644 --- a/apps/site/util/__tests__/blogUtils.test.mjs +++ b/apps/site/util/__tests__/blog.test.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { mapBlogCategoryToPreviewType } from '#site/util/blogUtils'; +import { mapBlogCategoryToPreviewType } from '#site/util/blog'; describe('mapBlogCategoryToPreviewType', () => { it('returns the correct preview type for recognized categories', () => { diff --git a/apps/site/util/__tests__/assignClientContext.test.mjs b/apps/site/util/__tests__/context.test.mjs similarity index 94% rename from apps/site/util/__tests__/assignClientContext.test.mjs rename to apps/site/util/__tests__/context.test.mjs index 5768d13cb8c3b..22b00b0cc2dd7 100644 --- a/apps/site/util/__tests__/assignClientContext.test.mjs +++ b/apps/site/util/__tests__/context.test.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { assignClientContext } from '#site/util/assignClientContext'; +import { assignClientContext } from '#site/util/context'; const mockContext = { frontmatter: { title: 'Sample Title' }, diff --git a/apps/site/util/__tests__/dateUtils.test.mjs b/apps/site/util/__tests__/date.test.mjs similarity index 96% rename from apps/site/util/__tests__/dateUtils.test.mjs rename to apps/site/util/__tests__/date.test.mjs index f9dfe49d738da..804197aa76f75 100644 --- a/apps/site/util/__tests__/dateUtils.test.mjs +++ b/apps/site/util/__tests__/date.test.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { dateIsBetween } from '#site/util/dateUtils'; +import { dateIsBetween } from '#site/util/date'; describe('dateIsBetween', () => { it('should return true when the current date is between start and end dates', () => { diff --git a/apps/site/util/__tests__/deepMerge.test.mjs b/apps/site/util/__tests__/deepMerge.test.mjs deleted file mode 100644 index c41a2d421e979..0000000000000 --- a/apps/site/util/__tests__/deepMerge.test.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; - -import deepMerge from '#site/util/deepMerge'; - -describe('deepMerge', () => { - it('should merge nested objects', () => { - const obj1 = { a: { b: 1 }, c: 2 }; - const obj2 = { a: { d: 3 }, e: 4 }; - const result = deepMerge(obj1, obj2); - assert.deepEqual(result, { a: { b: 1, d: 3 }, c: 2, e: 4 }); - }); - - it('should overwrite primitive properties', () => { - const obj1 = { a: 1 }; - const obj2 = { a: 2 }; - const result = deepMerge(obj1, obj2); - assert.deepEqual(result, { a: 2 }); - }); -}); diff --git a/apps/site/util/__tests__/downloadUtils.test.mjs b/apps/site/util/__tests__/download.test.mjs similarity index 99% rename from apps/site/util/__tests__/downloadUtils.test.mjs rename to apps/site/util/__tests__/download.test.mjs index 1f1e9a8bbb502..d4bd959de1073 100644 --- a/apps/site/util/__tests__/downloadUtils.test.mjs +++ b/apps/site/util/__tests__/download.test.mjs @@ -8,7 +8,7 @@ import { INSTALL_METHODS, PACKAGE_MANAGERS, PLATFORMS, -} from '#site/util/downloadUtils'; +} from '#site/util/download'; describe('parseCompat', () => { it('should handle all OS, install methods, and package managers', () => { diff --git a/apps/site/util/__tests__/getHighEntropyValues.test.mjs b/apps/site/util/__tests__/getHighEntropyValues.test.mjs deleted file mode 100644 index 6d789d3092a31..0000000000000 --- a/apps/site/util/__tests__/getHighEntropyValues.test.mjs +++ /dev/null @@ -1,46 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it, beforeEach } from 'node:test'; - -import { getHighEntropyValues } from '#site/util/getHighEntropyValues'; - -const mock = () => Promise.resolve({ platform: 'Win32', architecture: 'x86' }); - -describe('getHighEntropyValues', () => { - beforeEach(() => { - Object.defineProperty(global, 'navigator', { - value: { - userAgentData: { - getHighEntropyValues: mock, - }, - }, - configurable: true, - }); - }); - - it('should resolve and return hint values', async () => { - const hints = ['platform']; - const result = await getHighEntropyValues(hints); - assert.equal(result.platform, 'Win32'); - }); - - it('should return an empty object on rejection', async () => { - navigator.userAgentData.getHighEntropyValues = () => Promise.resolve({}); - const hints = ['platform']; - const result = await getHighEntropyValues(hints); - assert.equal(result.platform, undefined); - navigator.userAgentData.getHighEntropyValues = mock; - }); - - it('should return multiple hint values', async () => { - const hints = ['platform', 'architecture']; - const result = await getHighEntropyValues(hints); - assert.equal(result.platform, 'Win32'); - assert.equal(result.architecture, 'x86'); - }); - - it('should return undefined for unsupported hints', async () => { - const hints = ['unsupportedHint']; - const result = await getHighEntropyValues(hints); - assert.equal(result.unsupportedHint, undefined); - }); -}); diff --git a/apps/site/util/__tests__/getNodeApiLink.test.mjs b/apps/site/util/__tests__/getNodeApiLink.test.mjs deleted file mode 100644 index 1987405e325c3..0000000000000 --- a/apps/site/util/__tests__/getNodeApiLink.test.mjs +++ /dev/null @@ -1,52 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; - -import { getNodeApiLink } from '#site/util/getNodeApiLink'; - -describe('getNodeApiLink', () => { - it('should return the correct API link for versions >=0.3.1 and <0.5.1', () => { - const version = '0.4.0'; - const expectedLink = `https://nodejs.org/docs/${version}/api/`; - - const result = getNodeApiLink(version); - - assert.equal(result, expectedLink); - }); - - it('should return the correct URL for versions >=0.3.1 and <0.5.1', () => { - const url = getNodeApiLink('v0.4.10'); - assert.ok(url.includes('/api/')); - }); - - it('should return the correct API link for versions >=0.1.14 and <0.3.1', () => { - const version = '0.2.0'; - const expectedLink = `https://nodejs.org/docs/${version}/api.html`; - - const result = getNodeApiLink(version); - - assert.equal(result, expectedLink); - }); - - it('should return the correct API link for versions >=1.0.0 and <4.0.0', () => { - const version = '2.3.0'; - const expectedLink = `https://iojs.org/dist/${version}/docs/api/`; - - const result = getNodeApiLink(version); - - assert.equal(result, expectedLink); - }); - - it('should form the correct URL for versions >=1.0.0 and <4.0.0', () => { - const url = getNodeApiLink('v1.2.3'); - assert.ok(url.includes('iojs.org/dist/v1.2.3/docs/api/')); - }); - - it('should return the correct API link for other versions', () => { - const version = '5.0.0'; - const expectedLink = `https://nodejs.org/dist/${version}/docs/api/`; - - const result = getNodeApiLink(version); - - assert.equal(result, expectedLink); - }); -}); diff --git a/apps/site/util/__tests__/getUserPlatform.test.mjs b/apps/site/util/__tests__/getUserPlatform.test.mjs deleted file mode 100644 index 15a0ac90958d1..0000000000000 --- a/apps/site/util/__tests__/getUserPlatform.test.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; - -import { getUserPlatform } from '#site/util/getUserPlatform'; - -describe('getUserPlatform', () => { - it('should return arm64 for arm + 64', () => { - assert.equal(getUserPlatform('arm', '64'), 'arm64'); - }); - - it('should return x64 for non-arm + 64', () => { - assert.equal(getUserPlatform('amd64', '64'), 'x64'); - }); - - it('should return x86 otherwise', () => { - assert.equal(getUserPlatform('amd64', '32'), 'x86'); - }); -}); diff --git a/apps/site/util/__tests__/gitHubUtils.test.mjs b/apps/site/util/__tests__/github.test.mjs similarity index 97% rename from apps/site/util/__tests__/gitHubUtils.test.mjs rename to apps/site/util/__tests__/github.test.mjs index 5afc3e64b8c6a..03f20de2d4372 100644 --- a/apps/site/util/__tests__/gitHubUtils.test.mjs +++ b/apps/site/util/__tests__/github.test.mjs @@ -10,7 +10,7 @@ const { createGitHubSlugger, getGitHubBlobUrl, getGitHubApiDocsUrl, -} = await import('#site/util/gitHubUtils'); +} = await import('#site/util/github'); describe('gitHubUtils', () => { it('getGitHubAvatarUrl returns the correct URL', () => { diff --git a/apps/site/util/__tests__/hexToRGBA.test.mjs b/apps/site/util/__tests__/hexToRGBA.test.mjs deleted file mode 100644 index 94bdd29fa4f83..0000000000000 --- a/apps/site/util/__tests__/hexToRGBA.test.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; - -import { hexToRGBA } from '#site/util/hexToRGBA'; - -describe('hexToRGBA', () => { - it('should convert a hex color to an rgba color', () => { - const hexColor = '#000000'; - const rgbaColor = hexToRGBA(hexColor, 0.5); - - assert.equal(rgbaColor, 'rgba(0, 0, 0, 0.5)'); - }); - - it('should convert a hex color to an rgba color', () => { - const hexColor = '#ffffff'; - const rgbaColor = hexToRGBA(hexColor, 0.5); - - assert.equal(rgbaColor, 'rgba(255, 255, 255, 0.5)'); - }); - - it('should convert hex to RGBA correctly', () => { - assert.equal(hexToRGBA('#FFFFFF'), 'rgba(255, 255, 255, 0.9)'); - }); - - it('should handle custom alpha value', () => { - assert.equal(hexToRGBA('#000000', 0.5), 'rgba(0, 0, 0, 0.5)'); - }); -}); diff --git a/apps/site/util/__tests__/debounce.test.mjs b/apps/site/util/__tests__/objects.test.mjs similarity index 65% rename from apps/site/util/__tests__/debounce.test.mjs rename to apps/site/util/__tests__/objects.test.mjs index 0632a3c153f2b..d5309b40d4efd 100644 --- a/apps/site/util/__tests__/debounce.test.mjs +++ b/apps/site/util/__tests__/objects.test.mjs @@ -1,7 +1,23 @@ import assert from 'node:assert/strict'; import { describe, it, beforeEach } from 'node:test'; -import { debounce } from '#site/util/debounce'; +import { deepMerge, debounce } from '#site/util/objects'; + +describe('deepMerge', () => { + it('should merge nested objects', () => { + const obj1 = { a: { b: 1 }, c: 2 }; + const obj2 = { a: { d: 3 }, e: 4 }; + const result = deepMerge(obj1, obj2); + assert.deepEqual(result, { a: { b: 1, d: 3 }, c: 2, e: 4 }); + }); + + it('should overwrite primitive properties', () => { + const obj1 = { a: 1 }; + const obj2 = { a: 2 }; + const result = deepMerge(obj1, obj2); + assert.deepEqual(result, { a: 2 }); + }); +}); describe('debounce', () => { beforeEach(t => { diff --git a/apps/site/util/__tests__/stringUtils.test.mjs b/apps/site/util/__tests__/string.test.mjs similarity index 99% rename from apps/site/util/__tests__/stringUtils.test.mjs rename to apps/site/util/__tests__/string.test.mjs index 23429d185a32a..c4d95f125a9b8 100644 --- a/apps/site/util/__tests__/stringUtils.test.mjs +++ b/apps/site/util/__tests__/string.test.mjs @@ -5,7 +5,7 @@ import { getAcronymFromString, parseRichTextIntoPlainText, dashToCamelCase, -} from '#site/util/stringUtils'; +} from '#site/util/string'; describe('String utils', () => { it('getAcronymFromString returns the correct acronym', () => { diff --git a/apps/site/util/__tests__/getNodeDownloadUrl.test.mjs b/apps/site/util/__tests__/url.test.mjs similarity index 52% rename from apps/site/util/__tests__/getNodeDownloadUrl.test.mjs rename to apps/site/util/__tests__/url.test.mjs index 8ec40abda7fb2..8f6cd5be3f302 100644 --- a/apps/site/util/__tests__/getNodeDownloadUrl.test.mjs +++ b/apps/site/util/__tests__/url.test.mjs @@ -1,10 +1,58 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { getNodeDownloadUrl } from '#site/util/getNodeDownloadUrl'; +import { getNodeDownloadUrl, getNodeApiUrl } from '../url'; const version = 'v18.16.0'; +describe('getNodeApiUrl', () => { + it('should return the correct API link for versions >=0.3.1 and <0.5.1', () => { + const version = '0.4.0'; + const expectedLink = `https://nodejs.org/docs/${version}/api/`; + + const result = getNodeApiUrl(version); + + assert.equal(result, expectedLink); + }); + + it('should return the correct URL for versions >=0.3.1 and <0.5.1', () => { + const url = getNodeApiUrl('v0.4.10'); + assert.ok(url.includes('/api/')); + }); + + it('should return the correct API link for versions >=0.1.14 and <0.3.1', () => { + const version = '0.2.0'; + const expectedLink = `https://nodejs.org/docs/${version}/api.html`; + + const result = getNodeApiUrl(version); + + assert.equal(result, expectedLink); + }); + + it('should return the correct API link for versions >=1.0.0 and <4.0.0', () => { + const version = '2.3.0'; + const expectedLink = `https://iojs.org/dist/${version}/docs/api/`; + + const result = getNodeApiUrl(version); + + assert.equal(result, expectedLink); + }); + + it('should form the correct URL for versions >=1.0.0 and <4.0.0', () => { + const url = getNodeApiUrl('v1.2.3'); + assert.ok(url.includes('iojs.org/dist/v1.2.3/docs/api/')); + }); + + it('should return the correct API link for other versions', () => { + const version = '5.0.0'; + const expectedLink = `https://nodejs.org/dist/${version}/docs/api/`; + + const result = getNodeApiUrl(version); + + assert.equal(result, expectedLink); + }); +}); + describe('getNodeDownloadUrl', () => { it('should return the correct download URL for Mac', () => { const os = 'MAC'; diff --git a/apps/site/util/__tests__/detectOS.test.mjs b/apps/site/util/__tests__/userAgent.test.mjs similarity index 52% rename from apps/site/util/__tests__/detectOS.test.mjs rename to apps/site/util/__tests__/userAgent.test.mjs index c056140aa714a..adfa42d05e285 100644 --- a/apps/site/util/__tests__/detectOS.test.mjs +++ b/apps/site/util/__tests__/userAgent.test.mjs @@ -1,7 +1,12 @@ import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; +import { describe, it, beforeEach } from 'node:test'; -import { detectOsInUserAgent, detectOS } from '#site/util/detectOS'; +import { + detectOS, + detectOsInUserAgent, + getHighEntropyValues, + getUserPlatform, +} from '#site/util/userAgent'; const userAgentTestCases = [ [ @@ -29,6 +34,22 @@ const userAgentTestCases = [ [undefined, 'OTHER'], ]; +const mock = () => Promise.resolve({ platform: 'Win32', architecture: 'x86' }); + +describe('getUserPlatform', () => { + it('should return arm64 for arm + 64', () => { + assert.equal(getUserPlatform('arm', '64'), 'arm64'); + }); + + it('should return x64 for non-arm + 64', () => { + assert.equal(getUserPlatform('amd64', '64'), 'x64'); + }); + + it('should return x86 otherwise', () => { + assert.equal(getUserPlatform('amd64', '32'), 'x86'); + }); +}); + describe('detectOsInUserAgent', () => { for (const [userAgent, expected] of userAgentTestCases) { it(`should return ${expected} for userAgent ${userAgent}`, () => { @@ -69,3 +90,39 @@ describe('detectOS', () => { assert.equal(detectOS(), 'OTHER'); }); }); + +describe('getHighEntropyValues', () => { + beforeEach(() => { + Object.defineProperty(global, 'navigator', { + value: { + userAgentData: { + getHighEntropyValues: mock, + }, + }, + configurable: true, + }); + }); + + it('should resolve and return hint values', async () => { + const result = await getHighEntropyValues(['platform']); + assert.equal(result.platform, 'Win32'); + }); + + it('should return an empty object on rejection', async () => { + navigator.userAgentData.getHighEntropyValues = () => Promise.resolve({}); + const result = await getHighEntropyValues(['platform']); + assert.equal(result.platform, undefined); + navigator.userAgentData.getHighEntropyValues = mock; + }); + + it('should return multiple hint values', async () => { + const result = await getHighEntropyValues(['platform', 'architecture']); + assert.equal(result.platform, 'Win32'); + assert.equal(result.architecture, 'x86'); + }); + + it('should return undefined for unsupported hints', async () => { + const result = await getHighEntropyValues(['unsupportedHint']); + assert.equal(result.unsupportedHint, undefined); + }); +}); diff --git a/apps/site/util/authorUtils.ts b/apps/site/util/author.ts similarity index 94% rename from apps/site/util/authorUtils.ts rename to apps/site/util/author.ts index 4a3cccc975ac5..92ab969b5d90d 100644 --- a/apps/site/util/authorUtils.ts +++ b/apps/site/util/author.ts @@ -1,7 +1,7 @@ import { authors } from '#site/next.json.mjs'; import type { AuthorProps } from '#site/types'; -import { getGitHubAvatarUrl } from '#site/util/gitHubUtils'; -import { getAcronymFromString } from '#site/util/stringUtils'; +import { getGitHubAvatarUrl } from '#site/util/github'; +import { getAcronymFromString } from '#site/util/string'; export const mapAuthorToCardAuthors = (author: string) => { // Clears text in parentheses diff --git a/apps/site/util/blogUtils.ts b/apps/site/util/blog.ts similarity index 100% rename from apps/site/util/blogUtils.ts rename to apps/site/util/blog.ts diff --git a/apps/site/util/assignClientContext.ts b/apps/site/util/context.ts similarity index 100% rename from apps/site/util/assignClientContext.ts rename to apps/site/util/context.ts diff --git a/apps/site/util/dateUtils.ts b/apps/site/util/date.ts similarity index 100% rename from apps/site/util/dateUtils.ts rename to apps/site/util/date.ts diff --git a/apps/site/util/debounce.ts b/apps/site/util/debounce.ts deleted file mode 100644 index ca569ba359e97..0000000000000 --- a/apps/site/util/debounce.ts +++ /dev/null @@ -1,16 +0,0 @@ -type DebounceFunction = (...args: Array) => void; - -let timeoutId: NodeJS.Timeout; - -export const debounce = - ( - func: T, - delay: number - ): ((...args: Parameters) => void) => - (...args: Parameters) => { - clearTimeout(timeoutId); - - timeoutId = setTimeout(() => { - func(...args); - }, delay); - }; diff --git a/apps/site/util/detectOS.ts b/apps/site/util/detectOS.ts deleted file mode 100644 index c7769c5237375..0000000000000 --- a/apps/site/util/detectOS.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { UserOS } from '#site/types/userOS'; - -export const detectOsInUserAgent = (userAgent: string | undefined): UserOS => { - // Match OS names and convert to uppercase directly if there's a match - const osMatch = userAgent?.match(/(Win|Mac|Linux|AIX)/); - return osMatch ? (osMatch[1].toUpperCase() as UserOS) : 'OTHER'; -}; - -// Since `navigator.appVersion` is deprecated, we use the `userAgent`` -// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/appVersion -export const detectOS = (): UserOS => detectOsInUserAgent(navigator?.userAgent); diff --git a/apps/site/util/downloadUtils/constants.json b/apps/site/util/download/constants.json similarity index 100% rename from apps/site/util/downloadUtils/constants.json rename to apps/site/util/download/constants.json diff --git a/apps/site/util/downloadUtils/index.tsx b/apps/site/util/download/index.tsx similarity index 91% rename from apps/site/util/downloadUtils/index.tsx rename to apps/site/util/download/index.tsx index b7fa11d3dca8b..9b31281e4c1f8 100644 --- a/apps/site/util/downloadUtils/index.tsx +++ b/apps/site/util/download/index.tsx @@ -5,9 +5,13 @@ import * as PackageManagerIcons from '@node-core/ui-components/Icons/PackageMana import type { ElementType } from 'react'; import satisfies from 'semver/functions/satisfies'; -import type { IntlMessageKeys, NodeReleaseStatus } from '#site/types'; +import type { + IntlMessageKeys, + NodeReleaseStatus, + OperatingSystem, + Platform, +} from '#site/types'; import type * as Types from '#site/types/release'; -import type { UserOS, UserPlatform } from '#site/types/userOS'; import constants from './constants.json'; @@ -25,9 +29,9 @@ export const OperatingSystemLabel = Object.fromEntries( // Base types for dropdown functionality type DownloadCompatibility = { - os?: Array; + os?: Array; installMethod?: Array; - platform?: Array; + platform?: Array; semver?: Array; releases?: Array; }; @@ -103,7 +107,7 @@ export const OPERATING_SYSTEMS = Object.entries(systems as ActualSystems) .filter(([key]) => key !== 'LOADING' && key !== 'OTHER') .map(([key, data]) => ({ label: data.name as IntlMessageKeys, - value: key as UserOS, + value: key as OperatingSystem, compatibility: data.compatibility, iconImage: createIcon(OSIcons, data.icon), })); @@ -119,7 +123,7 @@ export const INSTALL_METHODS = installMethods.map(method => ({ info: method.info as IntlMessageKeys, compatibility: { ...method.compatibility, - os: method.compatibility?.os?.map(os => os as UserOS), + os: method.compatibility?.os?.map(os => os as OperatingSystem), releases: method.compatibility?.releases?.map( release => release as NodeReleaseStatus ), @@ -144,8 +148,8 @@ export const PLATFORMS = Object.fromEntries( key, data.platforms.map(platform => ({ label: platform.label, - value: platform.value as UserPlatform, + value: platform.value as Platform, compatibility: platform.compatibility || {}, })), ]) -) as Record>>; +) as Record>>; diff --git a/apps/site/util/getHighEntropyValues.ts b/apps/site/util/getHighEntropyValues.ts deleted file mode 100644 index 8a066490bb666..0000000000000 --- a/apps/site/util/getHighEntropyValues.ts +++ /dev/null @@ -1,33 +0,0 @@ -/// - -// This method is used to retrieve a User's platform based on their architecture and bitness. -// @see https://wicg.github.io/ua-client-hints/#http-ua-hints -export const getHighEntropyValues = async >( - hints: T -) => { - let result: UADataValues = {}; - - // This is necessary to detect Windows 11 on Edge. - // [MDN](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/getHighEntropyValues) - // [MSFT](https://learn.microsoft.com/en-us/microsoft-edge/web-platform/how-to-detect-win11) - if (typeof navigator?.userAgentData?.getHighEntropyValues === 'function') { - const entropyValues = navigator.userAgentData.getHighEntropyValues(hints); - - // Apparently in some cases this is not a Promise, we can Promisify it. - result = await Promise.resolve(entropyValues).catch( - // Fallback to an empty object if the Promise fails. - () => ({}) as UADataValues - ); - } - - const mappedResult = hints.map(hint => [ - hint, - // Since the values could come as empty string in some situations - // we should check if the hint is present in the result and if it's not an empty string. - hint in result && result[hint] ? result[hint] : undefined, - ]); - - return Object.fromEntries(mappedResult) as { - [K in T[number]]: UADataValues[K]; - }; -}; diff --git a/apps/site/util/getNodeApiLink.ts b/apps/site/util/getNodeApiLink.ts deleted file mode 100644 index ec99cd89c543b..0000000000000 --- a/apps/site/util/getNodeApiLink.ts +++ /dev/null @@ -1,17 +0,0 @@ -import semVer from 'semver'; - -import { DOCS_URL, DIST_URL } from '#site/next.constants.mjs'; - -export const getNodeApiLink = (version: string) => { - if (semVer.satisfies(version, '>=0.3.1 <0.5.1')) { - return `${DOCS_URL}${version}/api/`; - } - - if (semVer.satisfies(version, '>=0.1.14 <0.3.1')) { - return `${DOCS_URL}${version}/api.html`; - } - - return semVer.satisfies(version, '>=1.0.0 <4.0.0') - ? `https://iojs.org/dist/${version}/docs/api/` - : `${DIST_URL}${version}/docs/api/`; -}; diff --git a/apps/site/util/getUserPlatform.ts b/apps/site/util/getUserPlatform.ts deleted file mode 100644 index 496fe4dec2f24..0000000000000 --- a/apps/site/util/getUserPlatform.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type * as Types from '#site/types/userOS'; - -// This method is used to retrieve a User's platform based on their architecture and bitness. -// Note: This is only used for automatic Platform detection for supported platforms by using `useDetectOS` -// @see https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/getHighEntropyValues -export const getUserPlatform = ( - userArchitecture: Types.UserArchitecture | '', - userBitness: Types.UserBitness | '' -): Types.UserPlatform => { - if (userArchitecture === 'arm' && userBitness === '64') { - return 'arm64'; - } - - return userBitness === '64' ? 'x64' : 'x86'; -}; diff --git a/apps/site/util/gitHubUtils.ts b/apps/site/util/github.ts similarity index 100% rename from apps/site/util/gitHubUtils.ts rename to apps/site/util/github.ts diff --git a/apps/site/util/hexToRGBA.ts b/apps/site/util/hexToRGBA.ts deleted file mode 100644 index fcafe312303f4..0000000000000 --- a/apps/site/util/hexToRGBA.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const hexToRGBA = (hex = '', alpha = 0.9) => { - hex = hex.replace(/^#/, ''); - - const bigint = parseInt(hex, 16); - const r = (bigint >> 16) & 255; - const g = (bigint >> 8) & 255; - const b = bigint & 255; - - return `rgba(${r}, ${g}, ${b}, ${alpha})`; -}; diff --git a/apps/site/util/deepMerge.ts b/apps/site/util/objects.ts similarity index 55% rename from apps/site/util/deepMerge.ts rename to apps/site/util/objects.ts index 751c09a3afe16..b8e92400e8d81 100644 --- a/apps/site/util/deepMerge.ts +++ b/apps/site/util/objects.ts @@ -1,4 +1,21 @@ -export default function deepMerge( +type DebounceFunction = (...args: Array) => void; + +export const debounce = ( + func: T, + delay: number +): ((...args: Parameters) => void) => { + let timeoutId: NodeJS.Timeout; + + return (...args: Parameters) => { + clearTimeout(timeoutId); + + timeoutId = setTimeout(() => { + func(...args); + }, delay); + }; +}; + +export function deepMerge( obj1: Obj1, obj2: Obj2 ): Obj1 & Obj2 { diff --git a/apps/site/util/stringUtils.ts b/apps/site/util/string.ts similarity index 100% rename from apps/site/util/stringUtils.ts rename to apps/site/util/string.ts diff --git a/apps/site/util/getNodeDownloadUrl.ts b/apps/site/util/url.ts similarity index 80% rename from apps/site/util/getNodeDownloadUrl.ts rename to apps/site/util/url.ts index d5f2a8ef0c44d..9fde24b05294e 100644 --- a/apps/site/util/getNodeDownloadUrl.ts +++ b/apps/site/util/url.ts @@ -1,12 +1,26 @@ -import { DIST_URL } from '#site/next.constants.mjs'; -import type { UserOS, UserPlatform } from '#site/types/userOS'; +import { satisfies } from 'semver'; -export type DownloadKind = 'installer' | 'binary' | 'source'; +import { DOCS_URL, DIST_URL } from '#site/next.constants.mjs'; +import type { OperatingSystem, Platform, DownloadKind } from '#site/types'; + +export const getNodeApiUrl = (version: string) => { + if (satisfies(version, '>=0.3.1 <0.5.1')) { + return `${DOCS_URL}${version}/api/`; + } + + if (satisfies(version, '>=0.1.14 <0.3.1')) { + return `${DOCS_URL}${version}/api.html`; + } + + return satisfies(version, '>=1.0.0 <4.0.0') + ? `https://iojs.org/dist/${version}/docs/api/` + : `${DIST_URL}${version}/docs/api/`; +}; export const getNodeDownloadUrl = ( versionWithPrefix: string, - os: UserOS | 'LOADING', - platform: UserPlatform = 'x64', + os: OperatingSystem | 'LOADING', + platform: Platform = 'x64', kind: DownloadKind = 'installer' ) => { const baseURL = `${DIST_URL}${versionWithPrefix}`; diff --git a/apps/site/util/userAgent.ts b/apps/site/util/userAgent.ts new file mode 100644 index 0000000000000..9f2417f7ee867 --- /dev/null +++ b/apps/site/util/userAgent.ts @@ -0,0 +1,88 @@ +/// + +import type { + OperatingSystem, + Architecture, + Bitness, + Platform, +} from '#site/types'; + +// Constants for better maintainability +const OS_PATTERNS = /(Win|Mac|Linux|AIX)/; +const EMPTY_UA_DATA: UADataValues = {}; + +/** + * Detects operating system from user agent string + * @param userAgent - The user agent string to parse + * @returns The detected OS or 'OTHER' if not recognized + */ +export const detectOsInUserAgent = ( + userAgent: string | undefined +): OperatingSystem => { + const osMatch = userAgent?.match(OS_PATTERNS); + return osMatch ? (osMatch[1].toUpperCase() as OperatingSystem) : 'OTHER'; +}; + +/** + * Detects operating system using navigator.userAgent + * Note: navigator.appVersion is deprecated, so we use userAgent instead + * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/appVersion + */ +export const detectOS = (): OperatingSystem => + detectOsInUserAgent(navigator?.userAgent); + +/** + * Determines user platform based on architecture and bitness + * Used for automatic platform detection with `useDetectOS` + * @see https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/getHighEntropyValues + */ +export const getUserPlatform = ( + userArchitecture: Architecture | '', + userBitness: Bitness | '' +): Platform => { + if (userArchitecture === 'arm' && userBitness === '64') { + return 'arm64'; + } + + return userBitness === '64' ? 'x64' : 'x86'; +}; + +/** + * Retrieves high entropy values from User-Agent Client Hints API + * This method is used to get detailed user platform information including architecture and bitness + * Necessary for detecting Windows 11 on Edge and other platform-specific features + * + * @param hints - Array of hint keys to retrieve + * @returns Promise resolving to an object with requested hint values + * + * @see https://wicg.github.io/ua-client-hints/#http-ua-hints + * @see https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/getHighEntropyValues + * @see https://learn.microsoft.com/en-us/microsoft-edge/web-platform/how-to-detect-win11 + */ +export const getHighEntropyValues = async >( + hints: T +): Promise<{ [K in T[number]]: UADataValues[K] }> => { + let result: UADataValues = EMPTY_UA_DATA; + + // Check if the User-Agent Client Hints API is available + if (typeof navigator?.userAgentData?.getHighEntropyValues === 'function') { + try { + const entropyValues = navigator.userAgentData.getHighEntropyValues(hints); + // Handle both Promise and non-Promise return values + result = await Promise.resolve(entropyValues); + } catch { + // Fallback to empty object if the API call fails + result = EMPTY_UA_DATA; + } + } + + // Map hints to their values, filtering out empty strings + const mappedResult = hints.map(hint => [ + hint, + hint in result && result[hint] ? result[hint] : undefined, + ]); + + return Object.fromEntries(mappedResult) as { + [K in T[number]]: UADataValues[K]; + }; +};