From 4f5df6a832977a532f71325a6b79d896809a727c Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 17 Dec 2024 12:55:10 -0700 Subject: [PATCH] refactor: make locales array immutable (#74037) To improve memory efficiency, this marks all the locale arrays as readonly. This ensures that there's no undefined behaviour from accidentally mutating the array, and allows us to use the locales array as a stable reference for a future optimization PR. --- packages/next/src/build/index.ts | 14 +++++++++----- packages/next/src/build/static-paths/pages.ts | 10 ++++++++-- packages/next/src/build/utils.ts | 2 +- packages/next/src/client/get-domain-locale.ts | 4 ++-- packages/next/src/server/accept-header.ts | 4 ++-- packages/next/src/server/base-server.ts | 2 +- packages/next/src/server/config-shared.ts | 6 +++--- .../src/server/dev/static-paths-worker.ts | 2 +- packages/next/src/server/render.tsx | 19 +++++++++++-------- packages/next/src/server/require.ts | 2 +- .../shared/lib/i18n/detect-domain-locale.ts | 2 +- .../shared/lib/i18n/normalize-locale-path.ts | 2 +- packages/next/src/shared/lib/router/router.ts | 12 ++++++------ .../router/utils/get-next-pathname-info.ts | 2 +- .../lib/router/utils/resolve-rewrites.ts | 2 +- packages/next/src/shared/lib/utils.ts | 6 +++--- 16 files changed, 52 insertions(+), 39 deletions(-) diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 01be33f2b85b7..37b9f15438a17 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -377,13 +377,13 @@ export type RoutesManifest = { dynamicRoutes: Array dataRoutes: Array i18n?: { - domains?: Array<{ + domains?: ReadonlyArray<{ http?: true domain: string - locales?: string[] + locales?: readonly string[] defaultLocale: string }> - locales: string[] + locales: readonly string[] defaultLocale: string localeDetection?: false } @@ -473,7 +473,11 @@ async function writeClientSsgManifest( buildId, distDir, locales, - }: { buildId: string; distDir: string; locales: string[] } + }: { + buildId: string + distDir: string + locales: readonly string[] | undefined + } ) { const ssgPages = new Set( [ @@ -3622,7 +3626,7 @@ export default async function build( await writeClientSsgManifest(prerenderManifest, { distDir, buildId, - locales: config.i18n?.locales || [], + locales: config.i18n?.locales, }) } else { await writePrerenderManifest(distDir, { diff --git a/packages/next/src/build/static-paths/pages.ts b/packages/next/src/build/static-paths/pages.ts index 3a5ad406cbc79..b684006fc95b3 100644 --- a/packages/next/src/build/static-paths/pages.ts +++ b/packages/next/src/build/static-paths/pages.ts @@ -19,7 +19,7 @@ export async function buildPagesStaticPaths({ page: string getStaticPaths: GetStaticPaths configFileName: string - locales?: string[] + locales?: readonly string[] defaultLocale?: string }): Promise { const prerenderedRoutes: PrerenderedRoute[] = [] @@ -28,7 +28,13 @@ export async function buildPagesStaticPaths({ // Get the default list of allowed params. const routeParameterKeys = Object.keys(_routeMatcher(page)) - const staticPathsResult = await getStaticPaths({ locales, defaultLocale }) + const staticPathsResult = await getStaticPaths({ + // We create a copy here to avoid having the types of `getStaticPaths` + // change. This ensures that users can't mutate this array and have it + // poison the reference. + locales: [...(locales ?? [])], + defaultLocale, + }) const expectedReturnVal = `Expected: { paths: [], fallback: boolean }\n` + diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index f9b78d79ea77a..ad6c70ce3d297 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -981,7 +981,7 @@ export async function isPageStatic({ configFileName: string runtimeEnvConfig: any httpAgentOptions: NextConfigComplete['httpAgentOptions'] - locales?: string[] + locales?: readonly string[] defaultLocale?: string parentId?: any edgeInfo?: any diff --git a/packages/next/src/client/get-domain-locale.ts b/packages/next/src/client/get-domain-locale.ts index 0924cb34b7779..aebe4ccec95fe 100644 --- a/packages/next/src/client/get-domain-locale.ts +++ b/packages/next/src/client/get-domain-locale.ts @@ -8,8 +8,8 @@ const basePath = (process.env.__NEXT_ROUTER_BASEPATH as string) || '' export function getDomainLocale( path: string, locale?: string | false, - locales?: string[], - domainLocales?: DomainLocale[] + locales?: readonly string[], + domainLocales?: readonly DomainLocale[] ) { if (process.env.__NEXT_I18N_SUPPORT) { const normalizeLocalePath: typeof NormalizeFn = diff --git a/packages/next/src/server/accept-header.ts b/packages/next/src/server/accept-header.ts index 071c48737ae1c..38284657914df 100644 --- a/packages/next/src/server/accept-header.ts +++ b/packages/next/src/server/accept-header.ts @@ -12,7 +12,7 @@ interface Options { function parse( raw: string, - preferences: string[] | undefined, + preferences: readonly string[] | undefined, options: Options ) { const lowers = new Map() @@ -127,7 +127,7 @@ function parse( return preferred } -export function acceptLanguage(header = '', preferences?: string[]) { +export function acceptLanguage(header = '', preferences?: readonly string[]) { return ( parse(header, preferences, { type: 'accept-language', diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 3c23f8af11522..564ffc68f4aec 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -1437,7 +1437,7 @@ export default abstract class Server< } const normalizeResult = normalizeLocalePath( removePathPrefix(parsedUrl.pathname, this.nextConfig.basePath || ''), - this.nextConfig.i18n?.locales || [] + this.nextConfig.i18n?.locales ) if (normalizeResult.detectedLocale) { diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 026f9c93fee64..629fb1e740c82 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -23,20 +23,20 @@ export type NextConfigComplete = Required & { configFileName: string } -export type I18NDomains = DomainLocale[] +export type I18NDomains = readonly DomainLocale[] export interface I18NConfig { defaultLocale: string domains?: I18NDomains localeDetection?: false - locales: string[] + locales: readonly string[] } export interface DomainLocale { defaultLocale: string domain: string http?: true - locales?: string[] + locales?: readonly string[] } export interface ESLintConfig { diff --git a/packages/next/src/server/dev/static-paths-worker.ts b/packages/next/src/server/dev/static-paths-worker.ts index 17b1330aebf53..c1978b515f6fc 100644 --- a/packages/next/src/server/dev/static-paths-worker.ts +++ b/packages/next/src/server/dev/static-paths-worker.ts @@ -55,7 +55,7 @@ export async function loadStaticPaths({ pathname: string config: RuntimeConfig httpAgentOptions: NextConfigComplete['httpAgentOptions'] - locales?: string[] + locales?: readonly string[] defaultLocale?: string isAppPath: boolean page: string diff --git a/packages/next/src/server/render.tsx b/packages/next/src/server/render.tsx index b2022d743f53d..ed33dde5a8c3a 100644 --- a/packages/next/src/server/render.tsx +++ b/packages/next/src/server/render.tsx @@ -142,9 +142,9 @@ class ServerRouter implements NextRouter { isFallback: boolean locale?: string isReady: boolean - locales?: string[] + locales?: readonly string[] defaultLocale?: string - domainLocales?: DomainLocale[] + domainLocales?: readonly DomainLocale[] isPreview: boolean isLocaleDomain: boolean @@ -156,9 +156,9 @@ class ServerRouter implements NextRouter { isReady: boolean, basePath: string, locale?: string, - locales?: string[], + locales?: readonly string[], defaultLocale?: string, - domainLocales?: DomainLocale[], + domainLocales?: readonly DomainLocale[], isPreview?: boolean, isLocaleDomain?: boolean ) { @@ -261,9 +261,9 @@ export type RenderOptsPartial = { nextFontManifest?: DeepReadonly distDir?: string locale?: string - locales?: string[] + locales?: readonly string[] defaultLocale?: string - domainLocales?: DomainLocale[] + domainLocales?: readonly DomainLocale[] disableOptimizedLoading?: boolean supportsDynamicResponse: boolean isBot?: boolean @@ -848,7 +848,7 @@ export async function renderToHTMLImpl( ...(isPreview ? { draftMode: true, preview: true, previewData: previewData } : undefined), - locales: renderOpts.locales, + locales: [...(renderOpts.locales ?? [])], locale: renderOpts.locale, defaultLocale: renderOpts.defaultLocale, revalidateReason: renderOpts.isOnDemandRevalidate @@ -1071,7 +1071,10 @@ export async function renderToHTMLImpl( ...(previewData !== false ? { draftMode: true, preview: true, previewData: previewData } : undefined), - locales: renderOpts.locales, + // We create a copy here to avoid having the types of + // `getServerSideProps` change. This ensures that users can't + // mutate this array and have it poison the reference. + locales: [...(renderOpts.locales ?? [])], locale: renderOpts.locale, defaultLocale: renderOpts.defaultLocale, }) diff --git a/packages/next/src/server/require.ts b/packages/next/src/server/require.ts index fda15e7c5e855..95e4e22a599e7 100644 --- a/packages/next/src/server/require.ts +++ b/packages/next/src/server/require.ts @@ -19,7 +19,7 @@ const pagePathCache = !isDev ? new LRUCache(1000) : null export function getMaybePagePath( page: string, distDir: string, - locales: string[] | undefined, + locales: readonly string[] | undefined, isAppPath: boolean ): string | null { const cacheKey = `${page}:${distDir}:${locales}:${isAppPath}` diff --git a/packages/next/src/shared/lib/i18n/detect-domain-locale.ts b/packages/next/src/shared/lib/i18n/detect-domain-locale.ts index a7ce3e07a0cd5..df9d9f35a5e88 100644 --- a/packages/next/src/shared/lib/i18n/detect-domain-locale.ts +++ b/packages/next/src/shared/lib/i18n/detect-domain-locale.ts @@ -1,7 +1,7 @@ import type { DomainLocale } from '../../../server/config-shared' export function detectDomainLocale( - domainItems?: DomainLocale[], + domainItems?: readonly DomainLocale[], hostname?: string, detectedLocale?: string ) { diff --git a/packages/next/src/shared/lib/i18n/normalize-locale-path.ts b/packages/next/src/shared/lib/i18n/normalize-locale-path.ts index d687605e14a28..146b854b35652 100644 --- a/packages/next/src/shared/lib/i18n/normalize-locale-path.ts +++ b/packages/next/src/shared/lib/i18n/normalize-locale-path.ts @@ -14,7 +14,7 @@ export interface PathLocale { */ export function normalizeLocalePath( pathname: string, - locales?: string[] + locales?: readonly string[] ): PathLocale { let detectedLocale: string | undefined // first item will be empty string from splitting at first char diff --git a/packages/next/src/shared/lib/router/router.ts b/packages/next/src/shared/lib/router/router.ts index e74ae88268b16..5d0fa2b7eed4f 100644 --- a/packages/next/src/shared/lib/router/router.ts +++ b/packages/next/src/shared/lib/router/router.ts @@ -348,9 +348,9 @@ export type BaseRouter = { asPath: string basePath: string locale?: string | undefined - locales?: string[] | undefined + locales?: readonly string[] | undefined defaultLocale?: string | undefined - domainLocales?: DomainLocale[] | undefined + domainLocales?: readonly DomainLocale[] | undefined isLocaleDomain: boolean } @@ -681,9 +681,9 @@ export default class Router implements BaseRouter { isSsr: boolean _inFlightRoute?: string | undefined _shallow?: boolean | undefined - locales?: string[] | undefined + locales?: readonly string[] | undefined defaultLocale?: string | undefined - domainLocales?: DomainLocale[] | undefined + domainLocales?: readonly DomainLocale[] | undefined isReady: boolean isLocaleDomain: boolean isFirstPopStateEvent = true @@ -735,9 +735,9 @@ export default class Router implements BaseRouter { err?: Error isFallback: boolean locale?: string - locales?: string[] + locales?: readonly string[] defaultLocale?: string - domainLocales?: DomainLocale[] + domainLocales?: readonly DomainLocale[] isPreview?: boolean } ) { diff --git a/packages/next/src/shared/lib/router/utils/get-next-pathname-info.ts b/packages/next/src/shared/lib/router/utils/get-next-pathname-info.ts index 71937afa6db63..7016cfe47e801 100644 --- a/packages/next/src/shared/lib/router/utils/get-next-pathname-info.ts +++ b/packages/next/src/shared/lib/router/utils/get-next-pathname-info.ts @@ -39,7 +39,7 @@ interface Options { */ nextConfig?: { basePath?: string - i18n?: { locales?: string[] } | null + i18n?: { locales?: readonly string[] } | null trailingSlash?: boolean } diff --git a/packages/next/src/shared/lib/router/utils/resolve-rewrites.ts b/packages/next/src/shared/lib/router/utils/resolve-rewrites.ts index 4c1bb545147b6..a628d474f223c 100644 --- a/packages/next/src/shared/lib/router/utils/resolve-rewrites.ts +++ b/packages/next/src/shared/lib/router/utils/resolve-rewrites.ts @@ -17,7 +17,7 @@ export default function resolveRewrites( }, query: ParsedUrlQuery, resolveHref: (path: string) => string, - locales?: string[] + locales?: readonly string[] ): { matchedPage: boolean parsedAs: ParsedRelativeUrl diff --git a/packages/next/src/shared/lib/utils.ts b/packages/next/src/shared/lib/utils.ts index ff1955ab2456c..abc5d6da627f3 100644 --- a/packages/next/src/shared/lib/utils.ts +++ b/packages/next/src/shared/lib/utils.ts @@ -107,9 +107,9 @@ export type NEXT_DATA = { gip?: boolean appGip?: boolean locale?: string - locales?: string[] + locales?: readonly string[] defaultLocale?: string - domainLocales?: DomainLocale[] + domainLocales?: readonly DomainLocale[] scriptLoader?: any[] isPreview?: boolean notFoundSrcPage?: string @@ -150,7 +150,7 @@ export interface NextPageContext { /** * All configured locales */ - locales?: string[] + locales?: readonly string[] /** * The configured default locale */