diff --git a/docs/advanced-features/security-headers.md b/docs/advanced-features/security-headers.md
index 84e04c15d89968b..660c97711c157f6 100644
--- a/docs/advanced-features/security-headers.md
+++ b/docs/advanced-features/security-headers.md
@@ -81,7 +81,7 @@ This header allows you to control which features and APIs can be used in the bro
```jsx
{
key: 'Permissions-Policy',
- value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()'
+ value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()'
}
```
diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md
index c27c822fb76ef04..b303e54cb63e3b7 100644
--- a/docs/api-reference/next/image.md
+++ b/docs/api-reference/next/image.md
@@ -279,14 +279,16 @@ The Ref must point to a DOM element or a React component that [forwards the Ref]
import Image from 'next/image'
import React from 'react'
-const lazyRoot = React.useRef(null)
+const Example = () => {
+ const lazyRoot = React.useRef(null)
-const Example = () => (
-
-
-
-
-)
+ return (
+
+
+
+
+ )
+}
```
**Example pointing to a React component**
diff --git a/docs/testing.md b/docs/testing.md
index f185d0eac486511..e3504d418de3ab5 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -281,6 +281,7 @@ const createJestConfig = nextJest({
})
// Add any custom config to be passed to Jest
+/** @type {import('jest').Config} */
const customJestConfig = {
// Add more setup options before each test is run
// setupFilesAfterEnv: ['/jest.setup.js'],
diff --git a/lerna.json b/lerna.json
index d97edfc48be689e..57eb3bf48f24b3e 100644
--- a/lerna.json
+++ b/lerna.json
@@ -16,5 +16,5 @@
"registry": "https://registry.npmjs.org/"
}
},
- "version": "12.2.6-canary.6"
+ "version": "12.2.6-canary.7"
}
diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json
index 3d8df817c26574e..b44bb0d7864b7ae 100644
--- a/packages/create-next-app/package.json
+++ b/packages/create-next-app/package.json
@@ -1,6 +1,6 @@
{
"name": "create-next-app",
- "version": "12.2.6-canary.6",
+ "version": "12.2.6-canary.7",
"keywords": [
"react",
"next",
diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json
index b430e9f1b4ecffa..8e24e00c031b6a8 100644
--- a/packages/eslint-config-next/package.json
+++ b/packages/eslint-config-next/package.json
@@ -1,6 +1,6 @@
{
"name": "eslint-config-next",
- "version": "12.2.6-canary.6",
+ "version": "12.2.6-canary.7",
"description": "ESLint configuration used by NextJS.",
"main": "index.js",
"license": "MIT",
@@ -9,7 +9,7 @@
"directory": "packages/eslint-config-next"
},
"dependencies": {
- "@next/eslint-plugin-next": "12.2.6-canary.6",
+ "@next/eslint-plugin-next": "12.2.6-canary.7",
"@rushstack/eslint-patch": "^1.1.3",
"@typescript-eslint/parser": "^5.21.0",
"eslint-import-resolver-node": "^0.3.6",
diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json
index 89232255fca7dcc..f2226efd31173a4 100644
--- a/packages/eslint-plugin-next/package.json
+++ b/packages/eslint-plugin-next/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/eslint-plugin-next",
- "version": "12.2.6-canary.6",
+ "version": "12.2.6-canary.7",
"description": "ESLint plugin for NextJS.",
"main": "lib/index.js",
"license": "MIT",
diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json
index 4287bf3c1d1c6b5..52578c7bf162aa4 100644
--- a/packages/next-bundle-analyzer/package.json
+++ b/packages/next-bundle-analyzer/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/bundle-analyzer",
- "version": "12.2.6-canary.6",
+ "version": "12.2.6-canary.7",
"main": "index.js",
"types": "index.d.ts",
"license": "MIT",
diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json
index 9a124cc03039629..5874126c6a23a1a 100644
--- a/packages/next-codemod/package.json
+++ b/packages/next-codemod/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/codemod",
- "version": "12.2.6-canary.6",
+ "version": "12.2.6-canary.7",
"license": "MIT",
"dependencies": {
"chalk": "4.1.0",
diff --git a/packages/next-env/package.json b/packages/next-env/package.json
index 46f728b59f6bdc4..a559a569712be42 100644
--- a/packages/next-env/package.json
+++ b/packages/next-env/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/env",
- "version": "12.2.6-canary.6",
+ "version": "12.2.6-canary.7",
"keywords": [
"react",
"next",
diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json
index 33c71438f10eee6..c5b2c1370b1cfa8 100644
--- a/packages/next-mdx/package.json
+++ b/packages/next-mdx/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/mdx",
- "version": "12.2.6-canary.6",
+ "version": "12.2.6-canary.7",
"main": "index.js",
"license": "MIT",
"repository": {
diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json
index 4fd90f65419fa02..40258e8f791fa04 100644
--- a/packages/next-plugin-storybook/package.json
+++ b/packages/next-plugin-storybook/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/plugin-storybook",
- "version": "12.2.6-canary.6",
+ "version": "12.2.6-canary.7",
"repository": {
"url": "vercel/next.js",
"directory": "packages/next-plugin-storybook"
diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json
index 20cb9a32882fd38..48912ffaa1c2b23 100644
--- a/packages/next-polyfill-module/package.json
+++ b/packages/next-polyfill-module/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/polyfill-module",
- "version": "12.2.6-canary.6",
+ "version": "12.2.6-canary.7",
"description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)",
"main": "dist/polyfill-module.js",
"license": "MIT",
diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json
index b5ef4ed1d5f4558..f0ad046e77b9a8e 100644
--- a/packages/next-polyfill-nomodule/package.json
+++ b/packages/next-polyfill-nomodule/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/polyfill-nomodule",
- "version": "12.2.6-canary.6",
+ "version": "12.2.6-canary.7",
"description": "A polyfill for non-dead, nomodule browsers.",
"main": "dist/polyfill-nomodule.js",
"license": "MIT",
diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json
index 68abb3a5a023f34..7e226bb671a49da 100644
--- a/packages/next-swc/package.json
+++ b/packages/next-swc/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/swc",
- "version": "12.2.6-canary.6",
+ "version": "12.2.6-canary.7",
"private": true,
"scripts": {
"build-native": "napi build --platform -p next-swc-napi --cargo-name next_swc_napi native --features plugin",
diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts
index 3a24961a3fcaeaf..1cec41de0b50272 100644
--- a/packages/next/build/index.ts
+++ b/packages/next/build/index.ts
@@ -1162,16 +1162,17 @@ export default async function build(
const errorPageStaticResult = nonStaticErrorPageSpan.traceAsyncFn(
async () =>
hasCustomErrorPage &&
- staticWorkers.isPageStatic(
- '/_error',
+ staticWorkers.isPageStatic({
+ page: '/_error',
distDir,
- isLikeServerless,
+ serverless: isLikeServerless,
configFileName,
runtimeEnvConfig,
- config.httpAgentOptions,
- config.i18n?.locales,
- config.i18n?.defaultLocale
- )
+ httpAgentOptions: config.httpAgentOptions,
+ locales: config.i18n?.locales,
+ defaultLocale: config.i18n?.defaultLocale,
+ pageRuntime: config.experimental.runtime,
+ })
)
// we don't output _app in serverless mode so use _app export
@@ -1274,29 +1275,53 @@ export default async function build(
// Only calculate page static information if the page is not an
// app page.
pageType !== 'app' &&
- !isReservedPage(page) &&
- // We currently don't support static optimization in the Edge runtime.
- pageRuntime !== SERVER_RUNTIME.edge
+ !isReservedPage(page)
) {
try {
+ let edgeInfo: any
+
+ if (pageRuntime === SERVER_RUNTIME.edge) {
+ const manifest = require(join(
+ distDir,
+ serverDir,
+ MIDDLEWARE_MANIFEST
+ ))
+
+ edgeInfo = manifest.functions[page]
+ }
+
let isPageStaticSpan =
checkPageSpan.traceChild('is-page-static')
let workerResult = await isPageStaticSpan.traceAsyncFn(
() => {
- return staticWorkers.isPageStatic(
+ return staticWorkers.isPageStatic({
page,
distDir,
- isLikeServerless,
+ serverless: isLikeServerless,
configFileName,
runtimeEnvConfig,
- config.httpAgentOptions,
- config.i18n?.locales,
- config.i18n?.defaultLocale,
- isPageStaticSpan.id
- )
+ httpAgentOptions: config.httpAgentOptions,
+ locales: config.i18n?.locales,
+ defaultLocale: config.i18n?.defaultLocale,
+ parentId: isPageStaticSpan.id,
+ pageRuntime,
+ edgeInfo,
+ })
}
)
+ if (pageRuntime === SERVER_RUNTIME.edge) {
+ if (workerResult.hasStaticProps) {
+ console.warn(
+ `"getStaticProps" is not yet supported fully with "experimental-edge", detected on ${page}`
+ )
+ }
+ // TODO: add handling for statically rendering edge
+ // pages and allow edge with Prerender outputs
+ workerResult.isStatic = false
+ workerResult.hasStaticProps = false
+ }
+
if (config.outputFileTracing) {
pageTraceIncludes.set(
page,
diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts
index e37d1fa55420c2b..c050e89d49cb377 100644
--- a/packages/next/build/utils.ts
+++ b/packages/next/build/utils.ts
@@ -34,7 +34,10 @@ import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-
import { UnwrapPromise } from '../lib/coalesced-function'
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
import * as Log from './output/log'
-import { loadComponents } from '../server/load-components'
+import {
+ loadComponents,
+ LoadComponentsReturnType,
+} from '../server/load-components'
import { trace } from '../trace'
import { setHttpAgentOptions } from '../server/config'
import { recursiveDelete } from '../lib/recursive-delete'
@@ -43,6 +46,7 @@ import { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path'
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
import { AppBuildManifest } from './webpack/plugins/app-build-manifest-plugin'
+import { getRuntimeContext } from '../server/web/sandbox'
export type ROUTER_TYPE = 'pages' | 'app'
@@ -1008,17 +1012,31 @@ export async function buildStaticPaths(
}
}
-export async function isPageStatic(
- page: string,
- distDir: string,
- serverless: boolean,
- configFileName: string,
- runtimeEnvConfig: any,
- httpAgentOptions: NextConfigComplete['httpAgentOptions'],
- locales?: string[],
- defaultLocale?: string,
+export async function isPageStatic({
+ page,
+ distDir,
+ serverless,
+ configFileName,
+ runtimeEnvConfig,
+ httpAgentOptions,
+ locales,
+ defaultLocale,
+ parentId,
+ pageRuntime,
+ edgeInfo,
+}: {
+ page: string
+ distDir: string
+ serverless: boolean
+ configFileName: string
+ runtimeEnvConfig: any
+ httpAgentOptions: NextConfigComplete['httpAgentOptions']
+ locales?: string[]
+ defaultLocale?: string
parentId?: any
-): Promise<{
+ edgeInfo?: any
+ pageRuntime: ServerRuntime
+}): Promise<{
isStatic?: boolean
isAmpOnly?: boolean
isHybridAmp?: boolean
@@ -1037,24 +1055,51 @@ export async function isPageStatic(
require('../shared/lib/runtime-config').setConfig(runtimeEnvConfig)
setHttpAgentOptions(httpAgentOptions)
- const mod = await loadComponents(distDir, page, serverless)
- const Comp = mod.Component
+ let componentsResult: LoadComponentsReturnType
+
+ if (pageRuntime === SERVER_RUNTIME.edge) {
+ const runtime = await getRuntimeContext({
+ paths: edgeInfo.files.map((file: string) => path.join(distDir, file)),
+ env: edgeInfo.env,
+ edgeFunctionEntry: edgeInfo,
+ name: edgeInfo.name,
+ useCache: true,
+ distDir,
+ })
+ const mod =
+ runtime.context._ENTRIES[`middleware_${edgeInfo.name}`].ComponentMod
+
+ componentsResult = {
+ Component: mod.default,
+ ComponentMod: mod,
+ pageConfig: mod.config || {},
+ // @ts-expect-error this is not needed during require
+ buildManifest: {},
+ reactLoadableManifest: {},
+ getServerSideProps: mod.getServerSideProps,
+ getStaticPaths: mod.getStaticPaths,
+ getStaticProps: mod.getStaticProps,
+ }
+ } else {
+ componentsResult = await loadComponents(distDir, page, serverless)
+ }
+ const Comp = componentsResult.Component
if (!Comp || !isValidElementType(Comp) || typeof Comp === 'string') {
throw new Error('INVALID_DEFAULT_EXPORT')
}
const hasGetInitialProps = !!(Comp as any).getInitialProps
- const hasStaticProps = !!mod.getStaticProps
- const hasStaticPaths = !!mod.getStaticPaths
- const hasServerProps = !!mod.getServerSideProps
- const hasLegacyServerProps = !!(await mod.ComponentMod
+ const hasStaticProps = !!componentsResult.getStaticProps
+ const hasStaticPaths = !!componentsResult.getStaticPaths
+ const hasServerProps = !!componentsResult.getServerSideProps
+ const hasLegacyServerProps = !!(await componentsResult.ComponentMod
.unstable_getServerProps)
- const hasLegacyStaticProps = !!(await mod.ComponentMod
+ const hasLegacyStaticProps = !!(await componentsResult.ComponentMod
.unstable_getStaticProps)
- const hasLegacyStaticPaths = !!(await mod.ComponentMod
+ const hasLegacyStaticPaths = !!(await componentsResult.ComponentMod
.unstable_getStaticPaths)
- const hasLegacyStaticParams = !!(await mod.ComponentMod
+ const hasLegacyStaticParams = !!(await componentsResult.ComponentMod
.unstable_getStaticParams)
if (hasLegacyStaticParams) {
@@ -1121,7 +1166,7 @@ export async function isPageStatic(
encodedPaths: encodedPrerenderRoutes,
} = await buildStaticPaths(
page,
- mod.getStaticPaths!,
+ componentsResult.getStaticPaths!,
configFileName,
locales,
defaultLocale
@@ -1129,7 +1174,7 @@ export async function isPageStatic(
}
const isNextImageImported = (global as any).__NEXT_IMAGE_IMPORTED
- const config: PageConfig = mod.pageConfig
+ const config: PageConfig = componentsResult.pageConfig
return {
isStatic: !hasStaticProps && !hasGetInitialProps && !hasServerProps,
isHybridAmp: config.amp === 'hybrid',
diff --git a/packages/next/build/webpack/loaders/next-edge-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-edge-ssr-loader/index.ts
index c09e23cb7e68c32..527aae70d77dc26 100644
--- a/packages/next/build/webpack/loaders/next-edge-ssr-loader/index.ts
+++ b/packages/next/build/webpack/loaders/next-edge-ssr-loader/index.ts
@@ -112,6 +112,8 @@ export default async function edgeSSRLoader(this: any) {
config: ${stringifiedConfig},
buildId: ${JSON.stringify(buildId)},
})
+
+ export const ComponentMod = pageMod
export default function(opts) {
return adapter({
diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts
index 0763c0f423cea81..47246669056e63a 100644
--- a/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts
+++ b/packages/next/build/webpack/loaders/next-serverless-loader/utils.ts
@@ -68,6 +68,45 @@ export type ServerlessHandlerCtx = {
i18n?: NextConfig['i18n']
}
+export function interpolateDynamicPath(
+ pathname: string,
+ params: ParsedUrlQuery,
+ defaultRouteRegex?: ReturnType | undefined
+) {
+ 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
+}
+
export function getUtils({
page,
i18n,
@@ -297,41 +336,6 @@ export function getUtils({
)(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,
@@ -570,8 +574,11 @@ export function getUtils({
normalizeVercelUrl,
dynamicRouteMatcher,
defaultRouteMatches,
- interpolateDynamicPath,
getParamsFromRouteMatches,
normalizeDynamicRouteParams,
+ interpolateDynamicPath: (
+ pathname: string,
+ params: Record
+ ) => interpolateDynamicPath(pathname, params, defaultRouteRegex),
}
}
diff --git a/packages/next/package.json b/packages/next/package.json
index 35cb97df563f0eb..75b8d2b1e3a0c55 100644
--- a/packages/next/package.json
+++ b/packages/next/package.json
@@ -1,6 +1,6 @@
{
"name": "next",
- "version": "12.2.6-canary.6",
+ "version": "12.2.6-canary.7",
"description": "The React Framework",
"main": "./dist/server/next.js",
"license": "MIT",
@@ -70,7 +70,7 @@
]
},
"dependencies": {
- "@next/env": "12.2.6-canary.6",
+ "@next/env": "12.2.6-canary.7",
"@swc/helpers": "0.4.3",
"caniuse-lite": "^1.0.30001332",
"postcss": "8.4.14",
@@ -121,11 +121,11 @@
"@hapi/accept": "5.0.2",
"@napi-rs/cli": "2.7.0",
"@napi-rs/triples": "1.1.0",
- "@next/polyfill-module": "12.2.6-canary.6",
- "@next/polyfill-nomodule": "12.2.6-canary.6",
- "@next/react-dev-overlay": "12.2.6-canary.6",
- "@next/react-refresh-utils": "12.2.6-canary.6",
- "@next/swc": "12.2.6-canary.6",
+ "@next/polyfill-module": "12.2.6-canary.7",
+ "@next/polyfill-nomodule": "12.2.6-canary.7",
+ "@next/react-dev-overlay": "12.2.6-canary.7",
+ "@next/react-refresh-utils": "12.2.6-canary.7",
+ "@next/swc": "12.2.6-canary.7",
"@segment/ajv-human-errors": "2.1.2",
"@taskr/clear": "1.1.0",
"@taskr/esnext": "1.1.0",
diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts
index df3413e07fafb99..25bc78ad3f25d35 100644
--- a/packages/next/server/base-server.ts
+++ b/packages/next/server/base-server.ts
@@ -946,7 +946,8 @@ export default abstract class Server {
// Toggle whether or not this is a Data request
const isDataReq =
- !!query.__nextDataReq && (isSSG || hasServerProps || isServerComponent)
+ !!(query.__nextDataReq || req.headers['x-nextjs-data']) &&
+ (isSSG || hasServerProps || isServerComponent)
delete query.__nextDataReq
@@ -1577,6 +1578,18 @@ export default abstract class Server {
if (result !== false) return result
}
}
+
+ // currently edge functions aren't receiving the x-matched-path
+ // header so we need to fallback to matching the current page
+ // when we weren't able to match via dynamic route to handle
+ // the rewrite case
+ // @ts-expect-error extended in child class web-server
+ if (this.serverOptions.webServerConfig) {
+ // @ts-expect-error extended in child class web-server
+ ctx.pathname = this.serverOptions.webServerConfig.page
+ const result = await this.renderPageComponent(ctx, bubbleNoFallback)
+ if (result !== false) return result
+ }
} catch (error) {
const err = getProperError(error)
@@ -1608,11 +1621,16 @@ export default abstract class Server {
}
res.statusCode = 500
+
+ // if pages/500 is present we still need to trigger
+ // /_error `getInitialProps` to allow reporting error
+ if (await this.hasPage('/500')) {
+ ctx.query.__nextCustomErrorRender = '1'
+ await this.renderErrorToResponse(ctx, err)
+ delete ctx.query.__nextCustomErrorRender
+ }
+
const isWrappedError = err instanceof WrappedBuildError
- const response = await this.renderErrorToResponse(
- ctx,
- isWrappedError ? (err as WrappedBuildError).innerError : err
- )
if (!isWrappedError) {
if (
@@ -1624,6 +1642,10 @@ export default abstract class Server {
}
this.logError(getProperError(err))
}
+ const response = await this.renderErrorToResponse(
+ ctx,
+ isWrappedError ? (err as WrappedBuildError).innerError : err
+ )
return response
}
@@ -1712,7 +1734,11 @@ export default abstract class Server {
}
let statusPage = `/${res.statusCode}`
- if (!result && STATIC_STATUS_PAGES.includes(statusPage)) {
+ if (
+ !ctx.query.__nextCustomErrorRender &&
+ !result &&
+ STATIC_STATUS_PAGES.includes(statusPage)
+ ) {
// skip ensuring /500 in dev mode as it isn't used and the
// dev overlay is used instead
if (statusPage !== '/500' || !this.renderOpts.dev) {
diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts
index 461c3b08866b31f..492ab6b93e4b094 100644
--- a/packages/next/server/dev/next-dev-server.ts
+++ b/packages/next/server/dev/next-dev-server.ts
@@ -222,7 +222,6 @@ export default class DevServer extends Server {
for (const path in exportPathMap) {
const { page, query = {} } = exportPathMap[path]
- // We use unshift so that we're sure the routes is defined before Next's default routes
this.router.addFsRoute({
match: getPathMatch(path),
type: 'route',
diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts
index 18d75626b9a81fc..28021d731f7a055 100644
--- a/packages/next/server/next-server.ts
+++ b/packages/next/server/next-server.ts
@@ -96,6 +96,8 @@ import { checkIsManualRevalidate } from './api-utils'
import { shouldUseReactRoot, isTargetLikeServerless } from './utils'
import ResponseCache from './response-cache'
import { IncrementalCache } from './lib/incremental-cache'
+import { interpolateDynamicPath } from '../build/webpack/loaders/next-serverless-loader/utils'
+import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex'
if (shouldUseReactRoot) {
;(process.env as any).__NEXT_REACT_ROOT = 'true'
@@ -1951,7 +1953,32 @@ export default class NextNodeServer extends BaseServer {
}
// For middleware to "fetch" we must always provide an absolute URL
- const url = getRequestMeta(params.req, '__NEXT_INIT_URL')!
+ const isDataReq = !!params.query.__nextDataReq
+ const query = urlQueryToSearchParams(
+ Object.assign({}, getRequestMeta(params.req, '__NEXT_INIT_QUERY') || {})
+ ).toString()
+ const locale = params.query.__nextLocale
+ let normalizedPathname = params.page
+
+ if (isDataReq) {
+ params.req.headers['x-nextjs-data'] = '1'
+ }
+
+ if (isDynamicRoute(normalizedPathname)) {
+ const routeRegex = getNamedRouteRegex(params.page)
+ normalizedPathname = interpolateDynamicPath(
+ params.page,
+ Object.assign({}, params.params, params.query),
+ routeRegex
+ )
+ }
+
+ const url = `${getRequestMeta(params.req, '_protocol')}://${
+ this.hostname
+ }:${this.port}${locale ? `/${locale}` : ''}${normalizedPathname}${
+ query ? `?${query}` : ''
+ }`
+
if (!url.startsWith('http')) {
throw new Error(
'To use middleware you must provide a `hostname` and `port` to the Next.js Server'
diff --git a/packages/next/server/request-meta.ts b/packages/next/server/request-meta.ts
index 68ddb22a72808f9..39985a49184838f 100644
--- a/packages/next/server/request-meta.ts
+++ b/packages/next/server/request-meta.ts
@@ -63,6 +63,7 @@ type NextQueryMetadata = {
__nextSsgPath?: string
_nextBubbleNoFallback?: '1'
__nextDataReq?: '1'
+ __nextCustomErrorRender?: '1'
}
export type NextParsedUrlQuery = ParsedUrlQuery &
diff --git a/packages/next/server/router.ts b/packages/next/server/router.ts
index 0cb309f823afb4c..ca2ce97b1a7dc21 100644
--- a/packages/next/server/router.ts
+++ b/packages/next/server/router.ts
@@ -40,7 +40,7 @@ export type Route = {
res: BaseNextResponse,
params: Params,
parsedUrl: NextUrlWithParsedQuery,
- upgradeHead?: any
+ upgradeHead?: Buffer
) => Promise | RouteResult
}
@@ -49,21 +49,37 @@ export type DynamicRoutes = Array<{ page: string; match: RouteMatch }>
export type PageChecker = (pathname: string) => Promise
export default class Router {
- headers: Route[]
- fsRoutes: Route[]
- redirects: Route[]
- rewrites: {
- beforeFiles: Route[]
- afterFiles: Route[]
- fallback: Route[]
+ public catchAllMiddleware: ReadonlyArray
+
+ private readonly headers: ReadonlyArray
+ private readonly fsRoutes: Route[]
+ private readonly redirects: ReadonlyArray
+ private readonly rewrites: {
+ beforeFiles: ReadonlyArray
+ afterFiles: ReadonlyArray
+ fallback: ReadonlyArray
}
- catchAllRoute: Route
- catchAllMiddleware: Route[]
- pageChecker: PageChecker
- dynamicRoutes: DynamicRoutes
- useFileSystemPublicRoutes: boolean
- seenRequests: Set
- nextConfig: NextConfig
+ private readonly catchAllRoute: Route
+ private readonly pageChecker: PageChecker
+ private dynamicRoutes: DynamicRoutes
+ private readonly useFileSystemPublicRoutes: boolean
+ private readonly nextConfig: NextConfig
+ private compiledRoutes: ReadonlyArray
+ private needsRecompilation: boolean
+
+ /**
+ * context stores information used by the router.
+ */
+ private readonly context = new WeakMap<
+ BaseNextRequest,
+ {
+ /**
+ * pageChecks is the memoized record of all checks made against pages to
+ * help de-duplicate work.
+ */
+ pageChecks: Record
+ }
+ >()
constructor({
headers = [],
@@ -81,16 +97,16 @@ export default class Router {
useFileSystemPublicRoutes,
nextConfig,
}: {
- headers: Route[]
- fsRoutes: Route[]
+ headers: ReadonlyArray
+ fsRoutes: ReadonlyArray
rewrites: {
- beforeFiles: Route[]
- afterFiles: Route[]
- fallback: Route[]
+ beforeFiles: ReadonlyArray
+ afterFiles: ReadonlyArray
+ fallback: ReadonlyArray
}
- redirects: Route[]
+ redirects: ReadonlyArray
catchAllRoute: Route
- catchAllMiddleware: Route[]
+ catchAllMiddleware: ReadonlyArray
dynamicRoutes: DynamicRoutes | undefined
pageChecker: PageChecker
useFileSystemPublicRoutes: boolean
@@ -98,7 +114,7 @@ export default class Router {
}) {
this.nextConfig = nextConfig
this.headers = headers
- this.fsRoutes = fsRoutes
+ this.fsRoutes = [...fsRoutes]
this.rewrites = rewrites
this.redirects = redirects
this.pageChecker = pageChecker
@@ -106,7 +122,32 @@ export default class Router {
this.catchAllMiddleware = catchAllMiddleware
this.dynamicRoutes = dynamicRoutes
this.useFileSystemPublicRoutes = useFileSystemPublicRoutes
- this.seenRequests = new Set()
+
+ // Perform the initial route compilation.
+ this.compiledRoutes = this.compileRoutes()
+ this.needsRecompilation = false
+ }
+
+ private async checkPage(
+ req: BaseNextRequest,
+ pathname: string
+ ): Promise {
+ pathname = normalizeLocalePath(pathname, this.locales).pathname
+
+ const context = this.context.get(req)
+ if (!context) {
+ throw new Error(
+ 'Invariant: request is not available inside the context, this is an internal error please open an issue.'
+ )
+ }
+
+ if (context.pageChecks[pathname] !== undefined) {
+ return context.pageChecks[pathname]
+ }
+
+ const result = await this.pageChecker(pathname)
+ context.pageChecks[pathname] = result
+ return result
}
get locales() {
@@ -117,192 +158,201 @@ export default class Router {
return this.nextConfig.basePath || ''
}
- setDynamicRoutes(routes: DynamicRoutes = []) {
- this.dynamicRoutes = routes
+ public setDynamicRoutes(dynamicRoutes: DynamicRoutes) {
+ this.dynamicRoutes = dynamicRoutes
+ this.needsRecompilation = true
}
- setCatchallMiddleware(route?: Route[]) {
- this.catchAllMiddleware = route || []
+ public setCatchallMiddleware(catchAllMiddleware: ReadonlyArray) {
+ this.catchAllMiddleware = catchAllMiddleware
+ this.needsRecompilation = true
}
- addFsRoute(fsRoute: Route) {
+ public addFsRoute(fsRoute: Route) {
+ // We use unshift so that we're sure the routes is defined before Next's
+ // default routes.
this.fsRoutes.unshift(fsRoute)
+ this.needsRecompilation = true
}
- async execute(
+ private compileRoutes(): ReadonlyArray {
+ /*
+ Desired routes order
+ - headers
+ - redirects
+ - Check filesystem (including pages), if nothing found continue
+ - User rewrites (checking filesystem and pages each match)
+ */
+
+ const [middlewareCatchAllRoute] = this.catchAllMiddleware
+
+ return [
+ ...(middlewareCatchAllRoute
+ ? this.fsRoutes
+ .filter((route) => route.name === '_next/data catchall')
+ .map((route) => ({ ...route, check: false }))
+ : []),
+ ...this.headers,
+ ...this.redirects,
+ ...(this.useFileSystemPublicRoutes && middlewareCatchAllRoute
+ ? [middlewareCatchAllRoute]
+ : []),
+ ...this.rewrites.beforeFiles,
+ ...this.fsRoutes,
+ // We only check the catch-all route if public page routes hasn't been
+ // disabled
+ ...(this.useFileSystemPublicRoutes
+ ? [
+ {
+ type: 'route',
+ name: 'page checker',
+ match: getPathMatch('/:path*'),
+ fn: async (req, res, params, parsedUrl, upgradeHead) => {
+ const pathname = removeTrailingSlash(parsedUrl.pathname || '/')
+ if (!pathname) {
+ return { finished: false }
+ }
+
+ if (await this.checkPage(req, pathname)) {
+ return this.catchAllRoute.fn(
+ req,
+ res,
+ params,
+ parsedUrl,
+ upgradeHead
+ )
+ }
+
+ return { finished: false }
+ },
+ } as Route,
+ ]
+ : []),
+ ...this.rewrites.afterFiles,
+ ...(this.rewrites.fallback.length
+ ? [
+ {
+ type: 'route',
+ name: 'dynamic route/page check',
+ match: getPathMatch('/:path*'),
+ fn: async (req, res, _params, parsedCheckerUrl, upgradeHead) => {
+ return {
+ finished: await this.checkFsRoutes(
+ req,
+ res,
+ parsedCheckerUrl,
+ upgradeHead
+ ),
+ }
+ },
+ } as Route,
+ ...this.rewrites.fallback,
+ ]
+ : []),
+
+ // We only check the catch-all route if public page routes hasn't been
+ // disabled
+ ...(this.useFileSystemPublicRoutes ? [this.catchAllRoute] : []),
+ ]
+ }
+
+ private async checkFsRoutes(
req: BaseNextRequest,
res: BaseNextResponse,
parsedUrl: NextUrlWithParsedQuery,
- upgradeHead?: any
- ): Promise {
- if (this.seenRequests.has(req)) {
- throw new Error(
- `Invariant: request has already been processed: ${req.url}, this is an internal error please open an issue.`
- )
- }
- this.seenRequests.add(req)
- try {
- // memoize page check calls so we don't duplicate checks for pages
- const pageChecks: { [name: string]: Promise } = {}
- const memoizedPageChecker = async (p: string): Promise => {
- p = normalizeLocalePath(p, this.locales).pathname
+ upgradeHead?: Buffer
+ ) {
+ const originalFsPathname = parsedUrl.pathname
+ const fsPathname = removePathPrefix(originalFsPathname!, this.basePath)
+
+ for (const route of this.fsRoutes) {
+ const params = route.match(fsPathname)
- if (pageChecks[p] !== undefined) {
- return pageChecks[p]
+ if (params) {
+ parsedUrl.pathname = fsPathname
+
+ const { finished } = await route.fn(req, res, params, parsedUrl)
+ if (finished) {
+ return true
}
- const result = this.pageChecker(p)
- pageChecks[p] = result
- return result
+
+ parsedUrl.pathname = originalFsPathname
}
+ }
+
+ let matchedPage = await this.checkPage(req, fsPathname)
+
+ // If we didn't match a page check dynamic routes
+ if (!matchedPage) {
+ const normalizedFsPathname = normalizeLocalePath(
+ fsPathname,
+ this.locales
+ ).pathname
- let parsedUrlUpdated = parsedUrl
+ for (const dynamicRoute of this.dynamicRoutes) {
+ if (dynamicRoute.match(normalizedFsPathname)) {
+ matchedPage = true
+ }
+ }
+ }
- const applyCheckTrue = async (checkParsedUrl: NextUrlWithParsedQuery) => {
- const originalFsPathname = checkParsedUrl.pathname
- const fsPathname = removePathPrefix(originalFsPathname!, this.basePath)
+ // Matched a page or dynamic route so render it using catchAllRoute
+ if (matchedPage) {
+ const params = this.catchAllRoute.match(parsedUrl.pathname)
+ if (!params) {
+ throw new Error(
+ `Invariant: could not match params, this is an internal error please open an issue.`
+ )
+ }
- for (const fsRoute of this.fsRoutes) {
- const fsParams = fsRoute.match(fsPathname)
+ parsedUrl.pathname = fsPathname
+ parsedUrl.query._nextBubbleNoFallback = '1'
- if (fsParams) {
- checkParsedUrl.pathname = fsPathname
+ const { finished } = await this.catchAllRoute.fn(
+ req,
+ res,
+ params,
+ parsedUrl,
+ upgradeHead
+ )
- const fsResult = await fsRoute.fn(
- req,
- res,
- fsParams,
- checkParsedUrl
- )
+ return finished
+ }
- if (fsResult.finished) {
- return true
- }
+ return false
+ }
- checkParsedUrl.pathname = originalFsPathname
- }
- }
- let matchedPage = await memoizedPageChecker(fsPathname)
-
- // If we didn't match a page check dynamic routes
- if (!matchedPage) {
- const normalizedFsPathname = normalizeLocalePath(
- fsPathname,
- this.locales
- ).pathname
-
- for (const dynamicRoute of this.dynamicRoutes) {
- if (dynamicRoute.match(normalizedFsPathname)) {
- matchedPage = true
- }
- }
- }
+ async execute(
+ req: BaseNextRequest,
+ res: BaseNextResponse,
+ parsedUrl: NextUrlWithParsedQuery,
+ upgradeHead?: Buffer
+ ): Promise {
+ // Only recompile if the routes need to be recompiled, this should only
+ // happen in development.
+ if (this.needsRecompilation) {
+ this.compiledRoutes = this.compileRoutes()
+ this.needsRecompilation = false
+ }
- // Matched a page or dynamic route so render it using catchAllRoute
- if (matchedPage) {
- const pageParams = this.catchAllRoute.match(checkParsedUrl.pathname)
- checkParsedUrl.pathname = fsPathname
- checkParsedUrl.query._nextBubbleNoFallback = '1'
+ if (this.context.has(req)) {
+ throw new Error(
+ `Invariant: request has already been processed: ${req.url}, this is an internal error please open an issue.`
+ )
+ }
+ this.context.set(req, { pageChecks: {} })
- const result = await this.catchAllRoute.fn(
- req,
- res,
- pageParams as Params,
- checkParsedUrl
- )
- return result.finished
- }
+ try {
+ // Create a deep copy of the parsed URL.
+ const parsedUrlUpdated = {
+ ...parsedUrl,
+ query: {
+ ...parsedUrl.query,
+ },
}
- /*
- Desired routes order
- - headers
- - redirects
- - Check filesystem (including pages), if nothing found continue
- - User rewrites (checking filesystem and pages each match)
- */
-
- const [middlewareCatchAllRoute] = this.catchAllMiddleware
- const allRoutes = [
- ...(middlewareCatchAllRoute
- ? this.fsRoutes
- .filter((r) => r.name === '_next/data catchall')
- .map((r) => {
- return {
- ...r,
- check: false,
- }
- })
- : []),
- ...this.headers,
- ...this.redirects,
- ...(this.useFileSystemPublicRoutes && middlewareCatchAllRoute
- ? [middlewareCatchAllRoute]
- : []),
- ...this.rewrites.beforeFiles,
- ...this.fsRoutes,
- // We only check the catch-all route if public page routes hasn't been
- // disabled
- ...(this.useFileSystemPublicRoutes
- ? [
- {
- type: 'route',
- name: 'page checker',
- match: getPathMatch('/:path*'),
- fn: async (
- checkerReq,
- checkerRes,
- params,
- parsedCheckerUrl
- ) => {
- let { pathname } = parsedCheckerUrl
- pathname = removeTrailingSlash(pathname || '/')
-
- if (!pathname) {
- return { finished: false }
- }
-
- if (await memoizedPageChecker(pathname)) {
- return this.catchAllRoute.fn(
- checkerReq,
- checkerRes,
- params,
- parsedCheckerUrl
- )
- }
- return { finished: false }
- },
- } as Route,
- ]
- : []),
- ...this.rewrites.afterFiles,
- ...(this.rewrites.fallback.length
- ? [
- {
- type: 'route',
- name: 'dynamic route/page check',
- match: getPathMatch('/:path*'),
- fn: async (
- _checkerReq,
- _checkerRes,
- _params,
- parsedCheckerUrl
- ) => {
- return {
- finished: await applyCheckTrue(parsedCheckerUrl),
- }
- },
- } as Route,
- ...this.rewrites.fallback,
- ]
- : []),
-
- // We only check the catch-all route if public page routes hasn't been
- // disabled
- ...(this.useFileSystemPublicRoutes ? [this.catchAllRoute] : []),
- ]
-
- for (const testRoute of allRoutes) {
+ for (const route of this.compiledRoutes) {
// only process rewrites for upgrade request
- if (upgradeHead && testRoute.type !== 'rewrite') {
+ if (upgradeHead && route.type !== 'rewrite') {
continue
}
@@ -314,7 +364,7 @@ export default class Router {
if (
pathnameInfo.locale &&
- !testRoute.matchesLocaleAPIRoutes &&
+ !route.matchesLocaleAPIRoutes &&
pathnameInfo.pathname.match(/^\/api(?:\/|$)/)
) {
continue
@@ -325,20 +375,20 @@ export default class Router {
}
const basePath = pathnameInfo.basePath
- if (!testRoute.matchesBasePath) {
+ if (!route.matchesBasePath) {
pathnameInfo.basePath = ''
}
if (
- testRoute.matchesLocale &&
- parsedUrl.query.__nextLocale &&
+ route.matchesLocale &&
+ parsedUrlUpdated.query.__nextLocale &&
!pathnameInfo.locale
) {
- pathnameInfo.locale = parsedUrl.query.__nextLocale
+ pathnameInfo.locale = parsedUrlUpdated.query.__nextLocale
}
if (
- !testRoute.matchesLocale &&
+ !route.matchesLocale &&
pathnameInfo.locale === this.nextConfig.i18n?.defaultLocale &&
pathnameInfo.locale
) {
@@ -346,7 +396,7 @@ export default class Router {
}
if (
- testRoute.matchesTrailingSlash &&
+ route.matchesTrailingSlash &&
getRequestMeta(req, '__nextHadTrailingSlash')
) {
pathnameInfo.trailingSlash = true
@@ -357,13 +407,13 @@ export default class Router {
...pathnameInfo,
})
- let newParams = testRoute.match(matchPathname)
- if (testRoute.has && newParams) {
- const hasParams = matchHas(req, testRoute.has, parsedUrlUpdated.query)
+ let params = route.match(matchPathname)
+ if (route.has && params) {
+ const hasParams = matchHas(req, route.has, parsedUrlUpdated.query)
if (hasParams) {
- Object.assign(newParams, hasParams)
+ Object.assign(params, hasParams)
} else {
- newParams = false
+ params = false
}
}
@@ -373,35 +423,34 @@ export default class Router {
* never there, we consider this an invalid match and keep routing.
*/
if (
- newParams &&
+ params &&
this.basePath &&
- !testRoute.matchesBasePath &&
+ !route.matchesBasePath &&
!getRequestMeta(req, '_nextDidRewrite') &&
!basePath
) {
continue
}
- if (newParams) {
+ if (params) {
parsedUrlUpdated.pathname = matchPathname
- const result = await testRoute.fn(
+ const result = await route.fn(
req,
res,
- newParams,
+ params,
parsedUrlUpdated,
upgradeHead
)
-
if (result.finished) {
return true
}
- // since the fs route didn't finish routing we need to re-add the
- // basePath to continue checking with the basePath present
- parsedUrlUpdated.pathname = originalPathname
-
if (result.pathname) {
parsedUrlUpdated.pathname = result.pathname
+ } else {
+ // since the fs route didn't finish routing we need to re-add the
+ // basePath to continue checking with the basePath present
+ parsedUrlUpdated.pathname = originalPathname
}
if (result.query) {
@@ -412,16 +461,19 @@ export default class Router {
}
// check filesystem
- if (testRoute.check === true) {
- if (await applyCheckTrue(parsedUrlUpdated)) {
- return true
- }
+ if (
+ route.check &&
+ (await this.checkFsRoutes(req, res, parsedUrlUpdated))
+ ) {
+ return true
}
}
}
+
+ // All routes were tested, none were found.
return false
} finally {
- this.seenRequests.delete(req)
+ this.context.delete(req)
}
}
}
diff --git a/packages/next/server/web-server.ts b/packages/next/server/web-server.ts
index 657b05bbc375f4d..7afff576aea23a7 100644
--- a/packages/next/server/web-server.ts
+++ b/packages/next/server/web-server.ts
@@ -66,7 +66,6 @@ export default class NextWebServer extends BaseServer {
res: BaseNextResponse,
parsedUrl: UrlWithParsedQuery
): Promise {
- parsedUrl.pathname = this.serverOptions.webServerConfig.page
super.run(req, res, parsedUrl)
}
protected async hasPage(page: string) {
@@ -343,11 +342,10 @@ export default class NextWebServer extends BaseServer {
{} as any,
pathname,
query,
- {
- ...renderOpts,
+ Object.assign(renderOpts, {
disableOptimizedLoading: true,
runtime: 'experimental-edge',
- },
+ }),
!!pagesRenderToHTML
)
} else {
diff --git a/packages/next/server/web/sandbox/sandbox.ts b/packages/next/server/web/sandbox/sandbox.ts
index 773936c75017435..d232294d5fceab6 100644
--- a/packages/next/server/web/sandbox/sandbox.ts
+++ b/packages/next/server/web/sandbox/sandbox.ts
@@ -3,6 +3,7 @@ import { getServerError } from 'next/dist/compiled/@next/react-dev-overlay/dist/
import { getModuleContext } from './context'
import { EdgeFunctionDefinition } from '../../../build/webpack/plugins/middleware-plugin'
import { requestToBodyStream } from '../../body-streams'
+import type { EdgeRuntime } from 'next/dist/compiled/edge-runtime'
export const ErrorSource = Symbol('SandboxError')
@@ -43,7 +44,15 @@ function withTaggedErrors(fn: RunnerFn): RunnerFn {
})
}
-export const run = withTaggedErrors(async (params) => {
+export const getRuntimeContext = async (params: {
+ name: string
+ onWarning?: any
+ useCache: boolean
+ env: string[]
+ edgeFunctionEntry: any
+ distDir: string
+ paths: string[]
+}): Promise> => {
const { runtime, evaluateInContext } = await getModuleContext({
moduleName: params.name,
onWarning: params.onWarning ?? (() => {}),
@@ -56,7 +65,11 @@ export const run = withTaggedErrors(async (params) => {
for (const paramPath of params.paths) {
evaluateInContext(paramPath)
}
+ return runtime
+}
+export const run = withTaggedErrors(async (params) => {
+ const runtime = await getRuntimeContext(params)
const subreq = params.request.headers[`x-middleware-subrequest`]
const subrequests = typeof subreq === 'string' ? subreq.split(':') : []
if (subrequests.includes(params.name)) {
diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json
index 5d16661ace2dff2..b31065e23675d4f 100644
--- a/packages/react-dev-overlay/package.json
+++ b/packages/react-dev-overlay/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/react-dev-overlay",
- "version": "12.2.6-canary.6",
+ "version": "12.2.6-canary.7",
"description": "A development-only overlay for developing React applications.",
"repository": {
"url": "vercel/next.js",
diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json
index 3e84fb69a567852..9cd9abe590f04ef 100644
--- a/packages/react-refresh-utils/package.json
+++ b/packages/react-refresh-utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/react-refresh-utils",
- "version": "12.2.6-canary.6",
+ "version": "12.2.6-canary.7",
"description": "An experimental package providing utilities for React Refresh.",
"repository": {
"url": "vercel/next.js",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 645811547409167..dfe5387e501f5d8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -363,7 +363,7 @@ importers:
packages/eslint-config-next:
specifiers:
- '@next/eslint-plugin-next': 12.2.6-canary.6
+ '@next/eslint-plugin-next': 12.2.6-canary.7
'@rushstack/eslint-patch': ^1.1.3
'@typescript-eslint/parser': ^5.21.0
eslint-import-resolver-node: ^0.3.6
@@ -419,12 +419,12 @@ importers:
'@hapi/accept': 5.0.2
'@napi-rs/cli': 2.7.0
'@napi-rs/triples': 1.1.0
- '@next/env': 12.2.6-canary.6
- '@next/polyfill-module': 12.2.6-canary.6
- '@next/polyfill-nomodule': 12.2.6-canary.6
- '@next/react-dev-overlay': 12.2.6-canary.6
- '@next/react-refresh-utils': 12.2.6-canary.6
- '@next/swc': 12.2.6-canary.6
+ '@next/env': 12.2.6-canary.7
+ '@next/polyfill-module': 12.2.6-canary.7
+ '@next/polyfill-nomodule': 12.2.6-canary.7
+ '@next/react-dev-overlay': 12.2.6-canary.7
+ '@next/react-refresh-utils': 12.2.6-canary.7
+ '@next/swc': 12.2.6-canary.7
'@segment/ajv-human-errors': 2.1.2
'@swc/helpers': 0.4.3
'@taskr/clear': 1.1.0
diff --git a/test/development/correct-tsconfig-defaults/index.test.ts b/test/development/correct-tsconfig-defaults/index.test.ts
index 70b8b51fee62db0..45ce6c521decfd8 100644
--- a/test/development/correct-tsconfig-defaults/index.test.ts
+++ b/test/development/correct-tsconfig-defaults/index.test.ts
@@ -1,7 +1,5 @@
import { createNext } from 'e2e-utils'
-import fs from 'fs'
-import { waitFor } from 'next-test-utils'
-import path from 'path'
+import { check } from 'next-test-utils'
import { NextInstance } from 'test/lib/next-modes/base'
describe('correct tsconfig.json defaults', () => {
@@ -23,38 +21,46 @@ describe('correct tsconfig.json defaults', () => {
afterAll(() => next.destroy())
it('should add `moduleResolution` when generating tsconfig.json in dev', async () => {
- const tsconfigPath = path.join(next.testDir, 'tsconfig.json')
- expect(fs.existsSync(tsconfigPath)).toBeFalse()
+ try {
+ expect(
+ await next.readFile('tsconfig.json').catch(() => false)
+ ).toBeFalse()
- await next.start()
- await waitFor(1000)
- await next.stop()
+ await next.start()
- expect(fs.existsSync(tsconfigPath)).toBeTrue()
+ // wait for tsconfig to be written
+ await check(async () => {
+ await next.readFile('tsconfig.json')
+ return 'success'
+ }, 'success')
- const tsconfig = JSON.parse(await next.readFile('tsconfig.json'))
- expect(next.cliOutput).not.toContain('moduleResolution')
+ const tsconfig = JSON.parse(await next.readFile('tsconfig.json'))
+ expect(next.cliOutput).not.toContain('moduleResolution')
- expect(tsconfig.compilerOptions).toEqual(
- expect.objectContaining({ moduleResolution: 'node' })
- )
+ expect(tsconfig.compilerOptions).toEqual(
+ expect.objectContaining({ moduleResolution: 'node' })
+ )
+ } finally {
+ await next.stop()
+ }
})
it('should not warn for `moduleResolution` when already present and valid', async () => {
- const tsconfigPath = path.join(next.testDir, 'tsconfig.json')
- expect(fs.existsSync(tsconfigPath)).toBeTrue()
+ try {
+ expect(
+ await next.readFile('tsconfig.json').catch(() => false)
+ ).toBeTruthy()
- await next.start()
- await waitFor(1000)
- await next.stop()
+ await next.start()
- expect(fs.existsSync(tsconfigPath)).toBeTrue()
+ const tsconfig = JSON.parse(await next.readFile('tsconfig.json'))
- const tsconfig = JSON.parse(await next.readFile('tsconfig.json'))
-
- expect(tsconfig.compilerOptions).toEqual(
- expect.objectContaining({ moduleResolution: 'node' })
- )
- expect(next.cliOutput).not.toContain('moduleResolution')
+ expect(tsconfig.compilerOptions).toEqual(
+ expect.objectContaining({ moduleResolution: 'node' })
+ )
+ expect(next.cliOutput).not.toContain('moduleResolution')
+ } finally {
+ await next.stop()
+ }
})
})
diff --git a/test/e2e/edge-render-getserversideprops/app/pages/[id].js b/test/e2e/edge-render-getserversideprops/app/pages/[id].js
new file mode 100644
index 000000000000000..c4d5932704aa255
--- /dev/null
+++ b/test/e2e/edge-render-getserversideprops/app/pages/[id].js
@@ -0,0 +1,22 @@
+export const config = {
+ runtime: 'experimental-edge',
+}
+
+export default function Page(props) {
+ return (
+ <>
+ /[id]
+ {JSON.stringify(props)}
+ >
+ )
+}
+
+export function getServerSideProps({ params, query }) {
+ return {
+ props: {
+ query,
+ params,
+ now: Date.now(),
+ },
+ }
+}
diff --git a/test/e2e/edge-render-getserversideprops/app/pages/index.js b/test/e2e/edge-render-getserversideprops/app/pages/index.js
new file mode 100644
index 000000000000000..8264c1fa7c48c30
--- /dev/null
+++ b/test/e2e/edge-render-getserversideprops/app/pages/index.js
@@ -0,0 +1,22 @@
+export const config = {
+ runtime: 'experimental-edge',
+}
+
+export default function Page(props) {
+ return (
+ <>
+ /index
+ {JSON.stringify(props)}
+ >
+ )
+}
+
+export function getServerSideProps({ params, query }) {
+ return {
+ props: {
+ query,
+ now: Date.now(),
+ params: params || null,
+ },
+ }
+}
diff --git a/test/e2e/edge-render-getserversideprops/index.test.ts b/test/e2e/edge-render-getserversideprops/index.test.ts
new file mode 100644
index 000000000000000..7bc8cef2d5abda6
--- /dev/null
+++ b/test/e2e/edge-render-getserversideprops/index.test.ts
@@ -0,0 +1,108 @@
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { fetchViaHTTP, normalizeRegEx, renderViaHTTP } from 'next-test-utils'
+import cheerio from 'cheerio'
+import { join } from 'path'
+import escapeStringRegexp from 'escape-string-regexp'
+
+describe('edge-render-getserversideprops', () => {
+ let next: NextInstance
+
+ if (process.env.NEXT_TEST_REACT_VERSION === '^17') {
+ it('should skip for react v17', () => {})
+ return
+ }
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: new FileRef(join(__dirname, 'app')),
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('should have correct query/params on index', async () => {
+ const html = await renderViaHTTP(next.url, '/')
+ const $ = cheerio.load(html)
+ expect($('#page').text()).toBe('/index')
+ const props = JSON.parse($('#props').text())
+ expect(props.query).toEqual({})
+ expect(props.params).toBe(null)
+ })
+
+ it('should have correct query/params on /[id]', async () => {
+ const html = await renderViaHTTP(next.url, '/123', { hello: 'world' })
+ const $ = cheerio.load(html)
+ expect($('#page').text()).toBe('/[id]')
+ const props = JSON.parse($('#props').text())
+ expect(props.query).toEqual({ id: '123', hello: 'world' })
+ expect(props.params).toEqual({ id: '123' })
+ })
+
+ it('should respond to _next/data for index correctly', async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/index.json`,
+ undefined,
+ {
+ headers: {
+ 'x-nextjs-data': '1',
+ },
+ }
+ )
+ expect(res.status).toBe(200)
+ const { pageProps: props } = await res.json()
+ expect(props.query).toEqual({})
+ expect(props.params).toBe(null)
+ })
+
+ it('should respond to _next/data for [id] correctly', async () => {
+ const res = await fetchViaHTTP(
+ next.url,
+ `/_next/data/${next.buildId}/321.json`,
+ { hello: 'world' },
+ {
+ headers: {
+ 'x-nextjs-data': '1',
+ },
+ }
+ )
+ expect(res.status).toBe(200)
+ const { pageProps: props } = await res.json()
+ expect(props.query).toEqual({ id: '321', hello: 'world' })
+ expect(props.params).toEqual({ id: '321' })
+ })
+
+ if ((global as any).isNextStart) {
+ it('should have data routes in routes-manifest', async () => {
+ const manifest = JSON.parse(
+ await next.readFile('.next/routes-manifest.json')
+ )
+
+ for (const route of manifest.dataRoutes) {
+ route.dataRouteRegex = normalizeRegEx(route.dataRouteRegex)
+ }
+
+ expect(manifest.dataRoutes).toEqual([
+ {
+ dataRouteRegex: normalizeRegEx(
+ `^/_next/data/${escapeStringRegexp(next.buildId)}/index.json$`
+ ),
+ page: '/',
+ },
+ {
+ dataRouteRegex: normalizeRegEx(
+ `^/_next/data/${escapeStringRegexp(next.buildId)}/([^/]+?)\\.json$`
+ ),
+ namedDataRouteRegex: `^/_next/data/${escapeStringRegexp(
+ next.buildId
+ )}/(?[^/]+?)\\.json$`,
+ page: '/[id]',
+ routeKeys: {
+ id: 'id',
+ },
+ },
+ ])
+ })
+ }
+})
diff --git a/test/production/custom-error-500/index.test.ts b/test/production/custom-error-500/index.test.ts
new file mode 100644
index 000000000000000..2dba5785f3cf144
--- /dev/null
+++ b/test/production/custom-error-500/index.test.ts
@@ -0,0 +1,77 @@
+import { createNext } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { check, renderViaHTTP } from 'next-test-utils'
+
+describe('custom-error-500', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ 'pages/index.js': `
+ export function getServerSideProps() {
+ throw new Error('custom error')
+ }
+
+ export default function Page() {
+ return index page
+ }
+ `,
+ 'pages/500.js': `
+ export default function Custom500() {
+ return (
+ <>
+ pages/500
+ >
+ )
+ }
+ `,
+ 'pages/_error.js': `
+ function Error({ hasError }) {
+ return (
+ <>
+ /_error
+ >
+ )
+ }
+
+ Error.getInitialProps = ({ err }) => {
+ console.log(\`called Error.getInitialProps \${!!err}\`)
+ return {
+ hasError: !!err
+ }
+ }
+
+ export default Error
+ `,
+ },
+ dependencies: {},
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('should correctly use pages/500 and call Error.getInitialProps', async () => {
+ const html = await renderViaHTTP(next.url, '/')
+ expect(html).toContain('pages/500')
+
+ await check(() => next.cliOutput, /called Error\.getInitialProps true/)
+ })
+
+ it('should work correctly with pages/404 present', async () => {
+ await next.stop()
+ await next.patchFile(
+ 'pages/404.js',
+ `
+ export default function Page() {
+ return custom 404 page
+ }
+ `
+ )
+ await next.start()
+
+ const html = await renderViaHTTP(next.url, '/')
+ expect(html).toContain('pages/500')
+
+ await check(() => next.cliOutput, /called Error\.getInitialProps true/)
+ })
+})