From 6bd9781eb03f8ffd8964faaaa5071949a30e6143 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 16 Jul 2024 19:30:53 +0100 Subject: [PATCH 01/17] fix: ensure 2nd level middleware match respects locales --- edge-runtime/lib/next-request.ts | 143 ++++++++++++++++--------------- edge-runtime/lib/response.ts | 34 ++++---- edge-runtime/lib/routing.ts | 2 +- edge-runtime/lib/util.ts | 7 ++ edge-runtime/middleware.ts | 16 ++-- src/build/functions/edge.ts | 5 +- 6 files changed, 107 insertions(+), 100 deletions(-) diff --git a/edge-runtime/lib/next-request.ts b/edge-runtime/lib/next-request.ts index e6a1bb95f8..fa4bce99c4 100644 --- a/edge-runtime/lib/next-request.ts +++ b/edge-runtime/lib/next-request.ts @@ -2,73 +2,66 @@ import type { Context } from '@netlify/edge-functions' import { addBasePath, - addTrailingSlash, + addLocale, normalizeDataUrl, normalizeLocalePath, + normalizeTrailingSlash, removeBasePath, } from './util.ts' -interface I18NConfig { - defaultLocale: string - localeDetection?: false - locales: string[] -} +import type { NextConfig } from 'next/dist/server/config-shared' +import type { NextRequest } from 'next/server' -export interface RequestData { - geo?: { - city?: string - country?: string - region?: string - latitude?: string - longitude?: string - timezone?: string - } - headers: Record - ip?: string - method: string - nextConfig?: { - basePath?: string - i18n?: I18NConfig | null - trailingSlash?: boolean - skipMiddlewareUrlNormalize?: boolean - } - page?: { - name?: string - params?: { [key: string]: string } - } - url: string - body?: ReadableStream +export type NetlifyNextRequest = Pick< + NextRequest, + 'url' | 'headers' | 'geo' | 'ip' | 'method' | 'body' +> + +export type NetlifyNextContext = { + localizedUrl: string detectedLocale?: string + i18n?: NextConfig['i18n'] + basePath?: NextConfig['basePath'] + trailingSlash?: NextConfig['trailingSlash'] } -const normalizeRequestURL = ( - originalURL: string, - nextConfig?: RequestData['nextConfig'], -): { url: string; detectedLocale?: string } => { +const normalizeRequestURL = (originalURL: string, nextConfig?: NextConfig): string => { const url = new URL(originalURL) - let pathname = removeBasePath(url.pathname, nextConfig?.basePath) + url.pathname = removeBasePath(url.pathname, nextConfig?.basePath) - // If it exists, remove the locale from the URL and store it - const { detectedLocale } = normalizeLocalePath(pathname, nextConfig?.i18n?.locales) + // We want to run middleware for data requests and expose the URL of the + // corresponding pages, so we have to normalize the URLs before running + // the handler. + url.pathname = normalizeDataUrl(url.pathname) - if (!nextConfig?.skipMiddlewareUrlNormalize) { - // We want to run middleware for data requests and expose the URL of the - // corresponding pages, so we have to normalize the URLs before running - // the handler. - pathname = normalizeDataUrl(pathname) + // Normalizing the trailing slash based on the `trailingSlash` configuration + // property from the Next.js config. + url.pathname = normalizeTrailingSlash(url.pathname, nextConfig?.trailingSlash) - // Normalizing the trailing slash based on the `trailingSlash` configuration - // property from the Next.js config. - if (nextConfig?.trailingSlash) { - pathname = addTrailingSlash(pathname) - } - } + url.pathname = addBasePath(url.pathname, nextConfig?.basePath) + + return url.toString() +} + +const localizeRequestURL = ( + originalURL: string, + nextConfig?: NextConfig, +): { localizedUrl: string; detectedLocale?: string } => { + const url = new URL(originalURL) + + url.pathname = removeBasePath(url.pathname, nextConfig?.basePath) - url.pathname = addBasePath(pathname, nextConfig?.basePath) + // Detect the locale from the URL + const { detectedLocale } = normalizeLocalePath(url.pathname, nextConfig?.i18n?.locales) + + // Add the locale to the URL if not already present + url.pathname = addLocale(url.pathname, detectedLocale ?? nextConfig?.i18n?.defaultLocale) + + url.pathname = addBasePath(url.pathname, nextConfig?.basePath) return { - url: url.toString(), + localizedUrl: url.toString(), detectedLocale, } } @@ -76,29 +69,43 @@ const normalizeRequestURL = ( export const buildNextRequest = ( request: Request, context: Context, - nextConfig?: RequestData['nextConfig'], -): RequestData => { + nextConfig?: NextConfig, +): { nextRequest: NetlifyNextRequest; nextContext: NetlifyNextContext } => { const { url, method, body, headers } = request - const { country, subdivision, city, latitude, longitude, timezone } = context.geo - const geo: RequestData['geo'] = { - city, - country: country?.code, - region: subdivision?.code, - latitude: latitude?.toString(), - longitude: longitude?.toString(), - timezone, - } + const { country, subdivision, city, latitude, longitude } = context.geo + const { i18n, basePath, trailingSlash } = nextConfig ?? {} - const { detectedLocale, url: normalizedUrl } = normalizeRequestURL(url, nextConfig) + const normalizedUrl = nextConfig?.skipMiddlewareUrlNormalize + ? url + : normalizeRequestURL(url, nextConfig) - return { - headers: Object.fromEntries(headers.entries()), - geo, + const { localizedUrl, detectedLocale } = localizeRequestURL(normalizedUrl, nextConfig) + + const nextRequest: NetlifyNextRequest = { url: normalizedUrl, - method, + headers, + geo: { + city, + country: country?.code, + region: subdivision?.code, + latitude: latitude?.toString(), + longitude: longitude?.toString(), + }, ip: context.ip, - body: body ?? undefined, - nextConfig, + method, + body, + } + + const nextContext = { + localizedUrl, detectedLocale, + i18n, + trailingSlash, + basePath, + } + + return { + nextRequest, + nextContext, } } diff --git a/edge-runtime/lib/response.ts b/edge-runtime/lib/response.ts index 6e2366354b..302893ddc2 100644 --- a/edge-runtime/lib/response.ts +++ b/edge-runtime/lib/response.ts @@ -4,7 +4,7 @@ import { HTMLRewriter } from '../vendor/deno.land/x/html_rewriter@v0.1.0-pre.17/ import { updateModifiedHeaders } from './headers.ts' import type { StructuredLogger } from './logging.ts' import { addMiddlewareHeaders, isMiddlewareRequest, isMiddlewareResponse } from './middleware.ts' -import { RequestData } from './next-request.ts' +import { NetlifyNextContext } from './next-request.ts' import { addBasePath, normalizeDataUrl, @@ -20,20 +20,18 @@ export interface FetchEventResult { interface BuildResponseOptions { context: Context - logger: StructuredLogger + nextContext?: NetlifyNextContext request: Request result: FetchEventResult - nextConfig?: RequestData['nextConfig'] - requestLocale?: string + logger: StructuredLogger } export const buildResponse = async ({ context, - logger, + nextContext, request, result, - nextConfig, - requestLocale, + logger, }: BuildResponseOptions): Promise => { logger .withFields({ is_nextresponse_next: result.response.headers.has('x-middleware-next') }) @@ -185,9 +183,9 @@ export const buildResponse = async ({ } // respect trailing slash rules to prevent 308s - rewriteUrl.pathname = normalizeTrailingSlash(rewriteUrl.pathname, nextConfig?.trailingSlash) + rewriteUrl.pathname = normalizeTrailingSlash(rewriteUrl.pathname, nextContext?.trailingSlash) - const target = normalizeLocalizedTarget({ target: rewriteUrl.toString(), request, nextConfig }) + const target = normalizeLocalizedTarget({ target: rewriteUrl.toString(), request, nextContext }) if (target === request.url) { logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url') return @@ -198,8 +196,8 @@ export const buildResponse = async ({ } // If we are redirecting a request that had a locale in the URL, we need to add it back in - if (redirect && requestLocale) { - redirect = normalizeLocalizedTarget({ target: redirect, request, nextConfig, requestLocale }) + if (redirect && nextContext?.detectedLocale) { + redirect = normalizeLocalizedTarget({ target: redirect, request, nextContext }) if (redirect === request.url) { logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url') return @@ -233,28 +231,26 @@ export const buildResponse = async ({ function normalizeLocalizedTarget({ target, request, - nextConfig, - requestLocale, + nextContext, }: { target: string request: Request - nextConfig?: RequestData['nextConfig'] - requestLocale?: string + nextContext?: NetlifyNextContext }) { const targetUrl = new URL(target, request.url) - const normalizedTarget = normalizeLocalePath(targetUrl.pathname, nextConfig?.i18n?.locales) + const normalizedTarget = normalizeLocalePath(targetUrl.pathname, nextContext?.i18n?.locales) - const locale = normalizedTarget.detectedLocale ?? requestLocale + const locale = normalizedTarget.detectedLocale ?? nextContext?.detectedLocale if ( locale && !normalizedTarget.pathname.startsWith(`/api/`) && !normalizedTarget.pathname.startsWith(`/_next/static/`) ) { targetUrl.pathname = - addBasePath(`/${locale}${normalizedTarget.pathname}`, nextConfig?.basePath) || `/` + addBasePath(`/${locale}${normalizedTarget.pathname}`, nextContext?.basePath) || `/` } else { - targetUrl.pathname = addBasePath(normalizedTarget.pathname, nextConfig?.basePath) || `/` + targetUrl.pathname = addBasePath(normalizedTarget.pathname, nextContext?.basePath) || `/` } return targetUrl.toString() } diff --git a/edge-runtime/lib/routing.ts b/edge-runtime/lib/routing.ts index 4619fda369..10cd261e41 100644 --- a/edge-runtime/lib/routing.ts +++ b/edge-runtime/lib/routing.ts @@ -7,8 +7,8 @@ import type { Key } from '../vendor/deno.land/x/path_to_regexp@v6.2.1/index.ts' -import { compile, pathToRegexp } from '../vendor/deno.land/x/path_to_regexp@v6.2.1/index.ts' import { getCookies } from '../vendor/deno.land/std@0.175.0/http/cookie.ts' +import { compile, pathToRegexp } from '../vendor/deno.land/x/path_to_regexp@v6.2.1/index.ts' /* ┌─────────────────────────────────────────────────────────────────────────┐ diff --git a/edge-runtime/lib/util.ts b/edge-runtime/lib/util.ts index 2bc11cd2e8..06e8f292eb 100644 --- a/edge-runtime/lib/util.ts +++ b/edge-runtime/lib/util.ts @@ -29,6 +29,13 @@ export const addBasePath = (path: string, basePath?: string) => { return path } +export const addLocale = (path: string, locale?: string) => { + if (locale && path !== `/${locale}` && !path.startsWith(`/${locale}/`)) { + return `/${locale}${path}` + } + return path +} + // https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/i18n/normalize-locale-path.ts export interface PathLocale { diff --git a/edge-runtime/middleware.ts b/edge-runtime/middleware.ts index f0170b912d..7553979ace 100644 --- a/edge-runtime/middleware.ts +++ b/edge-runtime/middleware.ts @@ -5,7 +5,7 @@ import nextConfig from './next.config.json' with { type: 'json' } import { InternalHeaders } from './lib/headers.ts' import { logger, LogLevel } from './lib/logging.ts' -import { buildNextRequest, RequestData } from './lib/next-request.ts' +import { buildNextRequest, NetlifyNextRequest } from './lib/next-request.ts' import { buildResponse, FetchEventResult } from './lib/response.ts' import { getMiddlewareRouteMatcher, @@ -13,7 +13,7 @@ import { type MiddlewareRouteMatch, } from './lib/routing.ts' -type NextHandler = (params: { request: RequestData }) => Promise +type NextHandler = (params: { request: NetlifyNextRequest }) => Promise const matchesMiddleware: MiddlewareRouteMatch = getMiddlewareRouteMatcher(matchers || []) @@ -31,7 +31,6 @@ export async function handleMiddleware( context: Context, nextHandler: NextHandler, ) { - const nextRequest = buildNextRequest(request, context, nextConfig) const url = new URL(request.url) const reqLogger = logger .withLogLevel( @@ -40,13 +39,15 @@ export async function handleMiddleware( .withFields({ url_path: url.pathname }) .withRequestID(request.headers.get(InternalHeaders.NFRequestID)) + const { nextRequest, nextContext } = buildNextRequest(request, context, nextConfig) + const localizedPath = new URL(nextContext.localizedUrl).pathname + // While we have already checked the path when mapping to the edge function, // Next.js supports extra rules that we need to check here too, because we // might be running an edge function for a path we should not. If we find // that's the case, short-circuit the execution. - if (!matchesMiddleware(url.pathname, request, searchParamsToUrlQuery(url.searchParams))) { + if (!matchesMiddleware(localizedPath, request, searchParamsToUrlQuery(url.searchParams))) { reqLogger.debug('Aborting middleware due to runtime rules') - return } @@ -54,11 +55,10 @@ export async function handleMiddleware( const result = await nextHandler({ request: nextRequest }) const response = await buildResponse({ context, - logger: reqLogger, + nextContext, request, result, - requestLocale: nextRequest.detectedLocale, - nextConfig, + logger: reqLogger, }) return response diff --git a/src/build/functions/edge.ts b/src/build/functions/edge.ts index 77c79bf2e3..4c95ad1353 100644 --- a/src/build/functions/edge.ts +++ b/src/build/functions/edge.ts @@ -65,10 +65,7 @@ const writeHandlerFile = async (ctx: PluginContext, { matchers, name }: NextDefi // Writing a file with the matchers that should trigger this function. We'll // read this file from the function at runtime. - await writeFile( - join(handlerRuntimeDirectory, 'matchers.json'), - JSON.stringify(augmentMatchers(matchers, ctx)), - ) + await writeFile(join(handlerRuntimeDirectory, 'matchers.json'), JSON.stringify(matchers)) // The config is needed by the edge function to match and normalize URLs. To // avoid shipping and parsing a large file at runtime, let's strip it down to From 49fe9adb8cc5372f2ef9f991719c779b88651dbc Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Thu, 18 Jul 2024 11:56:43 +0100 Subject: [PATCH 02/17] fix: ensure adding locale is case sensitive --- edge-runtime/lib/util.ts | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/edge-runtime/lib/util.ts b/edge-runtime/lib/util.ts index 06e8f292eb..2d06bc6bd7 100644 --- a/edge-runtime/lib/util.ts +++ b/edge-runtime/lib/util.ts @@ -30,7 +30,11 @@ export const addBasePath = (path: string, basePath?: string) => { } export const addLocale = (path: string, locale?: string) => { - if (locale && path !== `/${locale}` && !path.startsWith(`/${locale}/`)) { + if ( + locale && + path.toLowerCase() !== `/${locale.toLowerCase()}` && + !path.toLowerCase().startsWith(`/${locale.toLowerCase()}/`) + ) { return `/${locale}${path}` } return path @@ -54,18 +58,27 @@ export interface PathLocale { */ export function normalizeLocalePath(pathname: string, locales?: string[]): PathLocale { let detectedLocale: string | undefined - // first item will be empty string from splitting at first char - const pathnameParts = pathname.split('/') - - ;(locales || []).some((locale) => { - if (pathnameParts[1] && pathnameParts[1].toLowerCase() === locale.toLowerCase()) { - detectedLocale = locale - pathnameParts.splice(1, 1) - pathname = pathnameParts.join('/') - return true + + // normalize the locales to lowercase + const normalizedLocales = locales?.map((loc) => loc.toLowerCase()) + + // split the pathname into parts, removing the leading slash + const pathnameParts = pathname.substring(1).split('/') + + // split the first part of the pathname to check if it's a locale + const localeParts = pathnameParts[0].toLowerCase().split('-') + + // check if the first part of the pathname is a locale + // by matching the given locales, with decreasing specificity + for (let i = localeParts.length; i > 0; i--) { + const localePart = localeParts.slice(0, i).join('-') + if (normalizedLocales?.includes(localePart)) { + detectedLocale = localeParts.join('-') + pathname = `/${pathnameParts.slice(1).join('/')}` + break } - return false - }) + } + return { pathname, detectedLocale, From 61ff711b29b557d176165c20839ebd69b008a5c8 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Thu, 18 Jul 2024 11:57:35 +0100 Subject: [PATCH 03/17] test: update expect message --- tests/integration/edge-handler.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration/edge-handler.test.ts b/tests/integration/edge-handler.test.ts index 9aca477dfb..1be250df64 100644 --- a/tests/integration/edge-handler.test.ts +++ b/tests/integration/edge-handler.test.ts @@ -267,7 +267,10 @@ describe("aborts middleware execution when the matcher conditions don't match th origin, url: path, }) - expect(response.headers.has('x-hello-from-middleware-res'), `does match ${path}`).toBeTruthy() + expect( + response.headers.has('x-hello-from-middleware-res'), + `does not match ${path}`, + ).toBeTruthy() expect(await response.text()).toBe('Hello from origin!') expect(response.status).toBe(200) } From 116080103201aa8c4e3aae57683cd80b56fc3bd4 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Thu, 18 Jul 2024 11:58:16 +0100 Subject: [PATCH 04/17] chore: destructure localizedPath --- edge-runtime/middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edge-runtime/middleware.ts b/edge-runtime/middleware.ts index 7553979ace..e3445ff338 100644 --- a/edge-runtime/middleware.ts +++ b/edge-runtime/middleware.ts @@ -40,7 +40,7 @@ export async function handleMiddleware( .withRequestID(request.headers.get(InternalHeaders.NFRequestID)) const { nextRequest, nextContext } = buildNextRequest(request, context, nextConfig) - const localizedPath = new URL(nextContext.localizedUrl).pathname + const { pathname: localizedPath } = new URL(nextContext.localizedUrl) // While we have already checked the path when mapping to the edge function, // Next.js supports extra rules that we need to check here too, because we From b0101923b341835b549702597b962f24a0bf3846 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Fri, 19 Jul 2024 15:24:04 +0100 Subject: [PATCH 05/17] feat: allow for locale fallbacks when adding locale prefix --- edge-runtime/lib/util.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/edge-runtime/lib/util.ts b/edge-runtime/lib/util.ts index 2d06bc6bd7..d9fb2b1a48 100644 --- a/edge-runtime/lib/util.ts +++ b/edge-runtime/lib/util.ts @@ -29,11 +29,13 @@ export const addBasePath = (path: string, basePath?: string) => { return path } +// add locale prefix if not present, allowing for locale fallbacks export const addLocale = (path: string, locale?: string) => { if ( locale && path.toLowerCase() !== `/${locale.toLowerCase()}` && - !path.toLowerCase().startsWith(`/${locale.toLowerCase()}/`) + !path.toLowerCase().startsWith(`/${locale.toLowerCase()}/`) && + !path.toLowerCase().startsWith(`/${locale.toLowerCase()}-`) ) { return `/${locale}${path}` } From 74e2502552b157fbfa55fa02cc2e611fe39b00f7 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Mon, 22 Jul 2024 14:10:56 +0100 Subject: [PATCH 06/17] fix: pass NextConfig in NextRequest (needed even though it's not in the Next types) --- edge-runtime/lib/next-request.ts | 65 +++++++++++--------------------- edge-runtime/lib/response.ts | 39 ++++++++++--------- edge-runtime/middleware.ts | 12 +++--- 3 files changed, 49 insertions(+), 67 deletions(-) diff --git a/edge-runtime/lib/next-request.ts b/edge-runtime/lib/next-request.ts index fa4bce99c4..423db9dea4 100644 --- a/edge-runtime/lib/next-request.ts +++ b/edge-runtime/lib/next-request.ts @@ -10,24 +10,12 @@ import { } from './util.ts' import type { NextConfig } from 'next/dist/server/config-shared' -import type { NextRequest } from 'next/server' - -export type NetlifyNextRequest = Pick< - NextRequest, - 'url' | 'headers' | 'geo' | 'ip' | 'method' | 'body' -> - -export type NetlifyNextContext = { - localizedUrl: string - detectedLocale?: string - i18n?: NextConfig['i18n'] - basePath?: NextConfig['basePath'] - trailingSlash?: NextConfig['trailingSlash'] -} +import type { NextRequest, RequestInit } from 'next/dist/server/web/spec-extension/request.js' -const normalizeRequestURL = (originalURL: string, nextConfig?: NextConfig): string => { - const url = new URL(originalURL) +export type NetlifyNextRequest = RequestInit & + Pick +const normalizeRequest = (url: URL, nextConfig?: NextConfig): URL => { url.pathname = removeBasePath(url.pathname, nextConfig?.basePath) // We want to run middleware for data requests and expose the URL of the @@ -41,15 +29,13 @@ const normalizeRequestURL = (originalURL: string, nextConfig?: NextConfig): stri url.pathname = addBasePath(url.pathname, nextConfig?.basePath) - return url.toString() + return url } -const localizeRequestURL = ( - originalURL: string, +export const localizeRequest = ( + url: URL, nextConfig?: NextConfig, -): { localizedUrl: string; detectedLocale?: string } => { - const url = new URL(originalURL) - +): { localizedUrl: URL; locale?: string } => { url.pathname = removeBasePath(url.pathname, nextConfig?.basePath) // Detect the locale from the URL @@ -61,8 +47,8 @@ const localizeRequestURL = ( url.pathname = addBasePath(url.pathname, nextConfig?.basePath) return { - localizedUrl: url.toString(), - detectedLocale, + localizedUrl: url, + locale: detectedLocale, } } @@ -70,19 +56,18 @@ export const buildNextRequest = ( request: Request, context: Context, nextConfig?: NextConfig, -): { nextRequest: NetlifyNextRequest; nextContext: NetlifyNextContext } => { - const { url, method, body, headers } = request +): NetlifyNextRequest => { + const { method, body, headers } = request const { country, subdivision, city, latitude, longitude } = context.geo const { i18n, basePath, trailingSlash } = nextConfig ?? {} + const url = new URL(request.url) const normalizedUrl = nextConfig?.skipMiddlewareUrlNormalize ? url - : normalizeRequestURL(url, nextConfig) - - const { localizedUrl, detectedLocale } = localizeRequestURL(normalizedUrl, nextConfig) + : normalizeRequest(url, nextConfig) - const nextRequest: NetlifyNextRequest = { - url: normalizedUrl, + return { + url: normalizedUrl.toString(), headers, geo: { city, @@ -94,18 +79,10 @@ export const buildNextRequest = ( ip: context.ip, method, body, - } - - const nextContext = { - localizedUrl, - detectedLocale, - i18n, - trailingSlash, - basePath, - } - - return { - nextRequest, - nextContext, + nextConfig: { + i18n, + basePath, + trailingSlash, + }, } } diff --git a/edge-runtime/lib/response.ts b/edge-runtime/lib/response.ts index 302893ddc2..9c5c98bdc9 100644 --- a/edge-runtime/lib/response.ts +++ b/edge-runtime/lib/response.ts @@ -4,7 +4,7 @@ import { HTMLRewriter } from '../vendor/deno.land/x/html_rewriter@v0.1.0-pre.17/ import { updateModifiedHeaders } from './headers.ts' import type { StructuredLogger } from './logging.ts' import { addMiddlewareHeaders, isMiddlewareRequest, isMiddlewareResponse } from './middleware.ts' -import { NetlifyNextContext } from './next-request.ts' +import { NetlifyNextRequest } from './next-request.ts' import { addBasePath, normalizeDataUrl, @@ -20,7 +20,8 @@ export interface FetchEventResult { interface BuildResponseOptions { context: Context - nextContext?: NetlifyNextContext + config?: NetlifyNextRequest['nextConfig'] + locale?: string request: Request result: FetchEventResult logger: StructuredLogger @@ -28,7 +29,8 @@ interface BuildResponseOptions { export const buildResponse = async ({ context, - nextContext, + config, + locale, request, result, logger, @@ -183,9 +185,9 @@ export const buildResponse = async ({ } // respect trailing slash rules to prevent 308s - rewriteUrl.pathname = normalizeTrailingSlash(rewriteUrl.pathname, nextContext?.trailingSlash) + rewriteUrl.pathname = normalizeTrailingSlash(rewriteUrl.pathname, config?.trailingSlash) - const target = normalizeLocalizedTarget({ target: rewriteUrl.toString(), request, nextContext }) + const target = normalizeLocalizedTarget({ target: rewriteUrl.toString(), request, config }) if (target === request.url) { logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url') return @@ -196,8 +198,8 @@ export const buildResponse = async ({ } // If we are redirecting a request that had a locale in the URL, we need to add it back in - if (redirect && nextContext?.detectedLocale) { - redirect = normalizeLocalizedTarget({ target: redirect, request, nextContext }) + if (redirect && locale) { + redirect = normalizeLocalizedTarget({ target: redirect, request, config }) if (redirect === request.url) { logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url') return @@ -231,26 +233,27 @@ export const buildResponse = async ({ function normalizeLocalizedTarget({ target, request, - nextContext, + config, + locale, }: { target: string request: Request - nextContext?: NetlifyNextContext + config?: NetlifyNextRequest['nextConfig'] + locale?: string }) { const targetUrl = new URL(target, request.url) + const normalizedTarget = normalizeLocalePath(targetUrl.pathname, config?.i18n?.locales) + const targetPathname = normalizedTarget.pathname + const targetLocale = normalizedTarget.detectedLocale ?? locale - const normalizedTarget = normalizeLocalePath(targetUrl.pathname, nextContext?.i18n?.locales) - - const locale = normalizedTarget.detectedLocale ?? nextContext?.detectedLocale if ( - locale && - !normalizedTarget.pathname.startsWith(`/api/`) && - !normalizedTarget.pathname.startsWith(`/_next/static/`) + targetLocale && + !targetPathname.startsWith(`/api/`) && + !targetPathname.startsWith(`/_next/static/`) ) { - targetUrl.pathname = - addBasePath(`/${locale}${normalizedTarget.pathname}`, nextContext?.basePath) || `/` + targetUrl.pathname = addBasePath(`/${targetLocale}${targetPathname}`, config?.basePath) || `/` } else { - targetUrl.pathname = addBasePath(normalizedTarget.pathname, nextContext?.basePath) || `/` + targetUrl.pathname = addBasePath(targetPathname, config?.basePath) || `/` } return targetUrl.toString() } diff --git a/edge-runtime/middleware.ts b/edge-runtime/middleware.ts index e3445ff338..d1c0510133 100644 --- a/edge-runtime/middleware.ts +++ b/edge-runtime/middleware.ts @@ -5,7 +5,7 @@ import nextConfig from './next.config.json' with { type: 'json' } import { InternalHeaders } from './lib/headers.ts' import { logger, LogLevel } from './lib/logging.ts' -import { buildNextRequest, NetlifyNextRequest } from './lib/next-request.ts' +import { buildNextRequest, localizeRequest, NetlifyNextRequest } from './lib/next-request.ts' import { buildResponse, FetchEventResult } from './lib/response.ts' import { getMiddlewareRouteMatcher, @@ -39,14 +39,15 @@ export async function handleMiddleware( .withFields({ url_path: url.pathname }) .withRequestID(request.headers.get(InternalHeaders.NFRequestID)) - const { nextRequest, nextContext } = buildNextRequest(request, context, nextConfig) - const { pathname: localizedPath } = new URL(nextContext.localizedUrl) + const nextRequest = buildNextRequest(request, context, nextConfig) + const { localizedUrl, locale } = localizeRequest(url, nextConfig) + const query = searchParamsToUrlQuery(url.searchParams) // While we have already checked the path when mapping to the edge function, // Next.js supports extra rules that we need to check here too, because we // might be running an edge function for a path we should not. If we find // that's the case, short-circuit the execution. - if (!matchesMiddleware(localizedPath, request, searchParamsToUrlQuery(url.searchParams))) { + if (!matchesMiddleware(localizedUrl.pathname, request, query)) { reqLogger.debug('Aborting middleware due to runtime rules') return } @@ -55,7 +56,8 @@ export async function handleMiddleware( const result = await nextHandler({ request: nextRequest }) const response = await buildResponse({ context, - nextContext, + config: nextRequest.nextConfig, + locale, request, result, logger: reqLogger, From 71ed888de4d618d3911007de56062c439d0e11a7 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 23 Jul 2024 11:53:00 +0100 Subject: [PATCH 07/17] fix: headers need to be Node.js style plain object (even though Next types allow Headers object) --- edge-runtime/lib/next-request.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/edge-runtime/lib/next-request.ts b/edge-runtime/lib/next-request.ts index 423db9dea4..052ac7ed9f 100644 --- a/edge-runtime/lib/next-request.ts +++ b/edge-runtime/lib/next-request.ts @@ -13,7 +13,9 @@ import type { NextConfig } from 'next/dist/server/config-shared' import type { NextRequest, RequestInit } from 'next/dist/server/web/spec-extension/request.js' export type NetlifyNextRequest = RequestInit & - Pick + Pick & { + headers: HeadersInit + } const normalizeRequest = (url: URL, nextConfig?: NextConfig): URL => { url.pathname = removeBasePath(url.pathname, nextConfig?.basePath) @@ -68,7 +70,7 @@ export const buildNextRequest = ( return { url: normalizedUrl.toString(), - headers, + headers: Object.fromEntries(headers.entries()), geo: { city, country: country?.code, From 935e621d2b99e42eeb1a86273340ca8d0153c39a Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 23 Jul 2024 16:14:36 +0100 Subject: [PATCH 08/17] test: assert that paths don't match with an invalid prefix --- tests/integration/edge-handler.test.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/integration/edge-handler.test.ts b/tests/integration/edge-handler.test.ts index 1be250df64..572521af53 100644 --- a/tests/integration/edge-handler.test.ts +++ b/tests/integration/edge-handler.test.ts @@ -248,7 +248,7 @@ describe("aborts middleware execution when the matcher conditions don't match th expect(origin.calls).toBe(2) }) - test('should handle locale matching correctly', async (ctx) => { + test.only('should handle locale matching correctly', async (ctx) => { await createFixture('middleware-conditions', ctx) await runPlugin(ctx) @@ -275,16 +275,13 @@ describe("aborts middleware execution when the matcher conditions don't match th expect(response.status).toBe(200) } - for (const path of ['/hello/invalid', '/about', '/en/about']) { + for (const path of ['/hello/invalid', '/invalid/hello', '/about', '/en/about']) { const response = await invokeEdgeFunction(ctx, { functions: ['___netlify-edge-handler-middleware'], origin, url: path, }) - expect( - response.headers.has('x-hello-from-middleware-res'), - `does not match ${path}`, - ).toBeFalsy() + expect(response.headers.has('x-hello-from-middleware-res'), `does match ${path}`).toBeFalsy() expect(await response.text()).toBe('Hello from origin!') expect(response.status).toBe(200) } From 878f02203e1c4517144e645a8006ca00d57e5c8b Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 23 Jul 2024 17:30:11 +0100 Subject: [PATCH 09/17] test: remove only --- tests/integration/edge-handler.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/edge-handler.test.ts b/tests/integration/edge-handler.test.ts index 572521af53..f8ff32a37e 100644 --- a/tests/integration/edge-handler.test.ts +++ b/tests/integration/edge-handler.test.ts @@ -248,7 +248,7 @@ describe("aborts middleware execution when the matcher conditions don't match th expect(origin.calls).toBe(2) }) - test.only('should handle locale matching correctly', async (ctx) => { + test('should handle locale matching correctly', async (ctx) => { await createFixture('middleware-conditions', ctx) await runPlugin(ctx) From dccecf91ccb03c29df0504757f3ccd57b900ee1a Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 23 Jul 2024 17:30:39 +0100 Subject: [PATCH 10/17] chore: refactor for clarity --- edge-runtime/lib/response.ts | 21 +++++++++++---------- edge-runtime/middleware.ts | 9 ++++++--- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/edge-runtime/lib/response.ts b/edge-runtime/lib/response.ts index 9c5c98bdc9..b92aec9c37 100644 --- a/edge-runtime/lib/response.ts +++ b/edge-runtime/lib/response.ts @@ -20,7 +20,7 @@ export interface FetchEventResult { interface BuildResponseOptions { context: Context - config?: NetlifyNextRequest['nextConfig'] + nextConfig?: NetlifyNextRequest['nextConfig'] locale?: string request: Request result: FetchEventResult @@ -29,7 +29,7 @@ interface BuildResponseOptions { export const buildResponse = async ({ context, - config, + nextConfig, locale, request, result, @@ -185,9 +185,9 @@ export const buildResponse = async ({ } // respect trailing slash rules to prevent 308s - rewriteUrl.pathname = normalizeTrailingSlash(rewriteUrl.pathname, config?.trailingSlash) + rewriteUrl.pathname = normalizeTrailingSlash(rewriteUrl.pathname, nextConfig?.trailingSlash) - const target = normalizeLocalizedTarget({ target: rewriteUrl.toString(), request, config }) + const target = normalizeLocalizedTarget({ target: rewriteUrl.toString(), request, nextConfig }) if (target === request.url) { logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url') return @@ -199,7 +199,7 @@ export const buildResponse = async ({ // If we are redirecting a request that had a locale in the URL, we need to add it back in if (redirect && locale) { - redirect = normalizeLocalizedTarget({ target: redirect, request, config }) + redirect = normalizeLocalizedTarget({ target: redirect, request, nextConfig }) if (redirect === request.url) { logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url') return @@ -233,16 +233,16 @@ export const buildResponse = async ({ function normalizeLocalizedTarget({ target, request, - config, + nextConfig, locale, }: { target: string request: Request - config?: NetlifyNextRequest['nextConfig'] + nextConfig?: NetlifyNextRequest['nextConfig'] locale?: string }) { const targetUrl = new URL(target, request.url) - const normalizedTarget = normalizeLocalePath(targetUrl.pathname, config?.i18n?.locales) + const normalizedTarget = normalizeLocalePath(targetUrl.pathname, nextConfig?.i18n?.locales) const targetPathname = normalizedTarget.pathname const targetLocale = normalizedTarget.detectedLocale ?? locale @@ -251,9 +251,10 @@ function normalizeLocalizedTarget({ !targetPathname.startsWith(`/api/`) && !targetPathname.startsWith(`/_next/static/`) ) { - targetUrl.pathname = addBasePath(`/${targetLocale}${targetPathname}`, config?.basePath) || `/` + targetUrl.pathname = + addBasePath(`/${targetLocale}${targetPathname}`, nextConfig?.basePath) || `/` } else { - targetUrl.pathname = addBasePath(targetPathname, config?.basePath) || `/` + targetUrl.pathname = addBasePath(targetPathname, nextConfig?.basePath) || `/` } return targetUrl.toString() } diff --git a/edge-runtime/middleware.ts b/edge-runtime/middleware.ts index d1c0510133..96dbc8d9fa 100644 --- a/edge-runtime/middleware.ts +++ b/edge-runtime/middleware.ts @@ -32,6 +32,8 @@ export async function handleMiddleware( nextHandler: NextHandler, ) { const url = new URL(request.url) + const query = searchParamsToUrlQuery(url.searchParams) + const { localizedUrl, locale } = localizeRequest(url, nextConfig) const reqLogger = logger .withLogLevel( request.headers.has(InternalHeaders.NFDebugLogging) ? LogLevel.Debug : LogLevel.Log, @@ -39,9 +41,10 @@ export async function handleMiddleware( .withFields({ url_path: url.pathname }) .withRequestID(request.headers.get(InternalHeaders.NFRequestID)) + // Convert the incoming request to a Next.js request, which includes + // normalizing the URL, adding geo and IP information and converting + // the headers to a plain object, among other things. const nextRequest = buildNextRequest(request, context, nextConfig) - const { localizedUrl, locale } = localizeRequest(url, nextConfig) - const query = searchParamsToUrlQuery(url.searchParams) // While we have already checked the path when mapping to the edge function, // Next.js supports extra rules that we need to check here too, because we @@ -56,7 +59,7 @@ export async function handleMiddleware( const result = await nextHandler({ request: nextRequest }) const response = await buildResponse({ context, - config: nextRequest.nextConfig, + nextConfig: nextRequest.nextConfig, locale, request, result, From bfd3f5465ba57264f1712a8072d808d87894d0ac Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 23 Jul 2024 17:30:52 +0100 Subject: [PATCH 11/17] chore: revert red herring --- edge-runtime/lib/util.ts | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/edge-runtime/lib/util.ts b/edge-runtime/lib/util.ts index d9fb2b1a48..dc97794fef 100644 --- a/edge-runtime/lib/util.ts +++ b/edge-runtime/lib/util.ts @@ -60,27 +60,18 @@ export interface PathLocale { */ export function normalizeLocalePath(pathname: string, locales?: string[]): PathLocale { let detectedLocale: string | undefined - - // normalize the locales to lowercase - const normalizedLocales = locales?.map((loc) => loc.toLowerCase()) - - // split the pathname into parts, removing the leading slash - const pathnameParts = pathname.substring(1).split('/') - - // split the first part of the pathname to check if it's a locale - const localeParts = pathnameParts[0].toLowerCase().split('-') - - // check if the first part of the pathname is a locale - // by matching the given locales, with decreasing specificity - for (let i = localeParts.length; i > 0; i--) { - const localePart = localeParts.slice(0, i).join('-') - if (normalizedLocales?.includes(localePart)) { - detectedLocale = localeParts.join('-') - pathname = `/${pathnameParts.slice(1).join('/')}` - break + // first item will be empty string from splitting at first char + const pathnameParts = pathname.split('/') + + ;(locales || []).some((locale) => { + if (pathnameParts[1] && pathnameParts[1].toLowerCase() === locale.toLowerCase()) { + detectedLocale = locale + pathnameParts.splice(1, 1) + pathname = pathnameParts.join('/') + return true } - } - + return false + }) return { pathname, detectedLocale, From 1a5886eb698ec239924c66e4c5a6963e873e7701 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 23 Jul 2024 17:59:53 +0100 Subject: [PATCH 12/17] test: fix incorrect locale test --- tests/integration/edge-handler.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/edge-handler.test.ts b/tests/integration/edge-handler.test.ts index f8ff32a37e..8f26a851f4 100644 --- a/tests/integration/edge-handler.test.ts +++ b/tests/integration/edge-handler.test.ts @@ -261,7 +261,7 @@ describe("aborts middleware execution when the matcher conditions don't match th ctx.cleanup?.push(() => origin.stop()) - for (const path of ['/hello', '/en/hello', '/nl-NL/hello', '/nl-NL/about']) { + for (const path of ['/hello', '/en/hello', '/nl-NL/about']) { const response = await invokeEdgeFunction(ctx, { functions: ['___netlify-edge-handler-middleware'], origin, From 8e691dc4b65ef630a27f8170c443e9f92b89d06c Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 23 Jul 2024 18:00:27 +0100 Subject: [PATCH 13/17] chore: refactor NetlifyNextRequest type for clarity --- edge-runtime/lib/next-request.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/edge-runtime/lib/next-request.ts b/edge-runtime/lib/next-request.ts index 052ac7ed9f..cf5d12c56e 100644 --- a/edge-runtime/lib/next-request.ts +++ b/edge-runtime/lib/next-request.ts @@ -12,8 +12,8 @@ import { import type { NextConfig } from 'next/dist/server/config-shared' import type { NextRequest, RequestInit } from 'next/dist/server/web/spec-extension/request.js' -export type NetlifyNextRequest = RequestInit & - Pick & { +export type NetlifyNextRequest = Partial> & + RequestInit & { headers: HeadersInit } From a7620fdd9009f25fb7820dd43828a30aa22ff434 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 23 Jul 2024 18:21:14 +0100 Subject: [PATCH 14/17] test: fix faulty i18n test logic --- tests/fixtures/middleware-conditions/middleware.ts | 2 +- tests/integration/edge-handler.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/fixtures/middleware-conditions/middleware.ts b/tests/fixtures/middleware-conditions/middleware.ts index d5a3c51045..fdb332cf8e 100644 --- a/tests/fixtures/middleware-conditions/middleware.ts +++ b/tests/fixtures/middleware-conditions/middleware.ts @@ -19,7 +19,7 @@ export const config = { source: '/hello', }, { - source: '/nl-NL/about', + source: '/nl/about', locale: false, }, ], diff --git a/tests/integration/edge-handler.test.ts b/tests/integration/edge-handler.test.ts index 8f26a851f4..9353ca4415 100644 --- a/tests/integration/edge-handler.test.ts +++ b/tests/integration/edge-handler.test.ts @@ -261,7 +261,7 @@ describe("aborts middleware execution when the matcher conditions don't match th ctx.cleanup?.push(() => origin.stop()) - for (const path of ['/hello', '/en/hello', '/nl-NL/about']) { + for (const path of ['/hello', '/en/hello', '/es/hello', '/nl/about']) { const response = await invokeEdgeFunction(ctx, { functions: ['___netlify-edge-handler-middleware'], origin, @@ -275,7 +275,7 @@ describe("aborts middleware execution when the matcher conditions don't match th expect(response.status).toBe(200) } - for (const path of ['/hello/invalid', '/invalid/hello', '/about', '/en/about']) { + for (const path of ['/hello/invalid', '/invalid/hello', '/about', '/en/about', '/es/about']) { const response = await invokeEdgeFunction(ctx, { functions: ['___netlify-edge-handler-middleware'], origin, From da8215f0d9d05aef90526e957ced6447f8567c0c Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Wed, 24 Jul 2024 17:38:43 +0100 Subject: [PATCH 15/17] chore: refactor to remove normalizeLocalizedTarget --- edge-runtime/lib/response.ts | 56 ++---------------------------------- edge-runtime/lib/util.ts | 3 +- 2 files changed, 4 insertions(+), 55 deletions(-) diff --git a/edge-runtime/lib/response.ts b/edge-runtime/lib/response.ts index b92aec9c37..daea42905e 100644 --- a/edge-runtime/lib/response.ts +++ b/edge-runtime/lib/response.ts @@ -5,13 +5,7 @@ import { updateModifiedHeaders } from './headers.ts' import type { StructuredLogger } from './logging.ts' import { addMiddlewareHeaders, isMiddlewareRequest, isMiddlewareResponse } from './middleware.ts' import { NetlifyNextRequest } from './next-request.ts' -import { - addBasePath, - normalizeDataUrl, - normalizeLocalePath, - normalizeTrailingSlash, - relativizeURL, -} from './util.ts' +import { normalizeDataUrl, normalizeTrailingSlash, relativizeURL } from './util.ts' export interface FetchEventResult { response: Response @@ -187,26 +181,12 @@ export const buildResponse = async ({ // respect trailing slash rules to prevent 308s rewriteUrl.pathname = normalizeTrailingSlash(rewriteUrl.pathname, nextConfig?.trailingSlash) - const target = normalizeLocalizedTarget({ target: rewriteUrl.toString(), request, nextConfig }) - if (target === request.url) { - logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url') - return - } + const target = rewriteUrl.toString() res.headers.set('x-middleware-rewrite', relativeUrl) request.headers.set('x-middleware-rewrite', target) return addMiddlewareHeaders(context.rewrite(target), res) } - // If we are redirecting a request that had a locale in the URL, we need to add it back in - if (redirect && locale) { - redirect = normalizeLocalizedTarget({ target: redirect, request, nextConfig }) - if (redirect === request.url) { - logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url') - return - } - res.headers.set('location', redirect) - } - // Data requests shouldn't automatically redirect in the browser (they might be HTML pages): they're handled by the router if (redirect && isDataReq) { res.headers.delete('location') @@ -226,35 +206,3 @@ export const buildResponse = async ({ return res } - -/** - * Normalizes the locale in a URL. - */ -function normalizeLocalizedTarget({ - target, - request, - nextConfig, - locale, -}: { - target: string - request: Request - nextConfig?: NetlifyNextRequest['nextConfig'] - locale?: string -}) { - const targetUrl = new URL(target, request.url) - const normalizedTarget = normalizeLocalePath(targetUrl.pathname, nextConfig?.i18n?.locales) - const targetPathname = normalizedTarget.pathname - const targetLocale = normalizedTarget.detectedLocale ?? locale - - if ( - targetLocale && - !targetPathname.startsWith(`/api/`) && - !targetPathname.startsWith(`/_next/static/`) - ) { - targetUrl.pathname = - addBasePath(`/${targetLocale}${targetPathname}`, nextConfig?.basePath) || `/` - } else { - targetUrl.pathname = addBasePath(targetPathname, nextConfig?.basePath) || `/` - } - return targetUrl.toString() -} diff --git a/edge-runtime/lib/util.ts b/edge-runtime/lib/util.ts index dc97794fef..28fea98eaa 100644 --- a/edge-runtime/lib/util.ts +++ b/edge-runtime/lib/util.ts @@ -35,7 +35,8 @@ export const addLocale = (path: string, locale?: string) => { locale && path.toLowerCase() !== `/${locale.toLowerCase()}` && !path.toLowerCase().startsWith(`/${locale.toLowerCase()}/`) && - !path.toLowerCase().startsWith(`/${locale.toLowerCase()}-`) + !path.startsWith(`/api/`) && + !path.startsWith(`/_next/static/`) ) { return `/${locale}${path}` } From fb8d9ea93cfdd41fa3fc2e5dcb9d787da818f8cf Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Thu, 25 Jul 2024 09:39:30 +0100 Subject: [PATCH 16/17] test: update failure messages for clarity --- tests/integration/edge-handler.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/integration/edge-handler.test.ts b/tests/integration/edge-handler.test.ts index 9353ca4415..9ca3a633aa 100644 --- a/tests/integration/edge-handler.test.ts +++ b/tests/integration/edge-handler.test.ts @@ -269,7 +269,7 @@ describe("aborts middleware execution when the matcher conditions don't match th }) expect( response.headers.has('x-hello-from-middleware-res'), - `does not match ${path}`, + `should match ${path}`, ).toBeTruthy() expect(await response.text()).toBe('Hello from origin!') expect(response.status).toBe(200) @@ -281,7 +281,10 @@ describe("aborts middleware execution when the matcher conditions don't match th origin, url: path, }) - expect(response.headers.has('x-hello-from-middleware-res'), `does match ${path}`).toBeFalsy() + expect( + response.headers.has('x-hello-from-middleware-res'), + `should not match ${path}`, + ).toBeFalsy() expect(await response.text()).toBe('Hello from origin!') expect(response.status).toBe(200) } From af173a6969b1e7aaf72984aeb355f47652bc0dbb Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Thu, 25 Jul 2024 11:21:49 +0100 Subject: [PATCH 17/17] chore: update e2e test command docs --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a3fceee08..ce9bd1a608 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,8 +93,8 @@ given prefix, run `npm run pretest -- `. > Needs the `netlify-cli` installed and being logged in having access to Netlify Testing > Organization or providing your own site ID with NETLIFY_SITE_ID environment variable. -The e2e tests can be invoked with `npm run e2e` and perform a full e2e test. This means they do the -following: +The e2e tests can be invoked with `npm run test:e2e` and perform a full e2e test. This means they do +the following: 1. Building the next-runtime (just running `npm run build` in the repository) 2. Creating a temp directory and copying the provided fixture over to the directory. @@ -113,7 +113,7 @@ following: purposes. > [!TIP] If you'd like to always keep the deployment and the local fixture around for -> troubleshooting, run `E2E_PERSIST=1 npm run e2e`. +> troubleshooting, run `E2E_PERSIST=1 npm run test:e2e`. ### Next.js tests