@netlify/plugin-nextjs
Advanced tools
Comparing version 5.0.0-beta.2 to 5.0.0-beta.3
@@ -10,3 +10,3 @@ | ||
saveBuildCache | ||
} from "../esm-chunks/chunk-GGHAQM5D.js"; | ||
} from "../esm-chunks/chunk-XQ65S4R2.js"; | ||
import "../esm-chunks/chunk-RSKIKBZH.js"; | ||
@@ -13,0 +13,0 @@ export { |
@@ -9,3 +9,4 @@ | ||
createEdgeHandlers | ||
} from "../../esm-chunks/chunk-3PTPU5GO.js"; | ||
} from "../../esm-chunks/chunk-FFCTA32Q.js"; | ||
import "../../esm-chunks/chunk-AVWFCGVE.js"; | ||
import "../../esm-chunks/chunk-TJKO3X6O.js"; | ||
@@ -12,0 +13,0 @@ import "../../esm-chunks/chunk-RSKIKBZH.js"; |
@@ -19,3 +19,3 @@ | ||
createEdgeHandlers | ||
} from "./esm-chunks/chunk-3PTPU5GO.js"; | ||
} from "./esm-chunks/chunk-FFCTA32Q.js"; | ||
import { | ||
@@ -29,3 +29,3 @@ createServerHandler | ||
saveBuildCache | ||
} from "./esm-chunks/chunk-GGHAQM5D.js"; | ||
} from "./esm-chunks/chunk-XQ65S4R2.js"; | ||
import { | ||
@@ -32,0 +32,0 @@ setImageConfig |
@@ -11,3 +11,3 @@ | ||
setRunConfig | ||
} from "../esm-chunks/chunk-R4NHZWGU.js"; | ||
} from "../esm-chunks/chunk-OIL5MDCV.js"; | ||
import "../esm-chunks/chunk-4AJYXTWN.js"; | ||
@@ -14,0 +14,0 @@ import "../esm-chunks/chunk-RSKIKBZH.js"; |
@@ -8,9 +8,10 @@ | ||
import { | ||
StructuredLogger, | ||
logger | ||
} from "../esm-chunks/chunk-YZXA5QBC.js"; | ||
import_internal | ||
} from "../esm-chunks/chunk-GHDGGK6V.js"; | ||
import "../esm-chunks/chunk-RSKIKBZH.js"; | ||
var export_LogLevel = import_internal.LogLevel; | ||
var export_logger = import_internal.systemLogger; | ||
export { | ||
StructuredLogger, | ||
logger | ||
export_LogLevel as LogLevel, | ||
export_logger as logger | ||
}; |
@@ -0,1 +1,6 @@ | ||
export const InternalHeaders = { | ||
NFDebugLogging: 'x-nf-debug-logging', | ||
NFRequestID: 'x-nf-request-id', | ||
} | ||
// Next 13 supports request header mutations and has the side effect of prepending header values with 'x-middleware-request' | ||
@@ -2,0 +7,0 @@ // as part of invoking NextResponse.next() in the middleware. We need to remove that before sending the response the user |
import type { Context } from '@netlify/edge-functions' | ||
import { normalizeDataUrl, removeBasePath, normalizeLocalePath } from './util.ts' | ||
interface I18NConfig { | ||
@@ -32,4 +34,41 @@ defaultLocale: string | ||
body?: ReadableStream<Uint8Array> | ||
detectedLocale?: string | ||
} | ||
const normalizeRequestURL = ( | ||
originalURL: string, | ||
nextConfig?: RequestData['nextConfig'], | ||
): { url: string; detectedLocale?: string } => { | ||
const url = new URL(originalURL) | ||
url.pathname = removeBasePath(url.pathname, nextConfig?.basePath) | ||
let detectedLocale: string | undefined | ||
if (nextConfig?.i18n) { | ||
const { pathname, detectedLocale: detected } = normalizeLocalePath( | ||
url.pathname, | ||
nextConfig?.i18n?.locales, | ||
) | ||
url.pathname = pathname | ||
detectedLocale = detected | ||
} | ||
// 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) | ||
// Normalizing the trailing slash based on the `trailingSlash` configuration | ||
// property from the Next.js config. | ||
if (nextConfig?.trailingSlash && url.pathname !== '/' && !url.pathname.endsWith('/')) { | ||
url.pathname = `${url.pathname}/` | ||
} | ||
return { | ||
url: url.toString(), | ||
detectedLocale, | ||
} | ||
} | ||
export const buildNextRequest = ( | ||
@@ -51,6 +90,8 @@ request: Request, | ||
const { detectedLocale, url: normalizedUrl } = normalizeRequestURL(url, nextConfig) | ||
return { | ||
headers: Object.fromEntries(headers.entries()), | ||
geo, | ||
url, | ||
url: normalizedUrl, | ||
method, | ||
@@ -60,3 +101,4 @@ ip: context.ip, | ||
nextConfig, | ||
detectedLocale, | ||
} | ||
} |
@@ -5,4 +5,6 @@ import type { Context } from '@netlify/edge-functions' | ||
import { updateModifiedHeaders } from './headers.ts' | ||
import { normalizeDataUrl, relativizeURL } from './util.ts' | ||
import type { StructuredLogger } from './logging.ts' | ||
import { normalizeDataUrl, normalizeLocalePath, relativizeURL, rewriteDataPath } from './util.ts' | ||
import { addMiddlewareHeaders, isMiddlewareRequest, isMiddlewareResponse } from './middleware.ts' | ||
import { RequestData } from './next-request.ts' | ||
@@ -14,11 +16,23 @@ export interface FetchEventResult { | ||
interface BuildResponseOptions { | ||
context: Context | ||
logger: StructuredLogger | ||
request: Request | ||
result: FetchEventResult | ||
nextConfig?: RequestData['nextConfig'] | ||
requestLocale?: string | ||
} | ||
export const buildResponse = async ({ | ||
context, | ||
logger, | ||
request, | ||
result, | ||
request, | ||
context, | ||
}: { | ||
result: FetchEventResult | ||
request: Request | ||
context: Context | ||
}) => { | ||
nextConfig, | ||
requestLocale, | ||
}: BuildResponseOptions): Promise<Response | void> => { | ||
logger | ||
.withFields({ is_nextresponse_next: result.response.headers.has('x-middleware-next') }) | ||
.debug('Building Next.js response') | ||
updateModifiedHeaders(request.headers, result.response.headers) | ||
@@ -104,29 +118,76 @@ | ||
// Data requests (i.e. requests for /_next/data ) need special handling | ||
const isDataReq = request.headers.get('x-nextjs-data') | ||
const isDataReq = request.headers.has('x-nextjs-data') | ||
if (rewrite) { | ||
logger.withFields({ rewrite_url: rewrite }).debug('Found middleware rewrite') | ||
const rewriteUrl = new URL(rewrite, request.url) | ||
const baseUrl = new URL(request.url) | ||
if (rewriteUrl.toString() === baseUrl.toString()) { | ||
logger.withFields({ rewrite_url: rewrite }).debug('Rewrite url is same as original url') | ||
return | ||
} | ||
const relativeUrl = relativizeURL(rewrite, request.url) | ||
const originalPath = new URL(request.url, `http://n`).pathname | ||
// Data requests might be rewritten to an external URL | ||
// This header tells the client router the redirect target, and if it's external then it will do a full navigation | ||
if (isDataReq) { | ||
// Data requests might be rewritten to an external URL | ||
// This header tells the client router the redirect target, and if it's external then it will do a full navigation | ||
res.headers.set('x-nextjs-rewrite', relativeUrl) | ||
} | ||
if (rewriteUrl.origin !== baseUrl.origin) { | ||
// Netlify Edge Functions don't support proxying to external domains, but Next middleware does | ||
const proxied = fetch(new Request(rewriteUrl.toString(), request)) | ||
return addMiddlewareHeaders(proxied, res) | ||
logger.withFields({ rewrite_url: rewrite }).debug('Rewriting to external url') | ||
let proxyRequest: Request | ||
// Remove Netlify internal headers | ||
const headers = new Headers( | ||
[...request.headers.entries()].filter(([key]) => !key.startsWith('x-nf-')), | ||
) | ||
if (request.body && !request.bodyUsed) { | ||
// This is not ideal, but streaming to an external URL doesn't work | ||
const body = await request.arrayBuffer() | ||
proxyRequest = new Request(rewriteUrl, { | ||
headers, | ||
method: request.method, | ||
body, | ||
}) | ||
} else { | ||
proxyRequest = new Request(rewriteUrl, { | ||
headers, | ||
method: request.method, | ||
}) | ||
} | ||
return addMiddlewareHeaders(fetch(proxyRequest), res) | ||
} else if (isDataReq) { | ||
rewriteUrl.pathname = rewriteDataPath({ | ||
dataUrl: originalPath, | ||
newRoute: rewriteUrl.pathname, | ||
basePath: nextConfig?.basePath, | ||
}) | ||
} | ||
res.headers.set('x-middleware-rewrite', relativeUrl) | ||
request.headers.set('x-original-path', new URL(request.url, `http://n`).pathname) | ||
request.headers.set('x-middleware-rewrite', rewrite) | ||
return addMiddlewareHeaders(fetch(new Request(rewriteUrl, request)), res) | ||
} | ||
return addMiddlewareHeaders(context.rewrite(rewrite), res) | ||
let redirect = res.headers.get('location') | ||
// If we are redirecting a request that had a locale in the URL, we need to add it back in | ||
if (redirect && requestLocale) { | ||
const redirectUrl = new URL(redirect, request.url) | ||
const normalizedRedirect = normalizeLocalePath(redirectUrl.pathname, nextConfig?.i18n?.locales) | ||
const locale = normalizedRedirect.detectedLocale ?? requestLocale | ||
// Pages router API routes don't have a locale in the URL | ||
if (locale && !redirectUrl.pathname.startsWith(`/api/`)) { | ||
redirectUrl.pathname = `/${locale}${normalizedRedirect.pathname}` | ||
redirect = redirectUrl.toString() | ||
res.headers.set('location', redirect) | ||
} | ||
} | ||
const redirect = res.headers.get('Location') | ||
// Data requests shouldn't automatically redirect in the browser (they might be HTML pages): they're handled by the router | ||
@@ -148,3 +209,4 @@ if (redirect && isDataReq) { | ||
} | ||
return res | ||
} |
@@ -1,6 +0,10 @@ | ||
// If the redirect is a data URL, we need to normalize it. | ||
// https://github.com/vercel/next.js/blob/25e0988e7c9033cb1503cbe0c62ba5de2e97849c/packages/next/src/shared/lib/router/utils/get-next-pathname-info.ts#L69-L76 | ||
export function normalizeDataUrl(redirect: string) { | ||
if (redirect.startsWith('/_next/data/') && redirect.includes('.json')) { | ||
const paths = redirect | ||
import type { RequestData } from './next-request.ts' | ||
/** | ||
* Normalize a data URL into a route path. | ||
* @see https://github.com/vercel/next.js/blob/25e0988e7c9033cb1503cbe0c62ba5de2e97849c/packages/next/src/shared/lib/router/utils/get-next-pathname-info.ts#L69-L76 | ||
*/ | ||
export function normalizeDataUrl(urlPath: string) { | ||
if (urlPath.startsWith('/_next/data/') && urlPath.includes('.json')) { | ||
const paths = urlPath | ||
.replace(/^\/_next\/data\//, '') | ||
@@ -10,9 +14,52 @@ .replace(/\.json/, '') | ||
redirect = paths[1] !== 'index' ? `/${paths.slice(1).join('/')}` : '/' | ||
urlPath = paths[1] !== 'index' ? `/${paths.slice(1).join('/')}` : '/' | ||
} | ||
return redirect | ||
return urlPath | ||
} | ||
export const removeBasePath = (path: string, basePath?: string) => { | ||
if (basePath && path.startsWith(basePath)) { | ||
return path.replace(basePath, '') | ||
} | ||
return path | ||
} | ||
// https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/i18n/normalize-locale-path.ts | ||
export interface PathLocale { | ||
detectedLocale?: string | ||
pathname: string | ||
} | ||
/** | ||
* For a pathname that may include a locale from a list of locales, it | ||
* removes the locale from the pathname returning it alongside with the | ||
* detected locale. | ||
* | ||
* @param pathname A pathname that may include a locale. | ||
* @param locales A list of locales. | ||
* @returns The detected locale and pathname without locale | ||
*/ | ||
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 | ||
} | ||
return false | ||
}) | ||
return { | ||
pathname, | ||
detectedLocale, | ||
} | ||
} | ||
/** | ||
* This is how Next handles rewritten URLs. | ||
@@ -28,1 +75,26 @@ */ | ||
} | ||
export const normalizeIndex = (path: string) => (path === '/' ? '/index' : path) | ||
const stripTrailingSlash = (path: string) => | ||
path !== '/' && path.endsWith('/') ? path.slice(0, -1) : path | ||
/** | ||
* Modify a data url to point to a new page route. | ||
*/ | ||
export function rewriteDataPath({ | ||
dataUrl, | ||
newRoute, | ||
basePath, | ||
}: { | ||
dataUrl: string | ||
newRoute: string | ||
basePath?: string | ||
}) { | ||
const normalizedDataUrl = normalizeDataUrl(removeBasePath(dataUrl, basePath)) | ||
return dataUrl.replace( | ||
normalizeIndex(normalizedDataUrl), | ||
stripTrailingSlash(normalizeIndex(newRoute)), | ||
) | ||
} |
import type { Context } from '@netlify/edge-functions' | ||
import matchers from './matchers.json' assert { type: 'json' } | ||
import nextConfig from './next.config.json' assert { type: 'json' } | ||
import { InternalHeaders } from './lib/headers.ts' | ||
import { logger, LogLevel } from './lib/logging.ts' | ||
import { buildNextRequest, RequestData } from './lib/next-request.ts' | ||
@@ -32,4 +35,10 @@ import { buildResponse } from './lib/response.ts' | ||
) { | ||
const nextRequest = buildNextRequest(request, context) | ||
const nextRequest = buildNextRequest(request, context, nextConfig) | ||
const url = new URL(request.url) | ||
const reqLogger = logger | ||
.withLogLevel( | ||
request.headers.has(InternalHeaders.NFDebugLogging) ? LogLevel.Debug : LogLevel.Log, | ||
) | ||
.withFields({ url_path: url.pathname }) | ||
.withRequestID(request.headers.get(InternalHeaders.NFRequestID)) | ||
@@ -41,2 +50,4 @@ // While we have already checked the path when mapping to the edge function, | ||
if (!matchesMiddleware(url.pathname, request, searchParamsToUrlQuery(url.searchParams))) { | ||
reqLogger.debug('Aborting middleware due to runtime rules') | ||
return | ||
@@ -47,3 +58,9 @@ } | ||
const result = await nextHandler({ request: nextRequest }) | ||
const response = await buildResponse({ result, request: request, context }) | ||
const response = await buildResponse({ | ||
context, | ||
logger: reqLogger, | ||
request, | ||
result, | ||
requestLocale: nextRequest.detectedLocale, | ||
}) | ||
@@ -50,0 +67,0 @@ return response |
@@ -19,1 +19,3 @@ // NOTE: This file contains a list of the third-party Deno dependencies we use. | ||
import 'https://esm.sh/v91/next@12.2.5/deno/dist/server/web/spec-extension/response.js' | ||
import 'https://v1-7-0--edge-utils.netlify.app/logger/mod.ts' |
@@ -6,3 +6,4 @@ { | ||
"https://esm.sh/": "./esm.sh/", | ||
"https://raw.githubusercontent.com/": "./raw.githubusercontent.com/" | ||
"https://raw.githubusercontent.com/": "./raw.githubusercontent.com/", | ||
"https://v1-7-0--edge-utils.netlify.app/": "./v1-7-0--edge-utils.netlify.app/" | ||
}, | ||
@@ -9,0 +10,0 @@ "scopes": { |
{ | ||
"name": "@netlify/plugin-nextjs", | ||
"version": "5.0.0-beta.2", | ||
"version": "5.0.0-beta.3", | ||
"description": "Run Next.js seamlessly on Netlify", | ||
@@ -5,0 +5,0 @@ "main": "./dist/index.js", |
@@ -20,5 +20,8 @@ # Next.js Runtime | ||
> Currently the tests require a built version of the `dist/run/handlers/cache.cjs` so you need to | ||
> run `npm run build` before executing the integration tests. In addition, the integration tests | ||
> need to be prepared before first use. You can do this by running `npm run pretest`. | ||
> run `npm run build` before executing the integration tests. | ||
In addition, the integration tests need to be prepared before first use. You can do this by running | ||
`npm run pretest`. To speed up this process and build only the fixtures whose name starts with a | ||
given prefix, run `npm run pretest -- <prefix>`. | ||
### E2E testing | ||
@@ -25,0 +28,0 @@ |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2056768
205
64100
65