From 129fb85da4a35fdcac7c6af90bd338905f91fac9 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 26 Oct 2020 18:07:23 +0100 Subject: [PATCH 01/12] Update image-optimization.md --- docs/basic-features/image-optimization.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index 886e8362555e5..16a3f0cac467c 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -106,7 +106,6 @@ For more information on what to do next, we recommend the following sections: -
- When using `next start` or a custom server image optimization works automatically. - [Vercel](https://vercel.com): Works automatically when you deploy on Vercel - [Imgix](https://www.imgix.com): `loader: 'imgix'` From 8db23295d393a63ca8546b5d4c4f9ebdac8bfff7 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 26 Oct 2020 18:12:40 +0100 Subject: [PATCH 02/12] Update image-optimization.md --- docs/basic-features/image-optimization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index 16a3f0cac467c..17ec5e047efd2 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -90,10 +90,10 @@ module.exports = { The following Image Optimization cloud providers are supported: +- Vercel - No configuration necessary - Imgix - `loader: 'imgix'` - Cloudinary - `loader: 'cloudinary'` - Akamai - `loader: 'akamai'` -- Vercel - No configuration necessary ## Related From c381a35247f17e403ca8d2166aea5b6ca7a01f66 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 26 Oct 2020 18:13:21 +0100 Subject: [PATCH 03/12] Update image-optimization.md --- docs/basic-features/image-optimization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index 17ec5e047efd2..80b68a6e6fba3 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -2,7 +2,7 @@ description: Next.js supports built-in image optimization, as well as third party loaders for Imgix, Cloudinary, and more! Learn more here. --- -# Image Optimization +# Image Component and Image Optimization
Examples From 557a0001dbe4030a3cad991c030e9095353473c9 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 26 Oct 2020 18:17:26 +0100 Subject: [PATCH 04/12] Update image-optimization.md --- docs/basic-features/image-optimization.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index 80b68a6e6fba3..f88194d86bb48 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -90,10 +90,12 @@ module.exports = { The following Image Optimization cloud providers are supported: -- Vercel - No configuration necessary -- Imgix - `loader: 'imgix'` -- Cloudinary - `loader: 'cloudinary'` -- Akamai - `loader: 'akamai'` +- When using `next start` or a custom server image optimization works automatically. +- [Vercel](https://vercel.com): Works automatically when you deploy on Vercel +- [Imgix](https://www.imgix.com): `loader: 'imgix'` +- [Cloudinary](https://cloudinary.com): `loader: 'cloudinary'` +- [Akamai](https://www.akamai.com): `loader: 'akamai'` + ## Related @@ -106,8 +108,3 @@ For more information on what to do next, we recommend the following sections:
-- When using `next start` or a custom server image optimization works automatically. -- [Vercel](https://vercel.com): Works automatically when you deploy on Vercel -- [Imgix](https://www.imgix.com): `loader: 'imgix'` -- [Cloudinary](https://cloudinary.com): `loader: 'cloudinary'` -- [Akamai](https://www.akamai.com): `loader: 'akamai'` From 3a169fbbf0e087b4430c66f5af835feacdb8a07c Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 26 Oct 2020 16:07:52 -0400 Subject: [PATCH 05/12] Separate config into `deviceSizes` and `iconSizes` (#18267) This separates the `next.config.js` property `images.sizes` into to properties: `images.deviceSizes` and `images.iconSizes`. The purpose is for images that are not intended to take up the majority of the viewport. Related to #18122 --- docs/basic-features/image-optimization.md | 20 ++++++--- packages/next/build/index.ts | 6 ++- packages/next/build/webpack-config.ts | 3 +- packages/next/client/image.tsx | 25 +++++++---- packages/next/next-server/server/config.ts | 44 +++++++++++++++---- .../next-server/server/image-optimizer.ts | 12 ++++- .../image-component/basic/next.config.js | 3 +- .../basic/pages/client-side.js | 14 ++++++ .../image-component/basic/pages/index.js | 14 ++++++ .../image-component/basic/test/index.test.js | 14 ++++++ .../image-optimizer/test/index.test.js | 44 +++++++++++++++---- 11 files changed, 165 insertions(+), 34 deletions(-) diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index f88194d86bb48..fbe1ab86c60c0 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -50,14 +50,26 @@ export default Home You can configure Image Optimization by using the `images` property in `next.config.js`. -### Sizes +### Device Sizes -You can specify a list of image widths to allow using the `sizes` property. Since images maintain their aspect ratio using the `width` and `height` attributes of the source image, there is no need to specify height in `next.config.js` – only the width. You can think of these as breakpoints. +You can specify a list of device width breakpoints using the `deviceSizes` property. Since images maintain their aspect ratio using the `width` and `height` attributes of the source image, there is no need to specify height in `next.config.js` – only the width. These values will be used by the browser to determine which size image should load. ```js module.exports = { images: { - sizes: [320, 420, 768, 1024, 1200], + deviceSizes: [320, 420, 768, 1024, 1200], + }, +} +``` + +### Icon Sizes + +You can specify a list of icon image widths using the `iconSizes` property. These widths should be smaller than the smallest value in `deviceSizes`. The purpose is for images that don't scale with the browser window, such as icons or badges. If `iconSizes` is not defined, then `deviceSizes` will be used. + +```js +module.exports = { + images: { + iconSizes: [16, 32, 64], }, } ``` @@ -96,7 +108,6 @@ The following Image Optimization cloud providers are supported: - [Cloudinary](https://cloudinary.com): `loader: 'cloudinary'` - [Akamai](https://www.akamai.com): `loader: 'akamai'` - ## Related For more information on what to do next, we recommend the following sections: @@ -107,4 +118,3 @@ For more information on what to do next, we recommend the following sections: See all available properties for the Image component - diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 81256b9875275..67cdd21d3b353 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -1110,11 +1110,15 @@ export default async function build( ) } + const images = { ...config.images } + const { deviceSizes, iconSizes } = images + images.sizes = [...deviceSizes, ...iconSizes] + await promises.writeFile( path.join(distDir, IMAGES_MANIFEST), JSON.stringify({ version: 1, - images: config.images, + images, }), 'utf8' ) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index d174521b7ad9a..3559fb5f955c5 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -991,7 +991,8 @@ export default async function getBaseWebpackConfig( config.experimental.scrollRestoration ), 'process.env.__NEXT_IMAGE_OPTS': JSON.stringify({ - sizes: config.images.sizes, + deviceSizes: config.images.deviceSizes, + iconSizes: config.images.iconSizes, path: config.images.path, loader: config.images.loader, ...(dev diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 457b2211d1869..d938348a338aa 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -14,7 +14,8 @@ const loaders = new Map string>([ type LoaderKey = 'imgix' | 'cloudinary' | 'akamai' | 'default' type ImageData = { - sizes: number[] + deviceSizes: number[] + iconSizes: number[] loader: LoaderKey path: string domains?: string[] @@ -36,12 +37,15 @@ type ImageProps = Omit< const imageData: ImageData = process.env.__NEXT_IMAGE_OPTS as any const { - sizes: configSizes, + deviceSizes: configDeviceSizes, + iconSizes: configIconSizes, loader: configLoader, path: configPath, domains: configDomains, } = imageData -configSizes.sort((a, b) => a - b) // smallest to largest +// sort smallest to largest +configDeviceSizes.sort((a, b) => a - b) +configIconSizes.sort((a, b) => a - b) let cachedObserver: IntersectionObserver const IntersectionObserver = @@ -79,12 +83,16 @@ function getObserver(): IntersectionObserver | undefined { )) } -function getWidthsFromConfig(width: number | undefined) { +function getDeviceSizes(width: number | undefined): number[] { if (typeof width !== 'number') { - return configSizes + return configDeviceSizes + } + const smallest = configDeviceSizes[0] + if (width < smallest && configIconSizes.includes(width)) { + return [width] } const widths: number[] = [] - for (let size of configSizes) { + for (let size of configDeviceSizes) { widths.push(size) if (size >= width) { break @@ -102,7 +110,7 @@ function computeSrc( if (unoptimized) { return src } - const widths = getWidthsFromConfig(width) + const widths = getDeviceSizes(width) const largest = widths[widths.length - 1] return callLoader({ src, width: largest, quality }) } @@ -136,7 +144,8 @@ function generateSrcSet({ if (unoptimized) { return undefined } - return getWidthsFromConfig(width) + + return getDeviceSizes(width) .map((w) => `${callLoader({ src, width: w, quality })} ${w}w`) .join(', ') } diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts index a489544e3b8ea..1940286b801c1 100644 --- a/packages/next/next-server/server/config.ts +++ b/packages/next/next-server/server/config.ts @@ -24,7 +24,8 @@ const defaultConfig: { [key: string]: any } = { poweredByHeader: true, compress: true, images: { - sizes: [320, 420, 768, 1024, 1200], + deviceSizes: [320, 420, 768, 1024, 1200], + iconSizes: [], domains: [], path: '/_next/image', loader: 'default', @@ -253,26 +254,53 @@ function assignDefaults(userConfig: { [key: string]: any }) { ) } } - if (images.sizes) { - if (!Array.isArray(images.sizes)) { + if (images.deviceSizes) { + const { deviceSizes } = images + if (!Array.isArray(deviceSizes)) { throw new Error( - `Specified images.sizes should be an Array received ${typeof images.sizes}` + `Specified images.deviceSizes should be an Array received ${typeof deviceSizes}` ) } - if (images.sizes.length > 50) { + if (deviceSizes.length > 25) { throw new Error( - `Specified images.sizes exceeds length of 50, received length (${images.sizes.length}), please reduce the length of the array to continue` + `Specified images.deviceSizes exceeds length of 25, received length (${deviceSizes.length}), please reduce the length of the array to continue` ) } - const invalid = images.sizes.filter((d: unknown) => { + const invalid = deviceSizes.filter((d: unknown) => { return typeof d !== 'number' || d < 1 || d > 10000 }) if (invalid.length > 0) { throw new Error( - `Specified images.sizes should be an Array of numbers that are between 1 and 10000, received invalid values (${invalid.join( + `Specified images.deviceSizes should be an Array of numbers that are between 1 and 10000, received invalid values (${invalid.join( + ', ' + )})` + ) + } + } + if (images.iconSizes) { + const { iconSizes } = images + if (!Array.isArray(iconSizes)) { + throw new Error( + `Specified images.iconSizes should be an Array received ${typeof iconSizes}` + ) + } + + if (iconSizes.length > 25) { + throw new Error( + `Specified images.iconSizes exceeds length of 25, received length (${iconSizes.length}), please reduce the length of the array to continue` + ) + } + + const invalid = iconSizes.filter((d: unknown) => { + return typeof d !== 'number' || d < 1 || d > 10000 + }) + + if (invalid.length > 0) { + throw new Error( + `Specified images.iconSizes should be an Array of numbers that are between 1 and 10000, received invalid values (${invalid.join( ', ' )})` ) diff --git a/packages/next/next-server/server/image-optimizer.ts b/packages/next/next-server/server/image-optimizer.ts index c0c2e023d7b01..d1825f7b2f5e6 100644 --- a/packages/next/next-server/server/image-optimizer.ts +++ b/packages/next/next-server/server/image-optimizer.ts @@ -23,6 +23,14 @@ const CACHE_VERSION = 1 const ANIMATABLE_TYPES = [WEBP, PNG, GIF] const VECTOR_TYPES = [SVG] +type ImageData = { + deviceSizes: number[] + iconSizes: number[] + loader: string + path: string + domains?: string[] +} + export async function imageOptimizer( server: Server, req: IncomingMessage, @@ -30,7 +38,9 @@ export async function imageOptimizer( parsedUrl: UrlWithParsedQuery ) { const { nextConfig, distDir } = server - const { sizes = [], domains = [], loader } = nextConfig?.images || {} + const imageData: ImageData = nextConfig.images + const { deviceSizes = [], iconSizes = [], domains = [], loader } = imageData + const sizes = [...deviceSizes, ...iconSizes] if (loader !== 'default') { await server.render404(req, res, parsedUrl) diff --git a/test/integration/image-component/basic/next.config.js b/test/integration/image-component/basic/next.config.js index 6adf797d1c2b1..617de0a68d69b 100644 --- a/test/integration/image-component/basic/next.config.js +++ b/test/integration/image-component/basic/next.config.js @@ -1,6 +1,7 @@ module.exports = { images: { - sizes: [480, 1024, 1600, 2000], + deviceSizes: [480, 1024, 1600, 2000], + iconSizes: [16, 64], path: 'https://example.com/myaccount/', loader: 'imgix', }, diff --git a/test/integration/image-component/basic/pages/client-side.js b/test/integration/image-component/basic/pages/client-side.js index 99f97951f2bdb..ddd00fd2fe3e9 100644 --- a/test/integration/image-component/basic/pages/client-side.js +++ b/test/integration/image-component/basic/pages/client-side.js @@ -53,6 +53,20 @@ const ClientSide = () => { width={300} height={400} /> + + Errors diff --git a/test/integration/image-component/basic/pages/index.js b/test/integration/image-component/basic/pages/index.js index a930e0ef1ef80..c08992abfb476 100644 --- a/test/integration/image-component/basic/pages/index.js +++ b/test/integration/image-component/basic/pages/index.js @@ -70,6 +70,20 @@ const Page = () => { width={300} height={400} /> + + Client Side diff --git a/test/integration/image-component/basic/test/index.test.js b/test/integration/image-component/basic/test/index.test.js index 9e9198cdac42b..ff369bda69b8d 100644 --- a/test/integration/image-component/basic/test/index.test.js +++ b/test/integration/image-component/basic/test/index.test.js @@ -51,6 +51,20 @@ function runTests() { await browser.elementById('preceding-slash-image').getAttribute('srcset') ).toBe('https://example.com/myaccount/fooslash.jpg?auto=format&w=480 480w') }) + it('should use iconSizes when width matches, not deviceSizes from next.config.js', async () => { + expect(await browser.elementById('icon-image-16').getAttribute('src')).toBe( + 'https://example.com/myaccount/icon.png?auto=format&w=16' + ) + expect( + await browser.elementById('icon-image-16').getAttribute('srcset') + ).toBe('https://example.com/myaccount/icon.png?auto=format&w=16 16w') + expect(await browser.elementById('icon-image-64').getAttribute('src')).toBe( + 'https://example.com/myaccount/icon.png?auto=format&w=64' + ) + expect( + await browser.elementById('icon-image-64').getAttribute('srcset') + ).toBe('https://example.com/myaccount/icon.png?auto=format&w=64 64w') + }) it('should support the unoptimized attribute', async () => { expect( await browser.elementById('unoptimized-image').getAttribute('src') diff --git a/test/integration/image-optimizer/test/index.test.js b/test/integration/image-optimizer/test/index.test.js index 8f457d326a6b1..3428ffd359b08 100644 --- a/test/integration/image-optimizer/test/index.test.js +++ b/test/integration/image-optimizer/test/index.test.js @@ -348,12 +348,12 @@ describe('Image Optimizer', () => { ) }) - it('should error when sizes length exceeds 50', async () => { + it('should error when sizes length exceeds 25', async () => { await nextConfig.replace( '{ /* replaceme */ }', JSON.stringify({ images: { - sizes: new Array(51).fill(1024), + deviceSizes: new Array(51).fill(1024), }, }) ) @@ -369,16 +369,16 @@ describe('Image Optimizer', () => { await nextConfig.restore() expect(stderr).toContain( - 'Specified images.sizes exceeds length of 50, received length (51), please reduce the length of the array to continue' + 'Specified images.deviceSizes exceeds length of 25, received length (51), please reduce the length of the array to continue' ) }) - it('should error when sizes contains invalid sizes', async () => { + it('should error when deviceSizes contains invalid widths', async () => { await nextConfig.replace( '{ /* replaceme */ }', JSON.stringify({ images: { - sizes: [0, 12000, 64, 128, 256], + deviceSizes: [0, 12000, 64, 128, 256], }, }) ) @@ -394,7 +394,32 @@ describe('Image Optimizer', () => { await nextConfig.restore() expect(stderr).toContain( - 'Specified images.sizes should be an Array of numbers that are between 1 and 10000, received invalid values (0, 12000)' + 'Specified images.deviceSizes should be an Array of numbers that are between 1 and 10000, received invalid values (0, 12000)' + ) + }) + + it('should error when iconSizes contains invalid widths', async () => { + await nextConfig.replace( + '{ /* replaceme */ }', + JSON.stringify({ + images: { + iconSizes: [0, 16, 64, 12000], + }, + }) + ) + let stderr = '' + + app = await launchApp(appDir, await findPort(), { + onStderr(msg) { + stderr += msg || '' + }, + }) + await waitFor(1000) + await killApp(app).catch(() => {}) + await nextConfig.restore() + + expect(stderr).toContain( + 'Specified images.iconSizes should be an Array of numbers that are between 1 and 10000, received invalid values (0, 12000)' ) }) }) @@ -421,7 +446,8 @@ describe('Image Optimizer', () => { beforeAll(async () => { const json = JSON.stringify({ images: { - sizes: [size, largeSize], + deviceSizes: [largeSize], + iconSizes: [size], domains, }, }) @@ -458,7 +484,7 @@ describe('Image Optimizer', () => { beforeAll(async () => { const json = JSON.stringify({ images: { - sizes: [size, largeSize], + deviceSizes: [size, largeSize], domains, }, }) @@ -482,7 +508,7 @@ describe('Image Optimizer', () => { const json = JSON.stringify({ target: 'experimental-serverless-trace', images: { - sizes: [size, largeSize], + deviceSizes: [size, largeSize], domains, }, }) From 9d838df7660cc6430c5512aba3dab5ed99f74122 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 26 Oct 2020 16:08:48 -0400 Subject: [PATCH 06/12] v9.5.6-canary.15 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- 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-google-analytics/package.json | 2 +- packages/next-plugin-sentry/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 +- 15 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lerna.json b/lerna.json index 239d9523b3500..dc2485c55dc65 100644 --- a/lerna.json +++ b/lerna.json @@ -17,5 +17,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "9.5.6-canary.14" + "version": "9.5.6-canary.15" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index ce97bcb440bef..d4c2677ee05fb 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "9.5.6-canary.14", + "version": "9.5.6-canary.15", "keywords": [ "react", "next", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 9e951d613ccfc..912ee37200874 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": "9.5.6-canary.14", + "version": "9.5.6-canary.15", "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 930fb377da430..9efe6e3c261c8 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "9.5.6-canary.14", + "version": "9.5.6-canary.15", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 748a864e4e1bf..f6b4292b76460 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "9.5.6-canary.14", + "version": "9.5.6-canary.15", "license": "MIT", "dependencies": { "chalk": "4.1.0", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index ef59ce9697b5a..89e1b0025ebd1 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "9.5.6-canary.14", + "version": "9.5.6-canary.15", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index cd96f126bb931..732f015454de9 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "9.5.6-canary.14", + "version": "9.5.6-canary.15", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-google-analytics/package.json b/packages/next-plugin-google-analytics/package.json index 1abe107a3f37e..fee8ae0efe0cb 100644 --- a/packages/next-plugin-google-analytics/package.json +++ b/packages/next-plugin-google-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-google-analytics", - "version": "9.5.6-canary.14", + "version": "9.5.6-canary.15", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-google-analytics" diff --git a/packages/next-plugin-sentry/package.json b/packages/next-plugin-sentry/package.json index 63a920806b0e9..6b6f9d024ffc8 100644 --- a/packages/next-plugin-sentry/package.json +++ b/packages/next-plugin-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-sentry", - "version": "9.5.6-canary.14", + "version": "9.5.6-canary.15", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-sentry" diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 13a043a38c698..a5b5e5772eebd 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "9.5.6-canary.14", + "version": "9.5.6-canary.15", "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 05701a6245880..90fd0ea9127e9 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "9.5.6-canary.14", + "version": "9.5.6-canary.15", "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 4e96606d16ea1..73a1e798c156b 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "9.5.6-canary.14", + "version": "9.5.6-canary.15", "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 95734c554c7d4..1b3f5079cbf47 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "9.5.6-canary.14", + "version": "9.5.6-canary.15", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -79,10 +79,10 @@ "@babel/runtime": "7.11.2", "@babel/types": "7.11.5", "@hapi/accept": "5.0.1", - "@next/env": "9.5.6-canary.14", - "@next/polyfill-module": "9.5.6-canary.14", - "@next/react-dev-overlay": "9.5.6-canary.14", - "@next/react-refresh-utils": "9.5.6-canary.14", + "@next/env": "9.5.6-canary.15", + "@next/polyfill-module": "9.5.6-canary.15", + "@next/react-dev-overlay": "9.5.6-canary.15", + "@next/react-refresh-utils": "9.5.6-canary.15", "ast-types": "0.13.2", "babel-plugin-transform-define": "2.0.0", "babel-plugin-transform-react-remove-prop-types": "0.4.24", @@ -129,7 +129,7 @@ "sharp": "0.26.2" }, "devDependencies": { - "@next/polyfill-nomodule": "9.5.6-canary.14", + "@next/polyfill-nomodule": "9.5.6-canary.15", "@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 a4cbec7fb1b88..314af1a3d2161 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": "9.5.6-canary.14", + "version": "9.5.6-canary.15", "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 937856d2c70c8..6a2379a239c39 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": "9.5.6-canary.14", + "version": "9.5.6-canary.15", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", From 31a011d6a56d1939888f8ce2129141882b345623 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Mon, 26 Oct 2020 23:36:31 -0400 Subject: [PATCH 07/12] Adjust configuration option placement (#18279) --- packages/next/build/index.ts | 2 +- packages/next/build/webpack-config.ts | 4 +--- packages/next/next-server/server/config.ts | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 67cdd21d3b353..9d1f8f414956a 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -1163,7 +1163,7 @@ export default async function build( printCustomRoutes({ redirects, rewrites, headers }) } - if (config.experimental.analyticsId) { + if (config.analyticsId) { console.log( chalk.bold.green('Next.js Analytics') + ' is enabled for this production build. ' + diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 3559fb5f955c5..f0d7eed6d91ef 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1010,9 +1010,7 @@ export default async function getBaseWebpackConfig( 'process.env.__NEXT_I18N_DOMAINS': JSON.stringify( config.experimental.i18n.domains ), - 'process.env.__NEXT_ANALYTICS_ID': JSON.stringify( - config.experimental.analyticsId - ), + 'process.env.__NEXT_ANALYTICS_ID': JSON.stringify(config.analyticsId), ...(isServer ? { // Fix bad-actors in the npm ecosystem (e.g. `node-formidable`) diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts index 1940286b801c1..500d63022857e 100644 --- a/packages/next/next-server/server/config.ts +++ b/packages/next/next-server/server/config.ts @@ -23,6 +23,7 @@ const defaultConfig: { [key: string]: any } = { target: 'server', poweredByHeader: true, compress: true, + analyticsId: process.env.VERCEL_ANALYTICS_ID || '', images: { deviceSizes: [320, 420, 768, 1024, 1200], iconSizes: [], @@ -62,7 +63,6 @@ const defaultConfig: { [key: string]: any } = { optimizeImages: false, scrollRestoration: false, i18n: false, - analyticsId: process.env.VERCEL_ANALYTICS_ID || '', }, future: { excludeDefaultMomentLocales: false, From 5a8398e34d620fe50dc36f414813aa36ba0d8ca9 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Mon, 26 Oct 2020 23:01:37 -0500 Subject: [PATCH 08/12] Remove unstable_ prefix from unstable_blocking (#18276) This removes the `unstable_` prefix to prepare for stabilizing the feature --- packages/next/build/index.ts | 2 +- packages/next/build/utils.ts | 6 +++--- packages/next/server/next-dev-server.ts | 2 +- packages/next/types/index.d.ts | 2 +- .../prerender/pages/blocking-fallback-once/[slug].js | 2 +- .../prerender/pages/blocking-fallback-some/[slug].js | 2 +- .../integration/prerender/pages/blocking-fallback/[slug].js | 2 +- test/integration/prerender/pages/non-json-blocking/[p].js | 2 +- test/integration/prerender/test/index.test.js | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 9d1f8f414956a..eb9179d6e92f1 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -594,7 +594,7 @@ export default async function build( ssgPageRoutes = workerResult.prerenderRoutes } - if (workerResult.prerenderFallback === 'unstable_blocking') { + if (workerResult.prerenderFallback === 'blocking') { ssgBlockingFallbackPages.add(page) } else if (workerResult.prerenderFallback === true) { ssgStaticFallbackPages.add(page) diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 9216875d7e980..b4b313418f66d 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -575,7 +575,7 @@ export async function buildStaticPaths( if ( !( typeof staticPathsResult.fallback === 'boolean' || - staticPathsResult.fallback === 'unstable_blocking' + staticPathsResult.fallback === 'blocking' ) ) { throw new Error( @@ -700,7 +700,7 @@ export async function isPageStatic( hasServerProps?: boolean hasStaticProps?: boolean prerenderRoutes?: string[] | undefined - prerenderFallback?: boolean | 'unstable_blocking' | undefined + prerenderFallback?: boolean | 'blocking' | undefined }> { try { require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig) @@ -775,7 +775,7 @@ export async function isPageStatic( } let prerenderRoutes: Array | undefined - let prerenderFallback: boolean | 'unstable_blocking' | undefined + let prerenderFallback: boolean | 'blocking' | undefined if (hasStaticProps && hasStaticPaths) { ;({ paths: prerenderRoutes, diff --git a/packages/next/server/next-dev-server.ts b/packages/next/server/next-dev-server.ts index d2b70721c3c0f..17944484b967f 100644 --- a/packages/next/server/next-dev-server.ts +++ b/packages/next/server/next-dev-server.ts @@ -556,7 +556,7 @@ export default class DevServer extends Server { return { staticPaths, fallbackMode: - fallback === 'unstable_blocking' + fallback === 'blocking' ? 'blocking' : fallback === true ? 'static' diff --git a/packages/next/types/index.d.ts b/packages/next/types/index.d.ts index 1f10532657a97..24e8619f1ede4 100644 --- a/packages/next/types/index.d.ts +++ b/packages/next/types/index.d.ts @@ -111,7 +111,7 @@ export type GetStaticPathsContext = { export type GetStaticPathsResult

= { paths: Array - fallback: boolean | 'unstable_blocking' + fallback: boolean | 'blocking' } export type GetStaticPaths

= ( diff --git a/test/integration/prerender/pages/blocking-fallback-once/[slug].js b/test/integration/prerender/pages/blocking-fallback-once/[slug].js index 0d9b4971ba00e..5b047ded890c0 100644 --- a/test/integration/prerender/pages/blocking-fallback-once/[slug].js +++ b/test/integration/prerender/pages/blocking-fallback-once/[slug].js @@ -5,7 +5,7 @@ import { useRouter } from 'next/router' export async function getStaticPaths() { return { paths: [], - fallback: 'unstable_blocking', + fallback: 'blocking', } } diff --git a/test/integration/prerender/pages/blocking-fallback-some/[slug].js b/test/integration/prerender/pages/blocking-fallback-some/[slug].js index a9ccb79b9594f..dc89af7f5117e 100644 --- a/test/integration/prerender/pages/blocking-fallback-some/[slug].js +++ b/test/integration/prerender/pages/blocking-fallback-some/[slug].js @@ -5,7 +5,7 @@ import { useRouter } from 'next/router' export async function getStaticPaths() { return { paths: [{ params: { slug: 'a' } }, { params: { slug: 'b' } }], - fallback: 'unstable_blocking', + fallback: 'blocking', } } diff --git a/test/integration/prerender/pages/blocking-fallback/[slug].js b/test/integration/prerender/pages/blocking-fallback/[slug].js index cb2a93e47f07c..0b39f579352ca 100644 --- a/test/integration/prerender/pages/blocking-fallback/[slug].js +++ b/test/integration/prerender/pages/blocking-fallback/[slug].js @@ -5,7 +5,7 @@ import { useRouter } from 'next/router' export async function getStaticPaths() { return { paths: [], - fallback: 'unstable_blocking', + fallback: 'blocking', } } diff --git a/test/integration/prerender/pages/non-json-blocking/[p].js b/test/integration/prerender/pages/non-json-blocking/[p].js index 9d872c8d3fec8..f7b406bdcb9b9 100644 --- a/test/integration/prerender/pages/non-json-blocking/[p].js +++ b/test/integration/prerender/pages/non-json-blocking/[p].js @@ -7,7 +7,7 @@ export async function getStaticProps() { } export async function getStaticPaths() { - return { paths: [], fallback: 'unstable_blocking' } + return { paths: [], fallback: 'blocking' } } const Page = ({ time }) => { diff --git a/test/integration/prerender/test/index.test.js b/test/integration/prerender/test/index.test.js index cab5b95e8f97c..c7c8e3b334a05 100644 --- a/test/integration/prerender/test/index.test.js +++ b/test/integration/prerender/test/index.test.js @@ -2101,7 +2101,7 @@ describe('SSG Prerender', () => { await fs.writeFile( pagePath, fallbackBlockingPageContents[page].replace( - "fallback: 'unstable_blocking'", + "fallback: 'blocking'", 'fallback: false' ) ) From d2e7f3c76992dc0392acc3ac9e2a023143891cc2 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 27 Oct 2020 00:06:15 -0400 Subject: [PATCH 09/12] v9.5.6-canary.16 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- 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-google-analytics/package.json | 2 +- packages/next-plugin-sentry/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 +- 15 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lerna.json b/lerna.json index dc2485c55dc65..a0c6944976a23 100644 --- a/lerna.json +++ b/lerna.json @@ -17,5 +17,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "9.5.6-canary.15" + "version": "9.5.6-canary.16" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index d4c2677ee05fb..7757d36353d6a 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "9.5.6-canary.15", + "version": "9.5.6-canary.16", "keywords": [ "react", "next", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 912ee37200874..eb85b1a79c23b 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": "9.5.6-canary.15", + "version": "9.5.6-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 9efe6e3c261c8..684268d1b8066 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "9.5.6-canary.15", + "version": "9.5.6-canary.16", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index f6b4292b76460..95f3075888b77 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "9.5.6-canary.15", + "version": "9.5.6-canary.16", "license": "MIT", "dependencies": { "chalk": "4.1.0", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 89e1b0025ebd1..63cc5d7b874ee 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "9.5.6-canary.15", + "version": "9.5.6-canary.16", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 732f015454de9..cae2904e09559 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "9.5.6-canary.15", + "version": "9.5.6-canary.16", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-google-analytics/package.json b/packages/next-plugin-google-analytics/package.json index fee8ae0efe0cb..288f382b1d348 100644 --- a/packages/next-plugin-google-analytics/package.json +++ b/packages/next-plugin-google-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-google-analytics", - "version": "9.5.6-canary.15", + "version": "9.5.6-canary.16", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-google-analytics" diff --git a/packages/next-plugin-sentry/package.json b/packages/next-plugin-sentry/package.json index 6b6f9d024ffc8..77fd331187036 100644 --- a/packages/next-plugin-sentry/package.json +++ b/packages/next-plugin-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-sentry", - "version": "9.5.6-canary.15", + "version": "9.5.6-canary.16", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-sentry" diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index a5b5e5772eebd..3ec335749bc7c 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "9.5.6-canary.15", + "version": "9.5.6-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 90fd0ea9127e9..ebe89716be948 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "9.5.6-canary.15", + "version": "9.5.6-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 73a1e798c156b..4ab65f6ceec04 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "9.5.6-canary.15", + "version": "9.5.6-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 1b3f5079cbf47..88f4ab1a906b7 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "9.5.6-canary.15", + "version": "9.5.6-canary.16", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -79,10 +79,10 @@ "@babel/runtime": "7.11.2", "@babel/types": "7.11.5", "@hapi/accept": "5.0.1", - "@next/env": "9.5.6-canary.15", - "@next/polyfill-module": "9.5.6-canary.15", - "@next/react-dev-overlay": "9.5.6-canary.15", - "@next/react-refresh-utils": "9.5.6-canary.15", + "@next/env": "9.5.6-canary.16", + "@next/polyfill-module": "9.5.6-canary.16", + "@next/react-dev-overlay": "9.5.6-canary.16", + "@next/react-refresh-utils": "9.5.6-canary.16", "ast-types": "0.13.2", "babel-plugin-transform-define": "2.0.0", "babel-plugin-transform-react-remove-prop-types": "0.4.24", @@ -129,7 +129,7 @@ "sharp": "0.26.2" }, "devDependencies": { - "@next/polyfill-nomodule": "9.5.6-canary.15", + "@next/polyfill-nomodule": "9.5.6-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 314af1a3d2161..ffe0c500fdd57 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": "9.5.6-canary.15", + "version": "9.5.6-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 6a2379a239c39..be6e5b0383471 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": "9.5.6-canary.15", + "version": "9.5.6-canary.16", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", From 3f22490a59bf663e5d20d1c486c8111d64a278f9 Mon Sep 17 00:00:00 2001 From: Tasuku Uno Date: Tue, 27 Oct 2020 14:14:23 +0900 Subject: [PATCH 10/12] Fix browser back issue of redirects from getServerSideProps / getStaticProps (#17741) * Fix test without expect for redirects from getStaticProps/getServerSideProps * Fix browser back issue of redirects from getServerSideProps / getStaticProps --- .../next/next-server/lib/router/router.ts | 7 +- .../gssp-redirect/test/index.test.js | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 5eeb52da7e5fe..d367a16db2eb3 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -813,12 +813,7 @@ export default class Router implements BaseRouter { this._resolveHref(parsedHref, pages) if (pages.includes(parsedHref.pathname)) { - return this.change( - 'replaceState', - destination, - destination, - options - ) + return this.change(method, destination, destination, options) } } diff --git a/test/integration/gssp-redirect/test/index.test.js b/test/integration/gssp-redirect/test/index.test.js index bdbb842bd51c6..5121bdc0f85d6 100644 --- a/test/integration/gssp-redirect/test/index.test.js +++ b/test/integration/gssp-redirect/test/index.test.js @@ -123,6 +123,10 @@ const runTests = () => { window.next.router.push('/gssp-blog/redirect-dest-_another') })()`) await browser.waitForElementByCss('#another') + + const text = await browser.elementByCss('#another').text() + + expect(text).toEqual('another Page') }) it('should apply redirect when GSSP page is navigated to client-side (external)', async () => { @@ -149,6 +153,10 @@ const runTests = () => { window.next.router.push('/gsp-blog/redirect-dest-_another') })()`) await browser.waitForElementByCss('#another') + + const text = await browser.elementByCss('#another').text() + + expect(text).toEqual('another Page') }) it('should apply redirect when GSP page is navigated to client-side (external)', async () => { @@ -167,6 +175,94 @@ const runTests = () => { }, }) }) + + it('should not replace history of the origin page when GSSP page is navigated to client-side (internal normal)', async () => { + const browser = await webdriver(appPort, '/another?mark_as=root') + + await browser.eval(`(function () { + window.location.href = '/' + })()`) + await browser.waitForElementByCss('#index') + + await browser.eval(`(function () { + window.next.router.push('/gssp-blog/redirect-dest-_another') + })()`) + await browser.waitForElementByCss('#another') + + await browser.eval(`(function () { + window.history.back() + })()`) + + const curUrl = await browser.url() + const { path } = url.parse(curUrl) + expect(path).toEqual('/') + }) + + it('should not replace history of the origin page when GSSP page is navigated to client-side (external)', async () => { + const browser = await webdriver(appPort, '/another?mark_as=root') + + await browser.eval(`(function () { + window.location.href = '/' + })()`) + await browser.waitForElementByCss('#index') + + await browser.eval(`(function () { + window.next.router.push('/gssp-blog/redirect-dest-_gssp-blog_first') + })()`) + await browser.waitForElementByCss('#gssp') + + await browser.eval(`(function () { + window.history.back() + })()`) + + const curUrl = await browser.url() + const { path } = url.parse(curUrl) + expect(path).toEqual('/') + }) + + it('should not replace history of the origin page when GSP page is navigated to client-side (internal)', async () => { + const browser = await webdriver(appPort, '/another?mark_as=root') + + await browser.eval(`(function () { + window.location.href = '/' + })()`) + await browser.waitForElementByCss('#index') + + await browser.eval(`(function () { + window.next.router.push('/gsp-blog/redirect-dest-_another') + })()`) + await browser.waitForElementByCss('#another') + + await browser.eval(`(function () { + window.history.back() + })()`) + + const curUrl = await browser.url() + const { path } = url.parse(curUrl) + expect(path).toEqual('/') + }) + + it('should not replace history of the origin page when GSP page is navigated to client-side (external)', async () => { + const browser = await webdriver(appPort, '/another?mark_as=root') + + await browser.eval(`(function () { + window.location.href = '/' + })()`) + await browser.waitForElementByCss('#index') + + await browser.eval(`(function () { + window.next.router.push('/gsp-blog/redirect-dest-_gsp-blog_first') + })()`) + await browser.waitForElementByCss('#gsp') + + await browser.eval(`(function () { + window.history.back() + })()`) + + const curUrl = await browser.url() + const { path } = url.parse(curUrl) + expect(path).toEqual('/') + }) } describe('GS(S)P Redirect Support', () => { From 4782bda3e35f39c0a6fcee277b8b5c3eb89b62a2 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 27 Oct 2020 00:42:12 -0500 Subject: [PATCH 11/12] Add support for notFound in getServerSideProps (#18241) This is a follow-up to https://github.com/vercel/next.js/pull/17755 which adds support for returning `notFound` to `getServerSideProps` also --- .../webpack/loaders/next-serverless-loader.ts | 2 +- packages/next/export/worker.ts | 4 +- .../next/next-server/server/next-server.ts | 4 +- packages/next/next-server/server/render.tsx | 30 +++++++-- packages/next/types/index.d.ts | 14 +++-- .../pages/not-found/[slug].js | 34 ++++++++++ .../pages/not-found/index.js | 34 ++++++++++ .../getserversideprops/test/index.test.js | 62 +++++++++++++++++++ 8 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 test/integration/getserversideprops/pages/not-found/[slug].js create mode 100644 test/integration/getserversideprops/pages/not-found/index.js diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index 669594fdb2b3b..d81de6a9b5deb 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -772,7 +772,7 @@ const nextServerlessLoader: loader.Loader = function () { if (!renderMode) { if (_nextData || getStaticProps || getServerSideProps) { - if (renderOpts.ssgNotFound) { + if (renderOpts.isNotFound) { res.statusCode = 404 const NotFoundComponent = ${ diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index f13b2c817187f..0645fc552972f 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -263,7 +263,7 @@ export default async function exportPage({ html = (result as any).html } - if (!html && !(curRenderOpts as any).ssgNotFound) { + if (!html && !(curRenderOpts as any).isNotFound) { throw new Error(`Failed to render serverless page`) } } else { @@ -318,7 +318,7 @@ export default async function exportPage({ html = await renderMethod(req, res, page, query, curRenderOpts) } } - results.ssgNotFound = (curRenderOpts as any).ssgNotFound + results.ssgNotFound = (curRenderOpts as any).isNotFound const validateAmp = async ( rawAmpHtml: string, diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index e478f1da66bc0..1ecf546d58176 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -1351,7 +1351,7 @@ export default class Server { html = renderResult.html pageData = renderResult.renderOpts.pageData sprRevalidate = renderResult.renderOpts.revalidate - isNotFound = renderResult.renderOpts.ssgNotFound + isNotFound = renderResult.renderOpts.isNotFound } else { const origQuery = parseUrl(req.url || '', true).query const resolvedUrl = formatUrl({ @@ -1393,7 +1393,7 @@ export default class Server { // TODO: change this to a different passing mechanism pageData = (renderOpts as any).pageData sprRevalidate = (renderOpts as any).revalidate - isNotFound = (renderOpts as any).ssgNotFound + isNotFound = (renderOpts as any).isNotFound } return { html, pageData, sprRevalidate, isNotFound } diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index 1cd05e6b21823..7aa8cf64faafd 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -643,7 +643,7 @@ export async function renderToHTML( ) } - ;(renderOpts as any).ssgNotFound = true + ;(renderOpts as any).isNotFound = true ;(renderOpts as any).revalidate = false return null } @@ -753,21 +753,35 @@ export async function renderToHTML( } const invalidKeys = Object.keys(data).filter( - (key) => key !== 'props' && key !== 'unstable_redirect' + (key) => + key !== 'props' && + key !== 'unstable_redirect' && + key !== 'unstable_notFound' ) if (invalidKeys.length) { throw new Error(invalidKeysMsg('getServerSideProps', invalidKeys)) } + if ('unstable_notFound' in data) { + if (pathname === '/404') { + throw new Error( + `The /404 page can not return unstable_notFound in "getStaticProps", please remove it to continue!` + ) + } + + ;(renderOpts as any).isNotFound = true + return null + } + if ( - data.unstable_redirect && + 'unstable_redirect' in data && typeof data.unstable_redirect === 'object' ) { checkRedirectValues(data.unstable_redirect, req) if (isDataReq) { - data.props = { + ;(data as any).props = { __N_REDIRECT: data.unstable_redirect.destination, } } else { @@ -778,7 +792,11 @@ export async function renderToHTML( if ( (dev || isBuildTimeSSG) && - !isSerializableProps(pathname, 'getServerSideProps', data.props) + !isSerializableProps( + pathname, + 'getServerSideProps', + (data as any).props + ) ) { // this fn should throw an error instead of ever returning `false` throw new Error( @@ -786,7 +804,7 @@ export async function renderToHTML( ) } - props.pageProps = Object.assign({}, props.pageProps, data.props) + props.pageProps = Object.assign({}, props.pageProps, (data as any).props) ;(renderOpts as any).pageData = props } } catch (dataFetchError) { diff --git a/packages/next/types/index.d.ts b/packages/next/types/index.d.ts index 24e8619f1ede4..f834d3114fa0e 100644 --- a/packages/next/types/index.d.ts +++ b/packages/next/types/index.d.ts @@ -132,10 +132,16 @@ export type GetServerSidePropsContext< locales?: string[] } -export type GetServerSidePropsResult

= { - props?: P - unstable_redirect?: Redirect -} +export type GetServerSidePropsResult

= + | { + props: P + } + | { + unstable_redirect: Redirect + } + | { + unstable_notFound: true + } export type GetServerSideProps< P extends { [key: string]: any } = { [key: string]: any }, diff --git a/test/integration/getserversideprops/pages/not-found/[slug].js b/test/integration/getserversideprops/pages/not-found/[slug].js new file mode 100644 index 0000000000000..158be98be1cef --- /dev/null +++ b/test/integration/getserversideprops/pages/not-found/[slug].js @@ -0,0 +1,34 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' + +export default function Page(props) { + const router = useRouter() + + return ( + <> +

gssp page

+

{JSON.stringify(props)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ) +} + +export const getServerSideProps = ({ query }) => { + if (query.hiding) { + return { + unstable_notFound: true, + } + } + + return { + props: { + hello: 'world', + }, + } +} diff --git a/test/integration/getserversideprops/pages/not-found/index.js b/test/integration/getserversideprops/pages/not-found/index.js new file mode 100644 index 0000000000000..158be98be1cef --- /dev/null +++ b/test/integration/getserversideprops/pages/not-found/index.js @@ -0,0 +1,34 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' + +export default function Page(props) { + const router = useRouter() + + return ( + <> +

gssp page

+

{JSON.stringify(props)}

+

{JSON.stringify(router.query)}

+

{router.pathname}

+

{router.asPath}

+ + to / + +
+ + ) +} + +export const getServerSideProps = ({ query }) => { + if (query.hiding) { + return { + unstable_notFound: true, + } + } + + return { + props: { + hello: 'world', + }, + } +} diff --git a/test/integration/getserversideprops/test/index.test.js b/test/integration/getserversideprops/test/index.test.js index 015ec3d7b10b0..b3e910d30a9fc 100644 --- a/test/integration/getserversideprops/test/index.test.js +++ b/test/integration/getserversideprops/test/index.test.js @@ -118,6 +118,24 @@ const expectedManifestRoutes = () => [ ), page: '/non-json', }, + { + dataRouteRegex: `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/not-found.json$`, + page: '/not-found', + }, + { + dataRouteRegex: `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/not\\-found\\/([^\\/]+?)\\.json$`, + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/not\\-found/(?[^/]+?)\\.json$`, + page: '/not-found/[slug]', + routeKeys: { + slug: 'slug', + }, + }, { dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex(buildId)}\\/refresh.json$` @@ -235,6 +253,50 @@ const navigateTest = (dev = false) => { const runTests = (dev = false) => { navigateTest(dev) + it('should render 404 correctly when notFound is returned (non-dynamic)', async () => { + const res = await fetchViaHTTP(appPort, '/not-found', { hiding: true }) + + expect(res.status).toBe(404) + expect(await res.text()).toContain('This page could not be found') + }) + + it('should render 404 correctly when notFound is returned client-transition (non-dynamic)', async () => { + const browser = await webdriver(appPort, '/') + await browser.eval(`(function() { + window.beforeNav = 1 + window.next.router.push('/not-found?hiding=true') + })()`) + + await browser.waitForElementByCss('h1') + expect(await browser.elementByCss('html').text()).toContain( + 'This page could not be found' + ) + expect(await browser.eval('window.beforeNav')).toBe(null) + }) + + it('should render 404 correctly when notFound is returned (dynamic)', async () => { + const res = await fetchViaHTTP(appPort, '/not-found/first', { + hiding: true, + }) + + expect(res.status).toBe(404) + expect(await res.text()).toContain('This page could not be found') + }) + + it('should render 404 correctly when notFound is returned client-transition (dynamic)', async () => { + const browser = await webdriver(appPort, '/') + await browser.eval(`(function() { + window.beforeNav = 1 + window.next.router.push('/not-found/first?hiding=true') + })()`) + + await browser.waitForElementByCss('h1') + expect(await browser.elementByCss('html').text()).toContain( + 'This page could not be found' + ) + expect(await browser.eval('window.beforeNav')).toBe(null) + }) + it('should SSR normal page correctly', async () => { const html = await renderViaHTTP(appPort, '/') expect(html).toMatch(/hello.*?world/) From 2972c0685b472a03542d2b6a2d5b6fdceda95925 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 27 Oct 2020 02:37:44 -0400 Subject: [PATCH 12/12] Improve type for GSP return type (#18285) --- packages/next/next-server/server/render.tsx | 22 +++++++++++++-------- packages/next/types/index.d.ts | 22 +++++++-------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index 7aa8cf64faafd..41cb03f46936e 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -636,7 +636,7 @@ export async function renderToHTML( throw new Error(invalidKeysMsg('getStaticProps', invalidKeys)) } - if (data.unstable_notFound) { + if ('unstable_notFound' in data && data.unstable_notFound) { if (pathname === '/404') { throw new Error( `The /404 page can not return unstable_notFound in "getStaticProps", please remove it to continue!` @@ -649,6 +649,7 @@ export async function renderToHTML( } if ( + 'unstable_redirect' in data && data.unstable_redirect && typeof data.unstable_redirect === 'object' ) { @@ -662,7 +663,7 @@ export async function renderToHTML( } if (isDataReq) { - data.props = { + ;(data as any).props = { __N_REDIRECT: data.unstable_redirect.destination, } } else { @@ -673,7 +674,7 @@ export async function renderToHTML( if ( (dev || isBuildTimeSSG) && - !isSerializableProps(pathname, 'getStaticProps', data.props) + !isSerializableProps(pathname, 'getStaticProps', (data as any).props) ) { // this fn should throw an error instead of ever returning `false` throw new Error( @@ -681,7 +682,7 @@ export async function renderToHTML( ) } - if (typeof data.revalidate === 'number') { + if ('revalidate' in data && typeof data.revalidate === 'number') { if (!Number.isInteger(data.revalidate)) { throw new Error( `A page's revalidate option must be seconds expressed as a natural number. Mixed numbers, such as '${data.revalidate}', cannot be used.` + @@ -702,20 +703,25 @@ export async function renderToHTML( `\nTo only run getStaticProps at build-time and not revalidate at runtime, you can set \`revalidate\` to \`false\`!` ) } - } else if (data.revalidate === true) { + } else if ('revalidate' in data && data.revalidate === true) { // When enabled, revalidate after 1 second. This value is optimal for // the most up-to-date page possible, but without a 1-to-1 // request-refresh ratio. data.revalidate = 1 } else { // By default, we never revalidate. - data.revalidate = false + ;(data as any).revalidate = false } - props.pageProps = Object.assign({}, props.pageProps, data.props) + props.pageProps = Object.assign( + {}, + props.pageProps, + 'props' in data ? data.props : undefined + ) // pass up revalidate and props for export // TODO: change this to a different passing mechanism - ;(renderOpts as any).revalidate = data.revalidate + ;(renderOpts as any).revalidate = + 'revalidate' in data ? data.revalidate : undefined ;(renderOpts as any).pageData = props } diff --git a/packages/next/types/index.d.ts b/packages/next/types/index.d.ts index f834d3114fa0e..9992573b63604 100644 --- a/packages/next/types/index.d.ts +++ b/packages/next/types/index.d.ts @@ -85,12 +85,10 @@ export type GetStaticPropsContext = { locales?: string[] } -export type GetStaticPropsResult

= { - props?: P - revalidate?: number | boolean - unstable_redirect?: Redirect - unstable_notFound?: true -} +export type GetStaticPropsResult

= + | { props: P; revalidate?: number | boolean } + | { unstable_redirect: Redirect; revalidate?: number | boolean } + | { unstable_notFound: true } export type GetStaticProps< P extends { [key: string]: any } = { [key: string]: any }, @@ -133,15 +131,9 @@ export type GetServerSidePropsContext< } export type GetServerSidePropsResult

= - | { - props: P - } - | { - unstable_redirect: Redirect - } - | { - unstable_notFound: true - } + | { props: P } + | { unstable_redirect: Redirect } + | { unstable_notFound: true } export type GetServerSideProps< P extends { [key: string]: any } = { [key: string]: any },