From 1bf2ab6a666638e09eeeededa7236bc4c5fb7f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Ferretti?= Date: Tue, 30 Aug 2022 16:38:29 +0200 Subject: [PATCH 1/9] docs(testing): add JSDoc typing in `jest.config.js` (#40090) This PR improves the Testing documentation in [Setting up Jest (with the Rust Compiler)](https://nextjs.org/docs/testing#setting-up-jest-with-the-rust-compiler). It adds JSDoc typing in `jest.config.js`. ## Documentation / Examples - [x] Make sure the linting passes by running `pnpm lint` --- docs/testing.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/testing.md b/docs/testing.md index f185d0eac486..e3504d418de3 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'], From b760c8dd0a992cd4fceb15cc603de86f2457988f Mon Sep 17 00:00:00 2001 From: Dueen Eduarda <42207398+Dueen@users.noreply.github.com> Date: Tue, 30 Aug 2022 17:22:55 +0200 Subject: [PATCH 2/9] docs(image): Use hook inside of function component (#40096) The example broke the [Rules of Hooks](https://reactjs.org/warnings/invalid-hook-call-warning.html#:~:text=Hooks%20can%20only%20be%20called%20inside%20the%20body%20of%20a%20function%20component.) by calling the hook outside of the function component. Co-authored-by: JJ Kasper --- docs/api-reference/next/image.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index c27c822fb76e..b303e54cb63e 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** From c89e25eba6b3aa5f0ddaffaf9849d8cdf0340700 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 30 Aug 2022 10:31:38 -0700 Subject: [PATCH 3/9] Update flakey tsconfig test (#40105) Updates this test to not rely on `waitFor` as the amount of time it takes to write the `tsconfig.json` can vary so we should use `check` instead. Fixes: https://github.com/vercel/next.js/pull/39902#issuecomment-1231939162 --- .../correct-tsconfig-defaults/index.test.ts | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/test/development/correct-tsconfig-defaults/index.test.ts b/test/development/correct-tsconfig-defaults/index.test.ts index 70b8b51fee62..45ce6c521dec 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() + } }) }) From 0b57a01ae6842fe5c45fd4345ed33724a06622da Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 30 Aug 2022 11:56:44 -0600 Subject: [PATCH 4/9] Refactor Server Router (#39902) This is intended to refactor the router code to reduce the overhead of executing routes. This is related to #32314 that may help reduce the memory overhead as this also replaced the `Set` with a `WeakMap`. Co-authored-by: JJ Kasper --- packages/next/server/dev/next-dev-server.ts | 1 - packages/next/server/router.ts | 478 +++++++++++--------- 2 files changed, 265 insertions(+), 214 deletions(-) diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 461c3b08866b..492ab6b93e4b 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/router.ts b/packages/next/server/router.ts index 0cb309f823af..ca2ce97b1a7d 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) } } } From 3bf8b2b4fe8b8b5c7ff60b5f5cb12e65273c6dab Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 30 Aug 2022 11:18:02 -0700 Subject: [PATCH 5/9] Update to detect GSSP with edge runtime during build (#40076) This updates to handle detecting `getStaticProps`/`getServerSideProps` correctly during build when `experimental-edge` is being used. This also fixes not parsing dynamic route params correctly with the edge runtime and sets up the handling needed for the static generation for app opened in the below mentioned PR. ## Bug - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` Fixes: [slack thread](https://vercel.slack.com/archives/C0289CGVAR2/p1661554455121189) x-ref: https://github.com/vercel/next.js/pull/39884 --- packages/next/build/index.ts | 59 +++++++--- packages/next/build/utils.ts | 89 +++++++++++---- .../loaders/next-edge-ssr-loader/index.ts | 2 + .../loaders/next-serverless-loader/utils.ts | 79 +++++++------ packages/next/server/base-server.ts | 3 +- packages/next/server/next-server.ts | 29 ++++- packages/next/server/web-server.ts | 6 +- packages/next/server/web/sandbox/sandbox.ts | 15 ++- .../app/pages/[id].js | 22 ++++ .../app/pages/index.js | 22 ++++ .../index.test.ts | 108 ++++++++++++++++++ 11 files changed, 352 insertions(+), 82 deletions(-) create mode 100644 test/e2e/edge-render-getserversideprops/app/pages/[id].js create mode 100644 test/e2e/edge-render-getserversideprops/app/pages/index.js create mode 100644 test/e2e/edge-render-getserversideprops/index.test.ts diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 3a24961a3fca..1cec41de0b50 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 e37d1fa55420..c050e89d49cb 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 c09e23cb7e68..527aae70d77d 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 0763c0f423ce..47246669056e 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/server/base-server.ts b/packages/next/server/base-server.ts index df3413e07faf..a6da2bd95780 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 diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 18d75626b9a8..28021d731f7a 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/web-server.ts b/packages/next/server/web-server.ts index 657b05bbc375..7afff576aea2 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 773936c75017..d232294d5fce 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/test/e2e/edge-render-getserversideprops/app/pages/[id].js b/test/e2e/edge-render-getserversideprops/app/pages/[id].js new file mode 100644 index 000000000000..c4d5932704aa --- /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 000000000000..8264c1fa7c48 --- /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 000000000000..7bc8cef2d5ab --- /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', + }, + }, + ]) + }) + } +}) From dddc60df15880fc91478bb75126474e7cd416f7f Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 30 Aug 2022 12:57:31 -0700 Subject: [PATCH 6/9] v12.2.6-canary.7 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- packages/eslint-config-next/package.json | 4 ++-- packages/eslint-plugin-next/package.json | 2 +- packages/next-bundle-analyzer/package.json | 2 +- packages/next-codemod/package.json | 2 +- packages/next-env/package.json | 2 +- packages/next-mdx/package.json | 2 +- packages/next-plugin-storybook/package.json | 2 +- packages/next-polyfill-module/package.json | 2 +- packages/next-polyfill-nomodule/package.json | 2 +- packages/next-swc/package.json | 2 +- packages/next/package.json | 14 +++++++------- packages/react-dev-overlay/package.json | 2 +- packages/react-refresh-utils/package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- 16 files changed, 29 insertions(+), 29 deletions(-) diff --git a/lerna.json b/lerna.json index d97edfc48be6..57eb3bf48f24 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 3d8df817c265..b44bb0d7864b 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 b430e9f1b4ec..8e24e00c031b 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 89232255fca7..f2226efd3117 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 4287bf3c1d1c..52578c7bf162 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 9a124cc03039..5874126c6a23 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 46f728b59f6b..a559a569712b 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 33c71438f10e..c5b2c1370b1c 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 4fd90f65419f..40258e8f791f 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 20cb9a32882f..48912ffaa1c2 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 b5ef4ed1d5f4..f0ad046e77b9 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 68abb3a5a023..7e226bb671a4 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/package.json b/packages/next/package.json index 35cb97df563f..75b8d2b1e3a0 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/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index 5d16661ace2d..b31065e23675 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 3e84fb69a567..9cd9abe590f0 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 645811547409..dfe5387e501f 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 From 68fb39a034d1b5d7eb8f2af23ca563956f1fa3be Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 30 Aug 2022 16:14:12 -0700 Subject: [PATCH 7/9] Fix handling with custom _error and pages/500 (#40110) This ensures we are properly calling `getInitialProps` in `_error` before serving `pages/500` as this has been the expected behavior since we introduced the process exiting behavior when deployed. This also ensures we don't attempt serving the `pages/500` before logging the error and exiting as this file isn't always expected to be present when statically optimized. Test deployments from provided reproduction can be seen here: - https://next-500-issue-ijdlh0e9y-ijjk-testing.vercel.app/ - https://next-500-issue-acn2vi68j-ijjk-testing.vercel.app/ ## Bug - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` Fixes: https://github.com/vercel/next.js/issues/40065 Fixes: https://github.com/vercel/next.js/issues/39952 --- packages/next/server/base-server.ts | 23 ++++-- packages/next/server/request-meta.ts | 1 + .../production/custom-error-500/index.test.ts | 77 +++++++++++++++++++ 3 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 test/production/custom-error-500/index.test.ts diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index a6da2bd95780..236f1fad44ff 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -1609,11 +1609,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 ( @@ -1625,6 +1630,10 @@ export default abstract class Server { } this.logError(getProperError(err)) } + const response = await this.renderErrorToResponse( + ctx, + isWrappedError ? (err as WrappedBuildError).innerError : err + ) return response } @@ -1713,7 +1722,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/request-meta.ts b/packages/next/server/request-meta.ts index 68ddb22a7280..39985a491848 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/test/production/custom-error-500/index.test.ts b/test/production/custom-error-500/index.test.ts new file mode 100644 index 000000000000..2dba5785f3cf --- /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/) + }) +}) From 29582c8b1ca967b1855db96c8b4d801050a04a45 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 30 Aug 2022 16:30:49 -0700 Subject: [PATCH 8/9] Fix edge rewrite handling (#40115) Follow-up to https://github.com/vercel/next.js/pull/40076 this ensures we handle rendering correctly when the URL doesn't match the edge function exactly e.g. when rewriting since it looks like we don't currently have access to the `x-matched-path` header like we do for serverless functions. ## Bug - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` Fixes: https://github.com/vercel/next.js/runs/8102187444?check_suite_focus=true Co-authored-by: Jiachi Liu --- packages/next/server/base-server.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 236f1fad44ff..25bc78ad3f25 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -1578,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) From 591f341d8236d8c978fecbc5226440d6359ec281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 31 Aug 2022 04:01:25 +0200 Subject: [PATCH 9/9] docs(security-headers): interest-cohort has been replaced by browsing-topics (#40113) See https://github.com/mdn/browser-compat-data/issues/9814 ## Documentation / Examples - [x] Make sure the linting passes by running `pnpm lint` - [x] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples) --- docs/advanced-features/security-headers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-features/security-headers.md b/docs/advanced-features/security-headers.md index 84e04c15d899..660c97711c15 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=()' } ```