rsnext/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts
JJ Kasper 7c6052a084
Fix failing E2E deployment test cases (#36368)
This continues off of https://github.com/vercel/next.js/pull/36285 fixing some of the failing test cases noticed when running the E2E tests against deployments. After these are resolved the tests will be added to our CI flow after each canary release. 

## Bug

- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

x-ref: https://github.com/vercel/next.js/pull/36285
2022-04-22 08:00:33 +00:00

524 lines
16 KiB
TypeScript

import type { IncomingMessage, ServerResponse } from 'http'
import type { Rewrite } from '../../../../lib/load-custom-routes'
import type { BuildManifest } from '../../../../server/get-page-files'
import type { NextConfig } from '../../../../server/config'
import type {
GetServerSideProps,
GetStaticPaths,
GetStaticProps,
} from '../../../../types'
import type { BaseNextRequest } from '../../../../server/base-http'
import { format as formatUrl, UrlWithParsedQuery, parse as parseUrl } from 'url'
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
import { normalizeLocalePath } from '../../../../shared/lib/i18n/normalize-locale-path'
import pathMatch from '../../../../shared/lib/router/utils/path-match'
import { getRouteRegex } from '../../../../shared/lib/router/utils/route-regex'
import { getRouteMatcher } from '../../../../shared/lib/router/utils/route-matcher'
import {
matchHas,
prepareDestination,
} from '../../../../shared/lib/router/utils/prepare-destination'
import { __ApiPreviewProps } from '../../../../server/api-utils'
import { acceptLanguage } from '../../../../server/accept-header'
import { detectLocaleCookie } from '../../../../shared/lib/i18n/detect-locale-cookie'
import { detectDomainLocale } from '../../../../shared/lib/i18n/detect-domain-locale'
import { denormalizePagePath } from '../../../../server/denormalize-page-path'
import cookie from 'next/dist/compiled/cookie'
import { TEMPORARY_REDIRECT_STATUS } from '../../../../shared/lib/constants'
import { addRequestMeta } from '../../../../server/request-meta'
const getCustomRouteMatcher = pathMatch(true)
export const vercelHeader = 'x-vercel-id'
export type ServerlessHandlerCtx = {
page: string
pageModule: any
pageComponent?: any
pageConfig?: any
pageGetStaticProps?: GetStaticProps
pageGetStaticPaths?: GetStaticPaths
pageGetServerSideProps?: GetServerSideProps
appModule?: any
errorModule?: any
documentModule?: any
notFoundModule?: any
runtimeConfig: any
buildManifest?: BuildManifest
reactLoadableManifest?: any
basePath: string
rewrites: Rewrite[]
pageIsDynamic: boolean
generateEtags: boolean
distDir: string
buildId: string
escapedBuildId: string
assetPrefix: string
poweredByHeader: boolean
canonicalBase: string
encodedPreviewProps: __ApiPreviewProps
i18n?: NextConfig['i18n']
}
export function getUtils({
page,
i18n,
basePath,
rewrites,
pageIsDynamic,
}: {
page: ServerlessHandlerCtx['page']
i18n?: ServerlessHandlerCtx['i18n']
basePath: ServerlessHandlerCtx['basePath']
rewrites: ServerlessHandlerCtx['rewrites']
pageIsDynamic: ServerlessHandlerCtx['pageIsDynamic']
}) {
let defaultRouteRegex: ReturnType<typeof getRouteRegex> | undefined
let dynamicRouteMatcher: ReturnType<typeof getRouteMatcher> | undefined
let defaultRouteMatches: ParsedUrlQuery | undefined
if (pageIsDynamic) {
defaultRouteRegex = getRouteRegex(page)
dynamicRouteMatcher = getRouteMatcher(defaultRouteRegex)
defaultRouteMatches = dynamicRouteMatcher(page) as ParsedUrlQuery
}
function handleRewrites(
req: BaseNextRequest | IncomingMessage,
parsedUrl: UrlWithParsedQuery
) {
const rewriteParams = {}
for (const rewrite of rewrites) {
const matcher = getCustomRouteMatcher(rewrite.source)
let params = matcher(parsedUrl.pathname)
if (rewrite.has && params) {
const hasParams = matchHas(req, rewrite.has, parsedUrl.query)
if (hasParams) {
Object.assign(params, hasParams)
} else {
params = false
}
}
if (params) {
const { parsedDestination, destQuery } = prepareDestination({
appendParamsToQuery: true,
destination: rewrite.destination,
params: params,
query: parsedUrl.query,
})
Object.assign(rewriteParams, destQuery, params)
Object.assign(parsedUrl.query, parsedDestination.query)
delete (parsedDestination as any).query
Object.assign(parsedUrl, parsedDestination)
let fsPathname = parsedUrl.pathname
if (basePath) {
fsPathname =
fsPathname!.replace(new RegExp(`^${basePath}`), '') || '/'
}
if (i18n) {
const destLocalePathResult = normalizeLocalePath(
fsPathname!,
i18n.locales
)
fsPathname = destLocalePathResult.pathname
parsedUrl.query.nextInternalLocale =
destLocalePathResult.detectedLocale || params.nextInternalLocale
}
if (fsPathname === page) {
break
}
if (pageIsDynamic && dynamicRouteMatcher) {
const dynamicParams = dynamicRouteMatcher(fsPathname)
if (dynamicParams) {
parsedUrl.query = {
...parsedUrl.query,
...dynamicParams,
}
break
}
}
}
}
return rewriteParams
}
function handleBasePath(
req: BaseNextRequest | IncomingMessage,
parsedUrl: UrlWithParsedQuery
) {
// always strip the basePath if configured since it is required
req.url = req.url!.replace(new RegExp(`^${basePath}`), '') || '/'
parsedUrl.pathname =
parsedUrl.pathname!.replace(new RegExp(`^${basePath}`), '') || '/'
}
function getParamsFromRouteMatches(
req: BaseNextRequest | IncomingMessage,
renderOpts?: any,
detectedLocale?: string
) {
return getRouteMatcher(
(function () {
const { groups, routeKeys } = defaultRouteRegex!
return {
re: {
// Simulate a RegExp match from the \`req.url\` input
exec: (str: string) => {
const obj = parseQs(str)
const matchesHasLocale =
i18n && detectedLocale && obj['1'] === detectedLocale
// favor named matches if available
const routeKeyNames = Object.keys(routeKeys || {})
const filterLocaleItem = (val: string | string[]) => {
if (i18n) {
// locale items can be included in route-matches
// for fallback SSG pages so ensure they are
// filtered
const isCatchAll = Array.isArray(val)
const _val = isCatchAll ? val[0] : val
if (
typeof _val === 'string' &&
i18n.locales.some((item) => {
if (item.toLowerCase() === _val.toLowerCase()) {
detectedLocale = item
renderOpts.locale = detectedLocale
return true
}
return false
})
) {
// remove the locale item from the match
if (isCatchAll) {
;(val as string[]).splice(0, 1)
}
// the value is only a locale item and
// shouldn't be added
return isCatchAll ? val.length === 0 : true
}
}
return false
}
if (routeKeyNames.every((name) => obj[name])) {
return routeKeyNames.reduce((prev, keyName) => {
const paramName = routeKeys?.[keyName]
if (paramName && !filterLocaleItem(obj[keyName])) {
prev[groups[paramName].pos] = obj[keyName]
}
return prev
}, {} as any)
}
return Object.keys(obj).reduce((prev, key) => {
if (!filterLocaleItem(obj[key])) {
let normalizedKey = key
if (matchesHasLocale) {
normalizedKey = parseInt(key, 10) - 1 + ''
}
return Object.assign(prev, {
[normalizedKey]: obj[key],
})
}
return prev
}, {})
},
},
groups,
}
})() as any
)(req.headers['x-now-route-matches'] as string) as ParsedUrlQuery
}
function interpolateDynamicPath(pathname: string, params: ParsedUrlQuery) {
if (!defaultRouteRegex) return pathname
for (const param of Object.keys(defaultRouteRegex.groups)) {
const { optional, repeat } = defaultRouteRegex.groups[param]
let builtParam = `[${repeat ? '...' : ''}${param}]`
if (optional) {
builtParam = `[${builtParam}]`
}
const paramIdx = pathname!.indexOf(builtParam)
if (paramIdx > -1) {
let paramValue: string
if (Array.isArray(params[param])) {
paramValue = (params[param] as string[])
.map((v) => v && encodeURIComponent(v))
.join('/')
} else {
paramValue =
params[param] && encodeURIComponent(params[param] as string)
}
pathname =
pathname.slice(0, paramIdx) +
(paramValue || '') +
pathname.slice(paramIdx + builtParam.length)
}
}
return pathname
}
function normalizeVercelUrl(
req: BaseNextRequest | IncomingMessage,
trustQuery: boolean,
paramKeys?: string[]
) {
// make sure to normalize req.url on Vercel to strip dynamic params
// from the query which are added during routing
if (pageIsDynamic && trustQuery && defaultRouteRegex) {
const _parsedUrl = parseUrl(req.url!, true)
delete (_parsedUrl as any).search
for (const param of paramKeys || Object.keys(defaultRouteRegex.groups)) {
delete _parsedUrl.query[param]
}
req.url = formatUrl(_parsedUrl)
}
}
function normalizeDynamicRouteParams(params: ParsedUrlQuery) {
let hasValidParams = true
if (!defaultRouteRegex) return { params, hasValidParams: false }
params = Object.keys(defaultRouteRegex.groups).reduce((prev, key) => {
let value: string | string[] | undefined = params[key]
// if the value matches the default value we can't rely
// on the parsed params, this is used to signal if we need
// to parse x-now-route-matches or not
const defaultValue = defaultRouteMatches![key]
const isDefaultValue = Array.isArray(defaultValue)
? defaultValue.some((defaultVal) => {
return Array.isArray(value)
? value.some((val) => val.includes(defaultVal))
: value?.includes(defaultVal)
})
: value?.includes(defaultValue as string)
if (isDefaultValue || typeof value === 'undefined') {
hasValidParams = false
}
// non-provided optional values should be undefined so normalize
// them to undefined
if (
defaultRouteRegex!.groups[key].optional &&
(!value ||
(Array.isArray(value) &&
value.length === 1 &&
// fallback optional catch-all SSG pages have
// [[...paramName]] for the root path on Vercel
(value[0] === 'index' || value[0] === `[[...${key}]]`)))
) {
value = undefined
delete params[key]
}
// query values from the proxy aren't already split into arrays
// so make sure to normalize catch-all values
if (
value &&
typeof value === 'string' &&
defaultRouteRegex!.groups[key].repeat
) {
value = value.split('/')
}
if (value) {
prev[key] = value
}
return prev
}, {} as ParsedUrlQuery)
return {
params,
hasValidParams,
}
}
function handleLocale(
req: IncomingMessage,
res: ServerResponse,
parsedUrl: UrlWithParsedQuery,
routeNoAssetPath: string,
shouldNotRedirect: boolean
) {
if (!i18n) return
const pathname = parsedUrl.pathname || '/'
let defaultLocale = i18n.defaultLocale
let detectedLocale = detectLocaleCookie(req, i18n.locales)
let acceptPreferredLocale
try {
acceptPreferredLocale =
i18n.localeDetection !== false
? acceptLanguage(req.headers['accept-language'], i18n.locales)
: detectedLocale
} catch (_) {
acceptPreferredLocale = detectedLocale
}
const { host } = req.headers || {}
// remove port from host and remove port if present
const hostname = host && host.split(':')[0].toLowerCase()
const detectedDomain = detectDomainLocale(i18n.domains, hostname)
if (detectedDomain) {
defaultLocale = detectedDomain.defaultLocale
detectedLocale = defaultLocale
addRequestMeta(req as any, '__nextIsLocaleDomain', true)
}
// if not domain specific locale use accept-language preferred
detectedLocale = detectedLocale || acceptPreferredLocale
let localeDomainRedirect
const localePathResult = normalizeLocalePath(pathname, i18n.locales)
routeNoAssetPath = normalizeLocalePath(
routeNoAssetPath,
i18n.locales
).pathname
if (localePathResult.detectedLocale) {
detectedLocale = localePathResult.detectedLocale
req.url = formatUrl({
...parsedUrl,
pathname: localePathResult.pathname,
})
addRequestMeta(req as any, '__nextStrippedLocale', true)
parsedUrl.pathname = localePathResult.pathname
}
// If a detected locale is a domain specific locale and we aren't already
// on that domain and path prefix redirect to it to prevent duplicate
// content from multiple domains
if (detectedDomain) {
const localeToCheck = localePathResult.detectedLocale
? detectedLocale
: acceptPreferredLocale
const matchedDomain = detectDomainLocale(
i18n.domains,
undefined,
localeToCheck
)
if (matchedDomain && matchedDomain.domain !== detectedDomain.domain) {
localeDomainRedirect = `http${matchedDomain.http ? '' : 's'}://${
matchedDomain.domain
}/${localeToCheck === matchedDomain.defaultLocale ? '' : localeToCheck}`
}
}
const denormalizedPagePath = denormalizePagePath(pathname)
const detectedDefaultLocale =
!detectedLocale ||
detectedLocale.toLowerCase() === defaultLocale.toLowerCase()
const shouldStripDefaultLocale = false
// detectedDefaultLocale &&
// denormalizedPagePath.toLowerCase() === \`/\${i18n.defaultLocale.toLowerCase()}\`
const shouldAddLocalePrefix =
!detectedDefaultLocale && denormalizedPagePath === '/'
detectedLocale = detectedLocale || i18n.defaultLocale
if (
!shouldNotRedirect &&
!req.headers[vercelHeader] &&
i18n.localeDetection !== false &&
(localeDomainRedirect ||
shouldAddLocalePrefix ||
shouldStripDefaultLocale)
) {
// set the NEXT_LOCALE cookie when a user visits the default locale
// with the locale prefix so that they aren't redirected back to
// their accept-language preferred locale
if (shouldStripDefaultLocale && acceptPreferredLocale !== defaultLocale) {
const previous = res.getHeader('set-cookie')
res.setHeader('set-cookie', [
...(typeof previous === 'string'
? [previous]
: Array.isArray(previous)
? previous
: []),
cookie.serialize('NEXT_LOCALE', defaultLocale, {
httpOnly: true,
path: '/',
}),
])
}
res.setHeader(
'Location',
formatUrl({
// make sure to include any query values when redirecting
...parsedUrl,
pathname: localeDomainRedirect
? localeDomainRedirect
: shouldStripDefaultLocale
? basePath || '/'
: `${basePath}/${detectedLocale}`,
})
)
res.statusCode = TEMPORARY_REDIRECT_STATUS
res.end()
return
}
detectedLocale =
localePathResult.detectedLocale ||
(detectedDomain && detectedDomain.defaultLocale) ||
defaultLocale
return {
defaultLocale,
detectedLocale,
routeNoAssetPath,
}
}
return {
handleLocale,
handleRewrites,
handleBasePath,
defaultRouteRegex,
normalizeVercelUrl,
dynamicRouteMatcher,
defaultRouteMatches,
interpolateDynamicPath,
getParamsFromRouteMatches,
normalizeDynamicRouteParams,
}
}