From 7644c17f43b35a5b2b56ee99906fe89f7350ffba Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 15 Jul 2021 16:42:36 -0500 Subject: [PATCH 01/10] v11.0.2-canary.16 --- 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/package.json | 12 ++++++------ packages/react-dev-overlay/package.json | 2 +- packages/react-refresh-utils/package.json | 2 +- 14 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lerna.json b/lerna.json index 5e3d953eceb0..c7322b6ead6b 100644 --- a/lerna.json +++ b/lerna.json @@ -17,5 +17,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "11.0.2-canary.15" + "version": "11.0.2-canary.16" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index f685c0af0045..a69d632d1478 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "11.0.2-canary.15", + "version": "11.0.2-canary.16", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index d47e385b272a..98a07b862027 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "11.0.2-canary.15", + "version": "11.0.2-canary.16", "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": "11.0.2-canary.15", + "@next/eslint-plugin-next": "11.0.2-canary.16", "@rushstack/eslint-patch": "^1.0.6", "@typescript-eslint/parser": "^4.20.0", "eslint-import-resolver-node": "^0.3.4", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index abb2991da731..6cc6fd3a1ff9 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": "11.0.2-canary.15", + "version": "11.0.2-canary.16", "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 20c5eacccf7c..b4c69231c015 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "11.0.2-canary.15", + "version": "11.0.2-canary.16", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 27ce80350a2d..14ff49b540ca 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "11.0.2-canary.15", + "version": "11.0.2-canary.16", "license": "MIT", "dependencies": { "chalk": "4.1.0", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index f05342e26c7c..e272a2d7e17f 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "11.0.2-canary.15", + "version": "11.0.2-canary.16", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index f55ddc0d4f7c..0efbfa50e15e 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "11.0.2-canary.15", + "version": "11.0.2-canary.16", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 2ee282040269..207b0a5f68d8 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "11.0.2-canary.15", + "version": "11.0.2-canary.16", "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 ab79c9a3bd66..e3d81ca76630 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "11.0.2-canary.15", + "version": "11.0.2-canary.16", "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 f069d0f3816a..f692292e0d23 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "11.0.2-canary.15", + "version": "11.0.2-canary.16", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next/package.json b/packages/next/package.json index ce0abdb7be77..6ce4e5f7d88c 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "11.0.2-canary.15", + "version": "11.0.2-canary.16", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -65,10 +65,10 @@ "dependencies": { "@babel/runtime": "7.12.5", "@hapi/accept": "5.0.2", - "@next/env": "11.0.2-canary.15", - "@next/polyfill-module": "11.0.2-canary.15", - "@next/react-dev-overlay": "11.0.2-canary.15", - "@next/react-refresh-utils": "11.0.2-canary.15", + "@next/env": "11.0.2-canary.16", + "@next/polyfill-module": "11.0.2-canary.16", + "@next/react-dev-overlay": "11.0.2-canary.16", + "@next/react-refresh-utils": "11.0.2-canary.16", "assert": "2.0.0", "ast-types": "0.13.2", "browserify-zlib": "0.2.0", @@ -152,7 +152,7 @@ "@babel/preset-typescript": "7.12.7", "@babel/traverse": "^7.12.10", "@babel/types": "7.12.12", - "@next/polyfill-nomodule": "11.0.2-canary.15", + "@next/polyfill-nomodule": "11.0.2-canary.16", "@taskr/clear": "1.1.0", "@taskr/esnext": "1.1.0", "@taskr/watch": "1.1.0", diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index 372e7923356e..8ce9699048e2 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": "11.0.2-canary.15", + "version": "11.0.2-canary.16", "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 ac21dc76e9aa..22734c833856 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": "11.0.2-canary.15", + "version": "11.0.2-canary.16", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", From 3b388c346c6990c98e83357ad68263edc7081210 Mon Sep 17 00:00:00 2001 From: Janicklas Ralph Date: Thu, 15 Jul 2021 15:51:01 -0700 Subject: [PATCH 02/10] Fix Script beforeInteractive on navigation (#26995) ## Bug - [x] fixes #26342 - [x] Integration tests added - [x] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [x] Make sure the linting passes --- packages/next/client/script.tsx | 2 ++ test/integration/script-loader/pages/index.js | 2 ++ .../script-loader/test/index.test.js | 28 ++++++++++++++++--- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/next/client/script.tsx b/packages/next/client/script.tsx index 05e12cfdb54c..9ea57ee21589 100644 --- a/packages/next/client/script.tsx +++ b/packages/next/client/script.tsx @@ -152,6 +152,8 @@ function Script(props: ScriptProps): JSX.Element | null { }, ]) updateScripts(scripts) + } else { + loadScript(props) } } diff --git a/test/integration/script-loader/pages/index.js b/test/integration/script-loader/pages/index.js index 57949b975166..fdeb646515be 100644 --- a/test/integration/script-loader/pages/index.js +++ b/test/integration/script-loader/pages/index.js @@ -1,4 +1,5 @@ import Script from 'next/script' +import Link from 'next/link' const Page = () => { return ( @@ -8,6 +9,7 @@ const Page = () => { src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptAfterInteractive" >
index
+ Page1 ) } diff --git a/test/integration/script-loader/test/index.test.js b/test/integration/script-loader/test/index.test.js index 8913b8316009..282add413849 100644 --- a/test/integration/script-loader/test/index.test.js +++ b/test/integration/script-loader/test/index.test.js @@ -43,13 +43,13 @@ describe('Script Loader', () => { async function test(id) { const script = await browser.elementById(id) const endScripts = await browser.elementsByCss( - `#${id} ~ script[src^="/_next/static/"]` + `#__NEXT_DATA__ ~ #${id}` ) // Renders script tag expect(script).toBeDefined() // Script is inserted at the end - expect(endScripts.length).toBe(0) + expect(endScripts.length).toBe(1) } // afterInteractive script in page @@ -72,13 +72,13 @@ describe('Script Loader', () => { async function test(id) { const script = await browser.elementById(id) const endScripts = await browser.elementsByCss( - `#${id} ~ script[src^="/_next/static/"]` + `#__NEXT_DATA__ ~ #${id}` ) // Renders script tag expect(script).toBeDefined() // Script is inserted at the end - expect(endScripts.length).toBe(0) + expect(endScripts.length).toBe(1) } // lazyOnload script in page @@ -110,6 +110,26 @@ describe('Script Loader', () => { test('documentBeforeInteractive') }) + it('priority beforeInteractive on navigate', async () => { + let browser + try { + browser = await webdriver(appPort, '/') + + await browser.waitForElementByCss('[href="/page1"]') + await browser.click('#page1') + + await browser.waitForElementByCss('.container') + await waitFor(1000) + + const script = await browser.elementById('scriptBeforeInteractive') + + // Renders script tag + expect(script).toBeDefined() + } finally { + if (browser) await browser.close() + } + }) + it('onload fires correctly', async () => { let browser try { From f9795fdd2665369b60a153fbb6231f478e4a7d08 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Fri, 16 Jul 2021 11:21:44 +0200 Subject: [PATCH 03/10] improve static generation UX (#27171) #### improve export spinner update at least once a minute in non-tty update progress regularly when using the spinner decrease frequency of the spinner (windows console output is expensive) #### restart static page generation and collecting page data worker pools when hanging when for 1 minute no activity happens on the worker pool, restart it log a warning for hanging jobs #### add page generation duration to summary tree ![image](https://user-images.githubusercontent.com/1365881/125750454-8845f1b1-faf0-4598-b7a4-ea796b884691.png) for `[+n more pages]` is will show `(avg 321 ms)` when the average is over the threshold. It will allocate 8 lines for preview pages (instead of 4) when they contain slow pages ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [x] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes --- errors/manifest.json | 8 ++ errors/page-data-collection-timeout.md | 18 ++++ errors/static-page-generation-timeout.md | 18 ++++ packages/next/build/index.ts | 54 ++++++++++-- packages/next/build/utils.ts | 81 +++++++++++++++-- packages/next/export/index.ts | 59 +++++++++---- packages/next/export/worker.ts | 17 ++-- packages/next/lib/worker.ts | 88 +++++++++++++++++++ packages/next/server/config-shared.ts | 4 + .../[propsDuration]/[renderDuration].js | 34 +++++++ .../build-output/test/index.test.js | 56 +++++++++--- 11 files changed, 384 insertions(+), 53 deletions(-) create mode 100644 errors/page-data-collection-timeout.md create mode 100644 errors/static-page-generation-timeout.md create mode 100644 packages/next/lib/worker.ts create mode 100644 test/integration/build-output/fixtures/basic-app/pages/slow-static/[propsDuration]/[renderDuration].js diff --git a/errors/manifest.json b/errors/manifest.json index 9c52478cbf19..ef11ac95b965 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -414,6 +414,14 @@ { "title": "import-esm-externals", "path": "/errors/import-esm-externals.md" + }, + { + "title": "static-page-generation-timeout", + "path": "/errors/static-page-generation-timeout.md" + }, + { + "title": "page-data-collection-timeout", + "path": "/errors/page-data-collection-timeout.md" } ] } diff --git a/errors/page-data-collection-timeout.md b/errors/page-data-collection-timeout.md new file mode 100644 index 000000000000..12332bfb430b --- /dev/null +++ b/errors/page-data-collection-timeout.md @@ -0,0 +1,18 @@ +# Collecting page data timed out after multiple attempts + +#### Why This Error Occurred + +Next.js tries to restart the worker pool of the page data collection when no progress happens for a while, to avoid hanging builds. + +When restarted it will retry all uncompleted jobs, but if a job was unsuccessfully attempted multiple times, this will lead to an error. + +#### Possible Ways to Fix It + +- Make sure that there is no infinite loop during execution. +- Make sure all Promises in `getStaticPaths` `resolve` or `reject` correctly. +- Avoid very long timeouts for network requests. +- Increase the timeout by changing the `experimental.pageDataCollectionTimeout` configuration option (default `60` in seconds). + +### Useful Links + +- [`getStaticPaths`](https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation) diff --git a/errors/static-page-generation-timeout.md b/errors/static-page-generation-timeout.md new file mode 100644 index 000000000000..604e0364870f --- /dev/null +++ b/errors/static-page-generation-timeout.md @@ -0,0 +1,18 @@ +# Static page generation timed out after multiple attempts + +#### Why This Error Occurred + +Next.js tries to restart the worker pool of the static page generation when no progress happens for a while, to avoid hanging builds. + +When restarted it will retry all uncompleted jobs, but if a job was unsuccessfully attempted multiple times, this will lead to an error. + +#### Possible Ways to Fix It + +- Make sure that there is no infinite loop during execution. +- Make sure all Promises in `getStaticProps` `resolve` or `reject` correctly. +- Avoid very long timeouts for network requests. +- Increase the timeout by changing the `experimental.staticPageGenerationTimeout` configuration option (default `60` in seconds). + +### Useful Links + +- [`getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index b93335a2f91a..5dd7e2f143f4 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -2,7 +2,7 @@ import { loadEnvConfig } from '@next/env' import chalk from 'chalk' import crypto from 'crypto' import { promises, writeFileSync } from 'fs' -import { Worker } from 'jest-worker' +import { Worker } from '../lib/worker' import devalue from 'next/dist/compiled/devalue' import escapeStringRegexp from 'next/dist/compiled/escape-string-regexp' import findUp from 'next/dist/compiled/find-up' @@ -682,13 +682,38 @@ export default async function build( } = await staticCheckSpan.traceAsyncFn(async () => { process.env.NEXT_PHASE = PHASE_PRODUCTION_BUILD + const timeout = config.experimental.pageDataCollectionTimeout || 0 + let infoPrinted = false const staticCheckWorkers = new Worker(staticCheckWorker, { + timeout: timeout * 1000, + onRestart: (_method, [pagePath], attempts) => { + if (attempts >= 2) { + throw new Error( + `Collecting page data for ${pagePath} is still timing out after 2 attempts. See more info here https://nextjs.org/docs/messages/page-data-collection-timeout` + ) + } + Log.warn( + `Restarted collecting page data for ${pagePath} because it took more than ${timeout} seconds` + ) + if (!infoPrinted) { + Log.warn( + 'See more info here https://nextjs.org/docs/messages/page-data-collection-timeout' + ) + infoPrinted = true + } + }, numWorkers: config.experimental.cpus, enableWorkerThreads: config.experimental.workerThreads, - }) as Worker & typeof import('./utils') - - staticCheckWorkers.getStdout().pipe(process.stdout) - staticCheckWorkers.getStderr().pipe(process.stderr) + exposedMethods: [ + 'hasCustomGetInitialProps', + 'isPageStatic', + 'getNamedExports', + ], + }) as Worker & + Pick< + typeof import('./utils'), + 'hasCustomGetInitialProps' | 'isPageStatic' | 'getNamedExports' + > const runtimeEnvConfig = { publicRuntimeConfig: config.publicRuntimeConfig, @@ -879,6 +904,8 @@ export default async function build( isHybridAmp, ssgPageRoutes, initialRevalidateSeconds: false, + pageDuration: undefined, + ssgPageDurations: undefined, }) }) }) @@ -1065,6 +1092,7 @@ export default async function build( const exportConfig: any = { ...config, initialPageRevalidationMap: {}, + pageDurationMap: {}, ssgNotFoundPaths: [] as string[], // Default map will be the collection of automatic statically exported // pages and incremental pages. @@ -1312,6 +1340,18 @@ export default async function build( const hasAmp = hybridAmpPages.has(page) const file = normalizePagePath(page) + const pageInfo = pageInfos.get(page) + const durationInfo = exportConfig.pageDurationMap[page] + if (pageInfo && durationInfo) { + // Set Build Duration + if (pageInfo.ssgPageRoutes) { + pageInfo.ssgPageDurations = pageInfo.ssgPageRoutes.map( + (pagePath) => durationInfo[pagePath] + ) + } + pageInfo.pageDuration = durationInfo[page] + } + // The dynamic version of SSG pages are only prerendered if the // fallback is enabled. Below, we handle the specific prerenders // of these. @@ -1367,11 +1407,9 @@ export default async function build( } } // Set Page Revalidation Interval - const pageInfo = pageInfos.get(page) if (pageInfo) { pageInfo.initialRevalidateSeconds = exportConfig.initialPageRevalidationMap[page] - pageInfos.set(page, pageInfo) } } else { // For a dynamic SSG page, we did not copy its data exports and only @@ -1430,11 +1468,9 @@ export default async function build( } // Set route Revalidation Interval - const pageInfo = pageInfos.get(route) if (pageInfo) { pageInfo.initialRevalidateSeconds = exportConfig.initialPageRevalidationMap[route] - pageInfos.set(route, pageInfo) } } } diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 0f45e3e7d920..81559b22fa1b 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -67,6 +67,8 @@ export interface PageInfo { isSsg: boolean ssgPageRoutes: string[] | null initialRevalidateSeconds: number | false + pageDuration: number | undefined + ssgPageDurations: number[] | undefined } export async function printTreeView( @@ -101,6 +103,17 @@ export async function printTreeView( return chalk.red.bold(size) } + const MIN_DURATION = 300 + const getPrettyDuration = (_duration: number): string => { + const duration = `${_duration} ms` + // green for 300-1000ms + if (_duration < 1000) return chalk.green(duration) + // yellow for 1000-2000ms + if (_duration < 2000) return chalk.yellow(duration) + // red for >= 2000ms + return chalk.red.bold(duration) + } + const getCleanName = (fileName: string) => fileName // Trim off `static/` @@ -159,6 +172,10 @@ export async function printTreeView( const pageInfo = pageInfos.get(item) const ampFirst = buildManifest.ampFirstPages.includes(item) + const totalDuration = + (pageInfo?.pageDuration || 0) + + (pageInfo?.ssgPageDurations?.reduce((a, b) => a + (b || 0), 0) || 0) + messages.push([ `${symbol} ${ item === '/_app' @@ -172,6 +189,10 @@ export async function printTreeView( pageInfo?.initialRevalidateSeconds ? `${item} (ISR: ${pageInfo?.initialRevalidateSeconds} Seconds)` : item + }${ + totalDuration > MIN_DURATION + ? ` (${getPrettyDuration(totalDuration)})` + : '' }`, pageInfo ? ampFirst @@ -209,18 +230,64 @@ export async function printTreeView( if (pageInfo?.ssgPageRoutes?.length) { const totalRoutes = pageInfo.ssgPageRoutes.length - const previewPages = totalRoutes === 4 ? 4 : 3 const contSymbol = i === arr.length - 1 ? ' ' : '├' - const routes = pageInfo.ssgPageRoutes.slice(0, previewPages) - if (totalRoutes > previewPages) { - const remaining = totalRoutes - previewPages - routes.push(`[+${remaining} more paths]`) + let routes: { route: string; duration: number; avgDuration?: number }[] + if ( + pageInfo.ssgPageDurations && + pageInfo.ssgPageDurations.some((d) => d > MIN_DURATION) + ) { + const previewPages = totalRoutes === 8 ? 8 : Math.min(totalRoutes, 7) + const routesWithDuration = pageInfo.ssgPageRoutes + .map((route, idx) => ({ + route, + duration: pageInfo.ssgPageDurations![idx] || 0, + })) + .sort(({ duration: a }, { duration: b }) => + // Sort by duration + // keep too small durations in original order at the end + a <= MIN_DURATION && b <= MIN_DURATION ? 0 : b - a + ) + routes = routesWithDuration.slice(0, previewPages) + const remainingRoutes = routesWithDuration.slice(previewPages) + if (remainingRoutes.length) { + const remaining = remainingRoutes.length + const avgDuration = Math.round( + remainingRoutes.reduce( + (total, { duration }) => total + duration, + 0 + ) / remainingRoutes.length + ) + routes.push({ + route: `[+${remaining} more paths]`, + duration: 0, + avgDuration, + }) + } + } else { + const previewPages = totalRoutes === 4 ? 4 : Math.min(totalRoutes, 3) + routes = pageInfo.ssgPageRoutes + .slice(0, previewPages) + .map((route) => ({ route, duration: 0 })) + if (totalRoutes > previewPages) { + const remaining = totalRoutes - previewPages + routes.push({ route: `[+${remaining} more paths]`, duration: 0 }) + } } - routes.forEach((slug, index, { length }) => { + routes.forEach(({ route, duration, avgDuration }, index, { length }) => { const innerSymbol = index === length - 1 ? '└' : '├' - messages.push([`${contSymbol} ${innerSymbol} ${slug}`, '', '']) + messages.push([ + `${contSymbol} ${innerSymbol} ${route}${ + duration > MIN_DURATION ? ` (${getPrettyDuration(duration)})` : '' + }${ + avgDuration && avgDuration > MIN_DURATION + ? ` (avg ${getPrettyDuration(avgDuration)})` + : '' + }`, + '', + '', + ]) }) } }) diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index c7255c65f851..9877b1d3d5c0 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -7,7 +7,7 @@ import { readFileSync, writeFileSync, } from 'fs' -import { Worker } from 'jest-worker' +import { Worker } from '../lib/worker' import { dirname, join, resolve, sep } from 'path' import { promisify } from 'util' import { AmpPageStatus, formatAmpMessages } from '../build/output/index' @@ -40,7 +40,6 @@ import { } from '../server/normalize-page-path' import { loadEnvConfig } from '@next/env' import { PrerenderManifest } from '../build' -import exportPage from './worker' import { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' import { getPagePath } from '../server/require' import { trace } from '../telemetry/trace' @@ -68,6 +67,7 @@ const createProgress = (total: number, label: string) => { } let currentSegmentTotal = segments.shift() let currentSegmentCount = 0 + let lastProgressOutput = Date.now() let curProgress = 0 let progressSpinner = createSpinner(`${label} (${curProgress}/${total})`, { spinner: { @@ -88,21 +88,29 @@ const createProgress = (total: number, label: string) => { '[== ]', '[= ]', ], - interval: 80, + interval: 500, }, }) return () => { curProgress++ - currentSegmentCount++ - // Make sure we only log once per fully generated segment - if (currentSegmentCount !== currentSegmentTotal) { - return - } + // Make sure we only log once + // - per fully generated segment, or + // - per minute + // when not showing the spinner + if (!progressSpinner) { + currentSegmentCount++ + + if (currentSegmentCount === currentSegmentTotal) { + currentSegmentTotal = segments.shift() + currentSegmentCount = 0 + } else if (lastProgressOutput + 60000 > Date.now()) { + return + } - currentSegmentTotal = segments.shift() - currentSegmentCount = 0 + lastProgressOutput = Date.now() + } const newText = `${label} (${curProgress}/${total})` if (progressSpinner) { @@ -507,15 +515,31 @@ export default async function exportApp( ) } + const timeout = configuration?.experimental.staticPageGenerationTimeout || 0 + let infoPrinted = false const worker = new Worker(require.resolve('./worker'), { + timeout: timeout * 1000, + onRestart: (_method, [{ path }], attempts) => { + if (attempts >= 3) { + throw new Error( + `Static page generation for ${path} is still timing out after 3 attempts. See more info here https://nextjs.org/docs/messages/static-page-generation-timeout` + ) + } + Log.warn( + `Restarted static page genertion for ${path} because it took more than ${timeout} seconds` + ) + if (!infoPrinted) { + Log.warn( + 'See more info here https://nextjs.org/docs/messages/static-page-generation-timeout' + ) + infoPrinted = true + } + }, maxRetries: 0, numWorkers: threads, enableWorkerThreads: nextConfig.experimental.workerThreads, exposedMethods: ['default'], - }) as Worker & { default: typeof exportPage } - - worker.getStdout().pipe(process.stdout) - worker.getStderr().pipe(process.stderr) + }) as Worker & typeof import('./worker') let renderError = false const errorPaths: string[] = [] @@ -526,9 +550,10 @@ export default async function exportApp( pageExportSpan.setAttribute('path', path) return pageExportSpan.traceAsyncFn(async () => { + const pathMap = exportPathMap[path] const result = await worker.default({ path, - pathMap: exportPathMap[path], + pathMap, distDir, outDir, pagesDataDir, @@ -565,6 +590,10 @@ export default async function exportApp( if (result.ssgNotFound === true) { configuration.ssgNotFoundPaths.push(path) } + + const durations = (configuration.pageDurationMap[pathMap.page] = + configuration.pageDurationMap[pathMap.page] || {}) + durations[path] = result.duration } if (progress) progress() diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index cdfc87c47426..e061f6107de7 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -61,6 +61,7 @@ interface ExportPageResults { fromBuildExportRevalidate?: number error?: boolean ssgNotFound?: boolean + duration: number } interface RenderOpts { @@ -105,7 +106,8 @@ export default async function exportPage({ const exportPageSpan = trace('export-page-worker', parentSpanId) return exportPageSpan.traceAsyncFn(async () => { - let results: ExportPageResults = { + const start = Date.now() + let results: Omit = { ampValidations: [], } @@ -270,7 +272,7 @@ export default async function exportPage({ // for non-dynamic SSG pages we should have already // prerendered the file if (renderedDuringBuild((mod as ComponentModule).getStaticProps)) - return results + return { ...results, duration: Date.now() - start } if ( (mod as ComponentModule).getStaticProps && @@ -331,7 +333,7 @@ export default async function exportPage({ // for non-dynamic SSG pages we should have already // prerendered the file if (renderedDuringBuild(components.getStaticProps)) { - return results + return { ...results, duration: Date.now() - start } } // TODO: de-dupe the logic here between serverless and server mode @@ -473,18 +475,17 @@ export default async function exportPage({ } results.fromBuildExportRevalidate = (curRenderOpts as any).revalidate - if (results.ssgNotFound) { + if (!results.ssgNotFound) { // don't attempt writing to disk if getStaticProps returned not found - return results + await promises.writeFile(htmlFilepath, html, 'utf8') } - await promises.writeFile(htmlFilepath, html, 'utf8') - return results } catch (error) { console.error( `\nError occurred prerendering page "${path}". Read more: https://nextjs.org/docs/messages/prerender-error\n` + error.stack ) - return { ...results, error: true } + results.error = true } + return { ...results, duration: Date.now() - start } }) } diff --git a/packages/next/lib/worker.ts b/packages/next/lib/worker.ts new file mode 100644 index 000000000000..74f7f359435e --- /dev/null +++ b/packages/next/lib/worker.ts @@ -0,0 +1,88 @@ +import { Worker as JestWorker } from 'jest-worker' + +type FarmOptions = ConstructorParameters[1] + +const RESTARTED = Symbol('restarted') + +export class Worker { + private _worker: JestWorker | undefined + + constructor( + workerPath: string, + options: FarmOptions & { + timeout?: number + onRestart?: (method: string, args: any[], attempts: number) => void + exposedMethods: ReadonlyArray + } + ) { + let { timeout, onRestart, ...farmOptions } = options + + let restartPromise: Promise + let resolveRestartPromise: (arg: typeof RESTARTED) => void + let activeTasks = 0 + + this._worker = undefined + + const createWorker = () => { + this._worker = new JestWorker(workerPath, farmOptions) as JestWorker + restartPromise = new Promise( + (resolve) => (resolveRestartPromise = resolve) + ) + + this._worker.getStdout().pipe(process.stdout) + this._worker.getStderr().pipe(process.stderr) + } + createWorker() + + const onHanging = () => { + const worker = this._worker + if (!worker) return + const resolve = resolveRestartPromise + createWorker() + worker.end().then(() => { + resolve(RESTARTED) + }) + } + + let hangingTimer: number | false = false + + const onActivity = () => { + if (hangingTimer) clearTimeout(hangingTimer) + hangingTimer = activeTasks > 0 && setTimeout(onHanging, timeout) + } + + for (const method of farmOptions.exposedMethods) { + if (method.startsWith('_')) continue + ;(this as any)[method] = timeout + ? // eslint-disable-next-line no-loop-func + async (...args: any[]) => { + activeTasks++ + try { + let attempts = 0 + for (;;) { + onActivity() + const result = await Promise.race([ + (this._worker as any)[method](...args), + restartPromise, + ]) + if (result !== RESTARTED) return result + if (onRestart) onRestart(method, args, ++attempts) + } + } finally { + activeTasks-- + onActivity() + } + } + : (this._worker as any)[method].bind(this._worker) + } + } + + end(): ReturnType { + const worker = this._worker + if (!worker) { + throw new Error('Farm is ended, no more calls can be done to it') + } + this._worker = undefined + return worker.end() + } +} diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts index a9e2a08d28f0..c47bad548827 100644 --- a/packages/next/server/config-shared.ts +++ b/packages/next/server/config-shared.ts @@ -94,6 +94,8 @@ export type NextConfig = { [key: string]: any } & { gzipSize?: boolean craCompat?: boolean esmExternals?: boolean | 'loose' + staticPageGenerationTimeout?: number + pageDataCollectionTimeout?: number } } @@ -157,6 +159,8 @@ export const defaultConfig: NextConfig = { gzipSize: true, craCompat: false, esmExternals: false, + staticPageGenerationTimeout: 60, + pageDataCollectionTimeout: 60, }, future: { strictPostcssConfiguration: false, diff --git a/test/integration/build-output/fixtures/basic-app/pages/slow-static/[propsDuration]/[renderDuration].js b/test/integration/build-output/fixtures/basic-app/pages/slow-static/[propsDuration]/[renderDuration].js new file mode 100644 index 000000000000..ad8dc3915cb8 --- /dev/null +++ b/test/integration/build-output/fixtures/basic-app/pages/slow-static/[propsDuration]/[renderDuration].js @@ -0,0 +1,34 @@ +export default ({ renderDuration }) => { + const target = Date.now() + (+renderDuration || 200) + while (Date.now() < target); + return
{renderDuration}
+} + +export function getStaticPaths() { + return { + paths: [ + [2000, 10], + [5, 5], + [25, 25], + [20, 20], + [10, 10], + [15, 15], + [15, 10], + [10, 10], + [300, 10], + [10, 1000], + ].map(([propsDuration, renderDuration]) => ({ + params: { + renderDuration: `${renderDuration}`, + propsDuration: `${propsDuration}`, + }, + })), + fallback: true, + } +} + +export async function getStaticProps({ params }) { + const { renderDuration, propsDuration } = params + await new Promise((r) => setTimeout(r, +propsDuration)) + return { props: { renderDuration } } +} diff --git a/test/integration/build-output/test/index.test.js b/test/integration/build-output/test/index.test.js index 687f276b1051..0b865e643bf2 100644 --- a/test/integration/build-output/test/index.test.js +++ b/test/integration/build-output/test/index.test.js @@ -43,7 +43,7 @@ describe('Build Output', () => { stdout: true, })) - expect(stdout).toMatch(/\/ [ ]* \d{1,} B/) + expect(stdout).toMatch(/\/ (.* )?\d{1,} B/) expect(stdout).toMatch(/\+ First Load JS shared by all [ 0-9.]* kB/) expect(stdout).toMatch(/ chunks\/main\.[0-9a-z]{6}\.js [ 0-9.]* kB/) expect(stdout).toMatch( @@ -156,6 +156,30 @@ describe('Build Output', () => { expect(frameworkSize.endsWith('kB')).toBe(true) }) + it('should print duration when rendering or get static props takes long', () => { + const matches = stdout.match( + / \/slow-static\/.+\/.+(?: \(\d+ ms\))?| \[\+\d+ more paths\]/g + ) + + expect(matches).toEqual([ + // summary + expect.stringMatching( + /\/\[propsDuration\]\/\[renderDuration\] \(\d+ ms\)/ + ), + // ordered by duration, includes duration + expect.stringMatching(/\/2000\/10 \(\d+ ms\)$/), + expect.stringMatching(/\/10\/1000 \(\d+ ms\)$/), + expect.stringMatching(/\/300\/10 \(\d+ ms\)$/), + // kept in original order + expect.stringMatching(/\/5\/5$/), + expect.stringMatching(/\/25\/25$/), + expect.stringMatching(/\/20\/20$/), + expect.stringMatching(/\/10\/10$/), + // max of 7 preview paths + ' [+2 more paths]', + ]) + }) + it('should not emit extracted comments', async () => { const files = await recursiveReadDir( join(appDir, '.next'), @@ -179,11 +203,13 @@ describe('Build Output', () => { stdout: true, }) - expect(stdout).toMatch(/\/ [ ]* \d{1,} B/) - expect(stdout).toMatch(/\/_app [ ]* \d{1,} B/) - expect(stdout).toMatch(/\+ First Load JS shared by all [ 0-9.]* kB/) - expect(stdout).toMatch(/ chunks\/main\.[0-9a-z]{6}\.js [ 0-9.]* kB/) - expect(stdout).toMatch(/ chunks\/framework\.[0-9a-z]{6}\.js [ 0-9. ]* kB/) + expect(stdout).toMatch(/\/ (.* )?\d{1,} B/) + expect(stdout).toMatch(/\/_app (.* )?\d{1,} B/) + expect(stdout).toMatch(/\+ First Load JS shared by all \s*[0-9.]+ kB/) + expect(stdout).toMatch(/ chunks\/main\.[0-9a-z]{6}\.js \s*[0-9.]+ kB/) + expect(stdout).toMatch( + / chunks\/framework\.[0-9a-z]{6}\.js \s*[0-9.]+ kB/ + ) expect(stdout).not.toContain(' /_document') expect(stdout).not.toContain(' /_error') @@ -206,12 +232,14 @@ describe('Build Output', () => { stdout: true, }) - expect(stdout).toMatch(/\/ [ 0-9.]* B [ 0-9.]* kB/) - expect(stdout).toMatch(/\/amp .* AMP/) - expect(stdout).toMatch(/\/hybrid [ 0-9.]* B/) - expect(stdout).toMatch(/\+ First Load JS shared by all [ 0-9.]* kB/) - expect(stdout).toMatch(/ chunks\/main\.[0-9a-z]{6}\.js [ 0-9.]* kB/) - expect(stdout).toMatch(/ chunks\/framework\.[0-9a-z]{6}\.js [ 0-9. ]* kB/) + expect(stdout).toMatch(/\/ (.* )?[0-9.]+ B \s*[0-9.]+ kB/) + expect(stdout).toMatch(/\/amp (.* )?AMP/) + expect(stdout).toMatch(/\/hybrid (.* )?[0-9.]+ B/) + expect(stdout).toMatch(/\+ First Load JS shared by all \s*[0-9.]+ kB/) + expect(stdout).toMatch(/ chunks\/main\.[0-9a-z]{6}\.js \s*[0-9.]+ kB/) + expect(stdout).toMatch( + / chunks\/framework\.[0-9a-z]{6}\.js \s*[0-9.]+ kB/ + ) expect(stdout).not.toContain(' /_document') expect(stdout).not.toContain(' /_error') @@ -233,8 +261,8 @@ describe('Build Output', () => { stdout: true, }) - expect(stdout).toMatch(/\/ [ ]* \d{1,} B/) - expect(stdout).toMatch(/λ \/404 [ ]* \d{1,} B/) + expect(stdout).toMatch(/\/ (.* )?\d{1,} B/) + expect(stdout).toMatch(/λ \/404 (.* )?\d{1,} B/) expect(stdout).toMatch(/\+ First Load JS shared by all [ 0-9.]* kB/) expect(stdout).toMatch(/ chunks\/main\.[0-9a-z]{6}\.js [ 0-9.]* kB/) expect(stdout).toMatch(/ chunks\/framework\.[0-9a-z]{6}\.js [ 0-9. ]* kB/) From 530a2a397daec636459e1986b5ef28d5c760131c Mon Sep 17 00:00:00 2001 From: Thang Vu <31528554+ThangHuuVu@users.noreply.github.com> Date: Fri, 16 Jul 2021 19:28:31 +0700 Subject: [PATCH 04/10] upgrade next-redux-wrapper to 7.0.2 (#26521) ## Bug - [x] Related issues linked using `fixes #number`: Fixes #26338 - [ ] Integration tests added --- examples/with-redux-wrapper/package.json | 2 +- examples/with-redux-wrapper/pages/index.js | 2 +- examples/with-redux-wrapper/pages/other.js | 10 ++++------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/examples/with-redux-wrapper/package.json b/examples/with-redux-wrapper/package.json index 8a76f71a1a7d..109c3672d0c0 100644 --- a/examples/with-redux-wrapper/package.json +++ b/examples/with-redux-wrapper/package.json @@ -7,7 +7,7 @@ }, "dependencies": { "next": "9.4.1", - "next-redux-wrapper": "^6.0.1", + "next-redux-wrapper": "^7.0.2", "react": "^17.0.2", "react-dom": "^17.0.2", "react-redux": "7.1.3", diff --git a/examples/with-redux-wrapper/pages/index.js b/examples/with-redux-wrapper/pages/index.js index 1030b9380e42..07fb7fcfbdb2 100644 --- a/examples/with-redux-wrapper/pages/index.js +++ b/examples/with-redux-wrapper/pages/index.js @@ -18,7 +18,7 @@ const Index = (props) => { return } -export const getStaticProps = wrapper.getStaticProps(async ({ store }) => { +export const getStaticProps = wrapper.getStaticProps((store) => () => { store.dispatch(serverRenderClock(true)) store.dispatch(addCount()) }) diff --git a/examples/with-redux-wrapper/pages/other.js b/examples/with-redux-wrapper/pages/other.js index 13b69f550220..baddaee9b8a9 100644 --- a/examples/with-redux-wrapper/pages/other.js +++ b/examples/with-redux-wrapper/pages/other.js @@ -18,12 +18,10 @@ const Other = (props) => { return } -export const getServerSideProps = wrapper.getServerSideProps( - async ({ store }) => { - store.dispatch(serverRenderClock(true)) - store.dispatch(addCount()) - } -) +export const getServerSideProps = wrapper.getServerSideProps((store) => () => { + store.dispatch(serverRenderClock(true)) + store.dispatch(addCount()) +}) const mapDispatchToProps = (dispatch) => { return { From dd029f559fb014ce5ecdd06155ed15368ce39fc6 Mon Sep 17 00:00:00 2001 From: Munawwar Date: Fri, 16 Jul 2021 17:41:52 +0400 Subject: [PATCH 05/10] updated zustand example for 3.5.4+ interface change (#27229) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Documentation / Examples - [✓] Make sure the linting passes --- examples/with-zustand/lib/store.js | 43 ++++++++++++++--------------- examples/with-zustand/package.json | 2 +- examples/with-zustand/pages/_app.js | 6 ++-- 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/examples/with-zustand/lib/store.js b/examples/with-zustand/lib/store.js index 75d296fa78ff..bd5c6db2258e 100644 --- a/examples/with-zustand/lib/store.js +++ b/examples/with-zustand/lib/store.js @@ -44,30 +44,27 @@ export const initializeStore = (preloadedState = {}) => { })) } -export function useHydrate(initialState) { - let _store = store ?? initializeStore(initialState) - +export function useCreateStore(initialState) { // For SSR & SSG, always use a new store. - if (typeof window !== 'undefined') { - // For CSR, always re-use same store. - if (!store) { - store = _store - } - - // And if initialState changes, then merge states in the next render cycle. - // - // eslint complaining "React Hooks must be called in the exact same order in every component render" - // is ignorable as this code runs in the same order in a given environment (CSR/SSR/SSG) - // eslint-disable-next-line react-hooks/rules-of-hooks - useLayoutEffect(() => { - if (initialState && store) { - store.setState({ - ...store.getState(), - ...initialState, - }) - } - }, [initialState]) + if (typeof window === 'undefined') { + return () => initializeStore(initialState) } - return _store + // For CSR, always re-use same store. + store = store ?? initializeStore(initialState) + // And if initialState changes, then merge states in the next render cycle. + // + // eslint complaining "React Hooks must be called in the exact same order in every component render" + // is ignorable as this code runs in same order in a given environment + // eslint-disable-next-line react-hooks/rules-of-hooks + useLayoutEffect(() => { + if (initialState && store) { + store.setState({ + ...store.getState(), + ...initialState, + }) + } + }, [initialState]) + + return () => store } diff --git a/examples/with-zustand/package.json b/examples/with-zustand/package.json index 0ed36281c6d5..dd3e3052fcc3 100644 --- a/examples/with-zustand/package.json +++ b/examples/with-zustand/package.json @@ -9,7 +9,7 @@ "next": "latest", "react": "^17.0.2", "react-dom": "^17.0.2", - "zustand": "3.5.1" + "zustand": "^3.5.4" }, "license": "MIT" } diff --git a/examples/with-zustand/pages/_app.js b/examples/with-zustand/pages/_app.js index 082837c7aac2..9488e2e3260b 100644 --- a/examples/with-zustand/pages/_app.js +++ b/examples/with-zustand/pages/_app.js @@ -1,9 +1,9 @@ -import { useHydrate, Provider } from '../lib/store' +import { useCreateStore, Provider } from '../lib/store' export default function App({ Component, pageProps }) { - const store = useHydrate(pageProps.initialZustandState) + const createStore = useCreateStore(pageProps.initialZustandState) return ( - + ) From 14dd7c2954c82ffab51345591f2e6f5f51e28264 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 16 Jul 2021 09:59:54 -0500 Subject: [PATCH 06/10] Add warning for large number of routes (#27214) This adds a warning when more than 1024 routes are added since it can have performance impacts and includes a document that we can add suggestions to reduce the number of routes being added. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes --- errors/manifest.json | 4 ++++ errors/max-custom-routes-reached.md | 17 +++++++++++++++++ packages/next/lib/load-custom-routes.ts | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 errors/max-custom-routes-reached.md diff --git a/errors/manifest.json b/errors/manifest.json index ef11ac95b965..1ba5b512db5f 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -415,6 +415,10 @@ "title": "import-esm-externals", "path": "/errors/import-esm-externals.md" }, + { + "title": "max-custom-routes-reached", + "path": "max-custom-routes-reached.md" + }, { "title": "static-page-generation-timeout", "path": "/errors/static-page-generation-timeout.md" diff --git a/errors/max-custom-routes-reached.md b/errors/max-custom-routes-reached.md new file mode 100644 index 000000000000..94398ff98777 --- /dev/null +++ b/errors/max-custom-routes-reached.md @@ -0,0 +1,17 @@ +# Max Custom Routes Reached + +#### Why This Error Occurred + +The number of combined routes from `headers`, `redirects`, and `rewrites` exceeds 1000. This can impact performance because each request will iterate over all routes to check for a match in the worst case. + +#### Possible Ways to Fix It + +- Leverage dynamic routes inside of the `pages` folder to reduce the number of rewrites needed +- Combine headers routes into dynamic matches e.g. `/first-header-route` `/second-header-route` -> `/(first-header-route$|second-header-route$)` + +### Useful Links + +- [Dynamic Routes documentation](https://nextjs.org/docs/routing/dynamic-routes) +- [Rewrites documentation](https://nextjs.org/docs/api-reference/next.config.js/rewrites) +- [Redirects documentation](https://nextjs.org/docs/api-reference/next.config.js/redirects) +- [Headers documentation](https://nextjs.org/docs/api-reference/next.config.js/headers) diff --git a/packages/next/lib/load-custom-routes.ts b/packages/next/lib/load-custom-routes.ts index ca4240bcf798..be3fb097b404 100644 --- a/packages/next/lib/load-custom-routes.ts +++ b/packages/next/lib/load-custom-routes.ts @@ -1,3 +1,4 @@ +import chalk from 'chalk' import { parse as parseUrl } from 'url' import { NextConfig } from '../server/config' import * as pathToRegexp from 'next/dist/compiled/path-to-regexp' @@ -621,6 +622,24 @@ export default async function loadCustomRoutes( loadRedirects(config), ]) + const totalRewrites = + rewrites.beforeFiles.length + + rewrites.afterFiles.length + + rewrites.fallback.length + + const totalRoutes = headers.length + redirects.length + totalRewrites + + if (totalRoutes > 1000) { + console.warn( + chalk.bold.yellow(`Warning: `) + + `total number of custom routes exceeds 1000, this can reduce performance. Route counts:\n` + + `headers: ${headers.length}\n` + + `rewrites: ${totalRewrites}\n` + + `redirects: ${redirects.length}\n` + + `See more info: https://nextjs.org/docs/messages/max-custom-routes-reached` + ) + } + if (config.trailingSlash) { redirects.unshift( { From 3fc5f73dc9232acc67bcf9a24e1c2218a8c6dcab Mon Sep 17 00:00:00 2001 From: Remek Ambroziak Date: Fri, 16 Jul 2021 17:05:20 +0200 Subject: [PATCH 07/10] Fix typo in env howto (#27227) --- docs/basic-features/environment-variables.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-features/environment-variables.md b/docs/basic-features/environment-variables.md index bb8fe78c13b0..7e2ff8e70c3b 100644 --- a/docs/basic-features/environment-variables.md +++ b/docs/basic-features/environment-variables.md @@ -110,7 +110,7 @@ Next.js allows you to set defaults in `.env` (all environments), `.env.developme `.env.local` always overrides the defaults set. -> **Note**: `.env`, `.env.development`, and `.env.production` files should be included in your repository as they define defaults. **`.env*.local` should be added to `.gitignore`**, as those files are intended to be ignored. `.env.local` is where secrets can be stored. +> **Note**: `.env`, `.env.development`, and `.env.production` files should be included in your repository as they define defaults. **`.env.*.local` should be added to `.gitignore`**, as those files are intended to be ignored. `.env.local` is where secrets can be stored. ## Environment Variables on Vercel From ccb62f8636f3f4a25d7ea58edd3d9747cd6c0f99 Mon Sep 17 00:00:00 2001 From: Thomas Marshall Date: Fri, 16 Jul 2021 16:11:12 +0100 Subject: [PATCH 08/10] Add x-forward headers to external rewrites (#17557) * Add x-forward headers to external rewrites This commit configures the proxy used for external rewrites to include x-forward headers [1]. This is particularly useful for incremental adoption, where some routes will be handled by Next.js and others by a different website. For example, a Rails app will use the X-Forwarded-Host header to determine which host to use for URL generation and redirects [2]. [1]: https://github.com/http-party/node-http-proxy/blob/91fee3e943dc4497e8dd4ef27116388dce091988/lib/http-proxy.js#L31 [2]: https://github.com/rails/rails/blob/41139f6ba27556e710ecad93f3cd241ea6764f9d/actionpack/lib/action_dispatch/http/url.rb#L221-L227 * Handle image-optimizer case Co-authored-by: JJ Kasper --- packages/next/server/image-optimizer.ts | 2 ++ packages/next/server/next-server.ts | 1 + test/integration/custom-routes/test/index.test.js | 8 ++++++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index 6a89d560dce8..7820d1a361a1 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -244,6 +244,7 @@ export async function imageOptimizer( mockRes.setHeader = (name: string, value: string | string[]) => (mockHeaders[name.toLowerCase()] = value) mockRes._implicitHeader = () => {} + mockRes.connection = res.connection mockRes.finished = false mockRes.statusCode = 200 @@ -258,6 +259,7 @@ export async function imageOptimizer( mockReq.headers = req.headers mockReq.method = req.method mockReq.url = href + mockReq.connection = req.connection await server.getRequestHandler()( mockReq, diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index 26afcff6d8b6..74de6480cff8 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -963,6 +963,7 @@ export default class Server { target, changeOrigin: true, ignorePath: true, + xfwd: true, proxyTimeout: 30_000, // limit proxying to 30 seconds }) diff --git a/test/integration/custom-routes/test/index.test.js b/test/integration/custom-routes/test/index.test.js index 17d77a3faa5a..be75525a3f02 100644 --- a/test/integration/custom-routes/test/index.test.js +++ b/test/integration/custom-routes/test/index.test.js @@ -646,7 +646,9 @@ const runTests = (isDev = false) => { }, }, ]) - expect(await res.text()).toContain('hi from external') + const nextHost = `localhost:${appPort}` + const externalHost = `localhost:${externalServerPort}` + expect(await res.text()).toContain(`hi ${nextHost} from ${externalHost}`) }) it('should support unnamed parameters correctly', async () => { @@ -1911,7 +1913,9 @@ describe('Custom routes', () => { externalServerPort = await findPort() externalServer = http.createServer((req, res) => { externalServerHits.add(req.url) - res.end('hi from external') + const nextHost = req.headers['x-forwarded-host'] + const externalHost = req.headers['host'] + res.end(`hi ${nextHost} from ${externalHost}`) }) await new Promise((resolve, reject) => { externalServer.listen(externalServerPort, (error) => { From 057d338db438ae97ebae776dd46eb04520ac9e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= Date: Fri, 16 Jul 2021 16:17:27 +0100 Subject: [PATCH 09/10] Update cms-wordpress readme file (#27234) ## Documentation / Examples - [x] Make sure the linting passes - Removed references to [WPGraphiQL](https://github.com/wp-graphql/wp-graphiql) as it is now part of [WPGraphQL](https://github.com/wp-graphql/wp-graphql) plugin. --- examples/cms-wordpress/README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/cms-wordpress/README.md b/examples/cms-wordpress/README.md index aabba005225d..4b057935a00a 100644 --- a/examples/cms-wordpress/README.md +++ b/examples/cms-wordpress/README.md @@ -60,11 +60,9 @@ Once the site is ready, you'll need to install the [WPGraphQL](https://www.wpgra ![WPGraphQL installed](./docs/plugin-installed.png) -#### Optional: Add WPGraphiQL +#### GraphiQL -The [WPGraphiQL](https://github.com/wp-graphql/wp-graphiql) plugin gives you access to a GraphQL IDE directly from your WordPress Admin, allowing you to inspect and play around with the GraphQL API. - -The process to add WPGraphiQL is the same as the one for WPGraphQL: Go to the [WPGraphiQL repo](https://github.com/wp-graphql/wp-graphiql), download the ZIP archive, and install it as a plugin in your WordPress site. Once that's done you should be able to access the GraphiQL page in the admin: +The [WPGraphQL](https://www.wpgraphql.com/) plugin also gives you access to a GraphQL IDE directly from your WordPress Admin, allowing you to inspect and play around with the GraphQL API. ![WPGraphiQL page](./docs/wp-graphiql.png) From 6e99f7af39d5a31bab8c227e945bd3e765b3d3fe Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 16 Jul 2021 11:13:40 -0500 Subject: [PATCH 10/10] Add note about beforeFiles continuing (#27211) This adds a note explaining that `beforeFiles` continue instead of checking the filesystem/dynamic routes immediately like they do in `afterFiles` and `fallback`. ## Documentation / Examples - [x] Make sure the linting passes Closes: https://github.com/vercel/next.js/issues/26795 --- docs/api-reference/next.config.js/rewrites.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-reference/next.config.js/rewrites.md b/docs/api-reference/next.config.js/rewrites.md index f9ca6aaa9718..cf092773dd08 100644 --- a/docs/api-reference/next.config.js/rewrites.md +++ b/docs/api-reference/next.config.js/rewrites.md @@ -87,6 +87,8 @@ module.exports = { } ``` +Note: rewrites in `beforeFiles` do not check the filesystem/dynamic routes immediately after matching a source, they continue until all `beforeFiles` have been checked. + ## Rewrite parameters When using parameters in a rewrite the parameters will be passed in the query by default when none of the parameters are used in the `destination`.