diff --git a/docs/api-reference/next/future/image.md b/docs/api-reference/next/future/image.md index ee395f9618a6..ea8323fad240 100644 --- a/docs/api-reference/next/future/image.md +++ b/docs/api-reference/next/future/image.md @@ -1,5 +1,5 @@ --- -description: Try the latest Image Optimization with the experimental `next/future/image` component. +description: Try the latest Image Optimization with the new `next/future/image` component. --- # next/future/image @@ -7,28 +7,17 @@ description: Try the latest Image Optimization with the experimental `next/futur
Version History -| Version | Changes | -| --------- | -------------------------------------------- | -| `v12.2.4` | Support for `fill` property added. | -| `v12.2.0` | Experimental `next/future/image` introduced. | +| Version | Changes | +| --------- | --------------------------------------------------------------------------------------------------------------------------- | +| `v12.3.0` | `next/future/image` component stable. `remotePatterns` config stable. `unoptimized` config stable. `alt` property required. | +| `v12.2.4` | `fill` property added. | +| `v12.2.0` | Experimental `next/future/image` component introduced. |
-The `next/future/image` component is an experiment to improve both the performance and developer experience of `next/image` by using the native `` element with better default behavior. +The `next/future/image` component improves both the performance and developer experience of `next/image` by using the native `` element with better default behavior. -This new component is considered experimental and therefore not covered by semver, and may cause unexpected or broken application behavior. This component uses browser native [lazy loading](https://caniuse.com/loading-lazy-attr), which may fallback to eager loading for older browsers before Safari 15.4. When using the blur-up placeholder, older browsers before Safari 12 will fallback to empty placeholder. When using styles with `width`/`height` of `auto`, it is possible to cause [Layout Shift](https://web.dev/cls/) on older browsers before Safari 15 that don't [preserve the aspect ratio](https://caniuse.com/mdn-html_elements_img_aspect_ratio_computed_from_attributes). For more details, see [this MDN video](https://www.youtube.com/watch?v=4-d_SoCHeWE). - -To use `next/future/image`, add the following to your `next.config.js` file: - -```js -module.exports = { - experimental: { - images: { - allowFutureImage: true, - }, - }, -} -``` +This component uses browser native [lazy loading](https://caniuse.com/loading-lazy-attr), which may fallback to eager loading for older browsers before Safari 15.4. When using the blur-up placeholder, older browsers before Safari 12 will fallback to empty placeholder. When using styles with `width`/`height` of `auto`, it is possible to cause [Layout Shift](https://web.dev/cls/) on older browsers before Safari 15 that don't [preserve the aspect ratio](https://caniuse.com/mdn-html_elements_img_aspect_ratio_computed_from_attributes). For more details, see [this MDN video](https://www.youtube.com/watch?v=4-d_SoCHeWE). ## Comparison @@ -43,6 +32,7 @@ Compared to `next/image`, the new `next/future/image` component has the followin - Removes `lazyBoundary` prop since there is no native equivalent - Removes `lazyRoot` prop since there is no native equivalent - Removes `loader` config in favor of [`loader`](#loader) prop +- Changed `alt` prop from optional to required ## Known Browser Bugs @@ -175,6 +165,16 @@ The `height` property represents the _rendered_ height in pixels, so it will aff Required, except for [statically imported images](/docs/basic-features/image-optimization.md#local-images) or images with the [`fill` property](#fill). +### alt + +The `alt` property is used to describe the image for screen readers and search engines. It is also the fallback text if images have been disabled or an error occurs while loading the image. + +It should contain text that could replace the image [without changing the meaning of the page](https://html.spec.whatwg.org/multipage/images.html#general-guidelines). It is not meant to supplement the image and should not repeat information that is already provided in the captions above or below the image. + +If the image is [purely decorative](https://html.spec.whatwg.org/multipage/images.html#a-purely-decorative-image-that-doesn't-add-any-information) or [not intended for the user](https://html.spec.whatwg.org/multipage/images.html#an-image-not-intended-for-the-user), the `alt` property should be an empty string (`alt=""`). + +[Learn more](https://html.spec.whatwg.org/multipage/images.html#alt) + ## Optional Props The `` component accepts a number of additional properties beyond those which are required. This section describes the most commonly-used properties of the Image component. Find details about more rarely-used properties in the [Advanced Props](#advanced-props) section. @@ -362,14 +362,12 @@ You can also [generate a solid color Data URL](https://png-pixel.com) to match t When true, the source image will be served as-is instead of changing quality, size, or format. Defaults to `false`. -This prop can be assigned to all images by updating `next.config.js` with the following experimental configuration: +This prop can be assigned to all images by updating `next.config.js` with the following configuration: ```js module.exports = { - experimental: { - images: { - unoptimized: true, - }, + images: { + unoptimized: true, }, } ``` @@ -387,23 +385,19 @@ Other properties on the `` component will be passed to the underlying ### Remote Patterns -> Note: The `remotePatterns` configuration is currently **experimental** and subject to change. Please use [`domains`](#domains) for production use cases. - To protect your application from malicious users, configuration is required in order to use external images. This ensures that only external images from your account can be served from the Next.js Image Optimization API. These external images can be configured with the `remotePatterns` property in your `next.config.js` file, as shown below: ```js module.exports = { - experimental: { - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: 'example.com', - port: '', - pathname: '/account123/**', - }, - ], - }, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'example.com', + port: '', + pathname: '/account123/**', + }, + ], }, } ``` @@ -414,15 +408,13 @@ Below is another example of the `remotePatterns` property in the `next.config.js ```js module.exports = { - experimental: { - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: '**.example.com', - }, - ], - }, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '**.example.com', + }, + ], }, } ``` diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index b303e54cb63e..23756edf0cab 100644 --- a/docs/api-reference/next/image.md +++ b/docs/api-reference/next/image.md @@ -16,6 +16,7 @@ description: Enable Image Optimization with the built-in Image component. | Version | Changes | | --------- | --------------------------------------------------------------------------------------------------------- | +| `v12.3.0` | `remotePatterns` and `unoptimized` configuration is stable. | | `v12.2.0` | Experimental `remotePatterns` and experimental `unoptimized` configuration added. `layout="raw"` removed. | | `v12.1.1` | `style` prop added. Experimental[\*](#experimental-raw-layout-mode) support for `layout="raw"` added. | | `v12.1.0` | `dangerouslyAllowSVG` and `contentSecurityPolicy` configuration added. | @@ -93,8 +94,6 @@ The layout behavior of the image as the viewport changes size. - When `fill`, the image will stretch both width and height to the dimensions of the parent element, provided the parent element is relative. - This is usually paired with the [`objectFit`](#objectFit) property. - Ensure the parent element has `position: relative` in their stylesheet. -- When `raw`[\*](#experimental-raw-layout-mode), the image will be rendered as a single image element with no wrappers, sizers or other responsive behavior. - - If your image styling will change the size of a `raw` image, you should include the `sizes` property for proper image serving. Otherwise your image will be requested as though it has fixed width and height. - [Demo background image](https://image-component.nextjs.gallery/background) ### loader @@ -324,14 +323,12 @@ const Example = () => { When true, the source image will be served as-is instead of changing quality, size, or format. Defaults to `false`. -This prop can be assigned to all images by updating `next.config.js` with the following experimental configuration: +This prop can be assigned to all images by updating `next.config.js` with the following configuration: ```js module.exports = { - experimental: { - images: { - unoptimized: true, - }, + images: { + unoptimized: true, }, } ``` @@ -351,23 +348,19 @@ Other properties on the `` component will be passed to the underlying ### Remote Patterns -> Note: The `remotePatterns` configuration is currently **experimental** and subject to change. Please use [`domains`](#domains) for production use cases. - To protect your application from malicious users, configuration is required in order to use external images. This ensures that only external images from your account can be served from the Next.js Image Optimization API. These external images can be configured with the `remotePatterns` property in your `next.config.js` file, as shown below: ```js module.exports = { - experimental: { - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: 'example.com', - port: '', - pathname: '/account123/**', - }, - ], - }, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'example.com', + port: '', + pathname: '/account123/**', + }, + ], }, } ``` @@ -378,15 +371,13 @@ Below is another example of the `remotePatterns` property in the `next.config.js ```js module.exports = { - experimental: { - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: '**.example.com', - }, - ], - }, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '**.example.com', + }, + ], }, } ``` diff --git a/docs/basic-features/font-optimization.md b/docs/basic-features/font-optimization.md index 33adf56fe48c..8965d97a26a9 100644 --- a/docs/basic-features/font-optimization.md +++ b/docs/basic-features/font-optimization.md @@ -27,28 +27,24 @@ To add a web font to your Next.js application, add the font to a [Custom `Docume ```js // pages/_document.js -import Document, { Html, Head, Main, NextScript } from 'next/document' - -class MyDocument extends Document { - render() { - return ( - - - - - -
- - - - ) - } +import { Html, Head, Main, NextScript } from 'next/document' + +export default function Document() { + return ( + + + + + +
+ + + + ) } - -export default MyDocument ``` Adding fonts to `_document` is preferred over individual pages. When adding fonts to a single page with [`next/head`](/docs/api-reference/next/head.md), font optimizations included by Next.js will not work on navigations between pages client-side or when using [streaming](/docs/advanced-features/react-18/streaming.md). diff --git a/docs/manifest.json b/docs/manifest.json index ac39acda9e63..ee3e0c2f7334 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -427,7 +427,7 @@ } }, { - "title": "next/future/image (experimental)", + "title": "next/future/image", "path": "/docs/api-reference/next/future/image.md" }, { diff --git a/errors/invalid-images-config.md b/errors/invalid-images-config.md index 978eadd08134..553c79facc74 100644 --- a/errors/invalid-images-config.md +++ b/errors/invalid-images-config.md @@ -31,17 +31,10 @@ module.exports = { dangerouslyAllowSVG: false, // set the Content-Security-Policy header contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", - // the following are experimental features, and may cause breaking changes - }, - experimental: { - images: { - // limit of 50 objects - remotePatterns: [], - // when true, every image will be unoptimized - unoptimized: false, - // when true, allow `next/future/image` to be imported - allowFutureImage: false, - }, + // limit of 50 objects + remotePatterns: [], + // when true, every image will be unoptimized + unoptimized: false, }, } ``` diff --git a/examples/active-class-name/next.config.js b/examples/active-class-name/next.config.js index 55e464ca31bf..31164407943d 100644 --- a/examples/active-class-name/next.config.js +++ b/examples/active-class-name/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { async rewrites() { return [ diff --git a/examples/analyze-bundles/next.config.js b/examples/analyze-bundles/next.config.js index e8f26c3425fe..2538b04e1ba0 100644 --- a/examples/analyze-bundles/next.config.js +++ b/examples/analyze-bundles/next.config.js @@ -1,8 +1,8 @@ -/** @type {import('next').NextConfig} */ const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }) +/** @type {import('next').NextConfig} */ const nextConfig = { // any configs you need } diff --git a/examples/blog-starter/tailwind.config.js b/examples/blog-starter/tailwind.config.js index 798640446b96..eb65df1d0c64 100644 --- a/examples/blog-starter/tailwind.config.js +++ b/examples/blog-starter/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: ['./components/**/*.tsx', './pages/**/*.tsx'], theme: { diff --git a/examples/blog-with-comment/tailwind.config.js b/examples/blog-with-comment/tailwind.config.js index cf9aebc4470b..bff6029207e2 100644 --- a/examples/blog-with-comment/tailwind.config.js +++ b/examples/blog-with-comment/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { mode: 'jit', purge: ['./pages/**/*.js', './components/**/*.js'], diff --git a/examples/blog/next.config.js b/examples/blog/next.config.js index edd2eaa0e6a9..c8f488590867 100644 --- a/examples/blog/next.config.js +++ b/examples/blog/next.config.js @@ -1,10 +1,10 @@ -/** @type {import('next').NextConfig} */ const withNextra = require('nextra')({ theme: 'nextra-theme-blog', themeConfig: './theme.config.js', // optional: add `unstable_staticImage: true` to enable Nextra's auto image import }) +/** @type {import('next').NextConfig} */ const nextConfig = { // any configs you need } diff --git a/examples/cms-agilitycms/tailwind.config.js b/examples/cms-agilitycms/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100644 --- a/examples/cms-agilitycms/tailwind.config.js +++ b/examples/cms-agilitycms/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/cms-builder-io/next.config.js b/examples/cms-builder-io/next.config.js index b4f3c29cb84d..873fcfa221c0 100644 --- a/examples/cms-builder-io/next.config.js +++ b/examples/cms-builder-io/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { images: { domains: ['cdn.builder.io'], diff --git a/examples/cms-builder-io/tailwind.config.js b/examples/cms-builder-io/tailwind.config.js index 1b23a5e9fe54..572cfc26ee53 100644 --- a/examples/cms-builder-io/tailwind.config.js +++ b/examples/cms-builder-io/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: ['./components/**/*.js', './pages/**/*.js'], theme: { diff --git a/examples/cms-buttercms/next.config.js b/examples/cms-buttercms/next.config.js index faa500f139b1..ab4e7c5d91dd 100644 --- a/examples/cms-buttercms/next.config.js +++ b/examples/cms-buttercms/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { reactStrictMode: true, async rewrites() { diff --git a/examples/cms-contentful/next.config.js b/examples/cms-contentful/next.config.js index 18ad50c430e8..ba59d56c7c95 100644 --- a/examples/cms-contentful/next.config.js +++ b/examples/cms-contentful/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { images: { loader: 'custom', diff --git a/examples/cms-contentful/tailwind.config.js b/examples/cms-contentful/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100644 --- a/examples/cms-contentful/tailwind.config.js +++ b/examples/cms-contentful/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/cms-cosmic/next.config.js b/examples/cms-cosmic/next.config.js index 6e746619622f..2ffe34808dc1 100644 --- a/examples/cms-cosmic/next.config.js +++ b/examples/cms-cosmic/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { images: { domains: ['imgix.cosmicjs.com'], diff --git a/examples/cms-cosmic/tailwind.config.js b/examples/cms-cosmic/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100644 --- a/examples/cms-cosmic/tailwind.config.js +++ b/examples/cms-cosmic/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/cms-datocms/next.config.js b/examples/cms-datocms/next.config.js index 761cfcc4d39b..a7240c74c00d 100644 --- a/examples/cms-datocms/next.config.js +++ b/examples/cms-datocms/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { images: { domains: ['www.datocms-assets.com'], diff --git a/examples/cms-datocms/tailwind.config.js b/examples/cms-datocms/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100644 --- a/examples/cms-datocms/tailwind.config.js +++ b/examples/cms-datocms/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/cms-drupal/next.config.js b/examples/cms-drupal/next.config.js index 2940b486cbb5..1cd44485a28b 100644 --- a/examples/cms-drupal/next.config.js +++ b/examples/cms-drupal/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { images: { domains: [process.env.NEXT_IMAGE_DOMAIN], diff --git a/examples/cms-drupal/tailwind.config.js b/examples/cms-drupal/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100644 --- a/examples/cms-drupal/tailwind.config.js +++ b/examples/cms-drupal/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/cms-ghost/next.config.js b/examples/cms-ghost/next.config.js index bdd3fa3f3a8e..f60a457f98f3 100644 --- a/examples/cms-ghost/next.config.js +++ b/examples/cms-ghost/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { images: { domains: ['static.ghost.org'], diff --git a/examples/cms-ghost/tailwind.config.js b/examples/cms-ghost/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100644 --- a/examples/cms-ghost/tailwind.config.js +++ b/examples/cms-ghost/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/cms-graphcms/next.config.js b/examples/cms-graphcms/next.config.js index ada42af3c759..a6f6d9553f73 100644 --- a/examples/cms-graphcms/next.config.js +++ b/examples/cms-graphcms/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { images: { domains: ['media.graphcms.com'], diff --git a/examples/cms-graphcms/tailwind.config.js b/examples/cms-graphcms/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100644 --- a/examples/cms-graphcms/tailwind.config.js +++ b/examples/cms-graphcms/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/cms-kontent/tailwind.config.js b/examples/cms-kontent/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100644 --- a/examples/cms-kontent/tailwind.config.js +++ b/examples/cms-kontent/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/cms-prepr/next.config.js b/examples/cms-prepr/next.config.js index fa43e390d377..8c66cf3f279f 100644 --- a/examples/cms-prepr/next.config.js +++ b/examples/cms-prepr/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { images: { domains: ['b-cdn.net'], diff --git a/examples/cms-prepr/tailwind.config.js b/examples/cms-prepr/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100644 --- a/examples/cms-prepr/tailwind.config.js +++ b/examples/cms-prepr/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/cms-prismic/next.config.js b/examples/cms-prismic/next.config.js index d3c513afdabd..ecad4e78e610 100644 --- a/examples/cms-prismic/next.config.js +++ b/examples/cms-prismic/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { images: { domains: ['images.prismic.io'], diff --git a/examples/cms-prismic/tailwind.config.js b/examples/cms-prismic/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100644 --- a/examples/cms-prismic/tailwind.config.js +++ b/examples/cms-prismic/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/cms-sanity/next.config.js b/examples/cms-sanity/next.config.js index 5e537bb53804..34e46875eae9 100644 --- a/examples/cms-sanity/next.config.js +++ b/examples/cms-sanity/next.config.js @@ -1,10 +1,9 @@ +/** @type {import('next').NextConfig} */ module.exports = { - experimental: { - images: { - allowFutureImage: true, - }, - }, images: { - domains: ['cdn.sanity.io', 'source.unsplash.com'], + remotePatterns: [ + { hostname: 'cdn.sanity.io' }, + { hostname: 'source.unsplash.com' }, + ], }, } diff --git a/examples/cms-sanity/tailwind.config.js b/examples/cms-sanity/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100644 --- a/examples/cms-sanity/tailwind.config.js +++ b/examples/cms-sanity/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/cms-storyblok/tailwind.config.js b/examples/cms-storyblok/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100644 --- a/examples/cms-storyblok/tailwind.config.js +++ b/examples/cms-storyblok/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/cms-strapi/next.config.js b/examples/cms-strapi/next.config.js index f9cdfbfe26d8..07325138840d 100644 --- a/examples/cms-strapi/next.config.js +++ b/examples/cms-strapi/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { images: { domains: ['localhost'], diff --git a/examples/cms-strapi/tailwind.config.js b/examples/cms-strapi/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100644 --- a/examples/cms-strapi/tailwind.config.js +++ b/examples/cms-strapi/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/cms-takeshape/tailwind.config.js b/examples/cms-takeshape/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100644 --- a/examples/cms-takeshape/tailwind.config.js +++ b/examples/cms-takeshape/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/cms-tina/tailwind.config.js b/examples/cms-tina/tailwind.config.js index b176069a2c6b..24ef0704e907 100644 --- a/examples/cms-tina/tailwind.config.js +++ b/examples/cms-tina/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: ['./components/**/*.js', './pages/**/*.js'], theme: { diff --git a/examples/cms-umbraco-heartcore/next.config.js b/examples/cms-umbraco-heartcore/next.config.js index cd98193170bb..bacc937a63fe 100644 --- a/examples/cms-umbraco-heartcore/next.config.js +++ b/examples/cms-umbraco-heartcore/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { images: { domains: ['media.umbraco.io'], diff --git a/examples/cms-umbraco-heartcore/tailwind.config.js b/examples/cms-umbraco-heartcore/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100755 --- a/examples/cms-umbraco-heartcore/tailwind.config.js +++ b/examples/cms-umbraco-heartcore/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/cms-wordpress/next.config.js b/examples/cms-wordpress/next.config.js index 1f189d615c02..8b9c50dd5731 100644 --- a/examples/cms-wordpress/next.config.js +++ b/examples/cms-wordpress/next.config.js @@ -5,6 +5,7 @@ if (!process.env.WORDPRESS_API_URL) { `) } +/** @type {import('next').NextConfig} */ module.exports = { images: { domains: [ diff --git a/examples/cms-wordpress/tailwind.config.js b/examples/cms-wordpress/tailwind.config.js index 21253ff3793e..ea0efae6d75f 100644 --- a/examples/cms-wordpress/tailwind.config.js +++ b/examples/cms-wordpress/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/github-pages/next.config.js b/examples/github-pages/next.config.js index 1318c5aa7ee6..41ad909fe4fa 100644 --- a/examples/github-pages/next.config.js +++ b/examples/github-pages/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { basePath: '/gh-pages-test', } diff --git a/examples/headers/next.config.js b/examples/headers/next.config.js index d19c985079e8..1fb9d8eb41df 100644 --- a/examples/headers/next.config.js +++ b/examples/headers/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { async headers() { return [ diff --git a/examples/i18n-routing/next.config.js b/examples/i18n-routing/next.config.js index f548199a3b10..4f24cc743643 100644 --- a/examples/i18n-routing/next.config.js +++ b/examples/i18n-routing/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { i18n: { locales: ['en', 'fr', 'nl'], diff --git a/examples/image-component/next.config.js b/examples/image-component/next.config.js index a57d88155532..ef4ded5dd232 100644 --- a/examples/image-component/next.config.js +++ b/examples/image-component/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { images: { domains: ['assets.vercel.com'], diff --git a/examples/modularize-imports/next.config.js b/examples/modularize-imports/next.config.js index 55be9582d2f9..fd1dae4f360f 100644 --- a/examples/modularize-imports/next.config.js +++ b/examples/modularize-imports/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { experimental: { modularizeImports: { diff --git a/examples/react-remove-properties/next.config.js b/examples/react-remove-properties/next.config.js index a74176cb7f10..7d3aca8225d8 100644 --- a/examples/react-remove-properties/next.config.js +++ b/examples/react-remove-properties/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { experimental: { reactRemoveProperties: true, diff --git a/examples/redirects/next.config.js b/examples/redirects/next.config.js index 6971321a8c23..44b3b62b9690 100644 --- a/examples/redirects/next.config.js +++ b/examples/redirects/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { // Uncomment the line below to enable basePath, pages and // redirects will then have a path prefix (`/app` in this case) diff --git a/examples/remove-console/next.config.js b/examples/remove-console/next.config.js index afad83efd0b7..12c0604656fa 100644 --- a/examples/remove-console/next.config.js +++ b/examples/remove-console/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { experimental: { removeConsole: { diff --git a/examples/reproduction-template/next.config.js b/examples/reproduction-template/next.config.js index b77e52627bdd..0e5c476c943b 100644 --- a/examples/reproduction-template/next.config.js +++ b/examples/reproduction-template/next.config.js @@ -1,6 +1,4 @@ /** @type {import("next").NextConfig} */ -const config = { +module.exports = { reactStrictMode: true, } - -module.exports = config diff --git a/examples/rewrites/next.config.js b/examples/rewrites/next.config.js index ade12e098a70..e106f864ec38 100644 --- a/examples/rewrites/next.config.js +++ b/examples/rewrites/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { async rewrites() { return [ diff --git a/examples/with-axiom/.gitignore b/examples/with-axiom/.gitignore new file mode 100644 index 000000000000..88b6f0d98164 --- /dev/null +++ b/examples/with-axiom/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo diff --git a/examples/with-axiom/README.md b/examples/with-axiom/README.md new file mode 100644 index 000000000000..0671cfa9c5fa --- /dev/null +++ b/examples/with-axiom/README.md @@ -0,0 +1,25 @@ +# Example app with Axiom + +This example shows how to use a [Next.js](https://nextjs.org/) project along with [Axiom](https://axiom.co) via the [next-axiom](https://github.com/axiomhq/next-axiom) package. A custom `withAxiom` wrapper is used to wrap the next config object, middleware and API functions. The `log` object could be used from frontend, middleware and API functions. + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) or preview live with [StackBlitz](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/with-axiom) + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-axiom&project-name=with-axiom&repository-name=with-axiom) + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. + +## How to use + +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:: + +```bash +npx create-next-app --example with-axiom with-axiom-app +# or +yarn create next-app --example with-axiom with-axiom-app +# or +pnpm create next-app --example with-axiom with-axiom-app +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)) and watch data coming into your Axiom dataset. diff --git a/examples/with-axiom/middleware.ts b/examples/with-axiom/middleware.ts new file mode 100644 index 000000000000..9c2833a5b8ec --- /dev/null +++ b/examples/with-axiom/middleware.ts @@ -0,0 +1,9 @@ +import { NextResponse } from 'next/server' +import { log, withAxiom } from 'next-axiom' + +async function middleware() { + log.info('Hello from middleware', { bar: 'baz' }) + return NextResponse.next() +} + +export default withAxiom(middleware) diff --git a/examples/with-axiom/next-env.d.ts b/examples/with-axiom/next-env.d.ts new file mode 100644 index 000000000000..4f11a03dc6cc --- /dev/null +++ b/examples/with-axiom/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/with-axiom/next.config.js b/examples/with-axiom/next.config.js new file mode 100644 index 000000000000..4ddf957b071a --- /dev/null +++ b/examples/with-axiom/next.config.js @@ -0,0 +1,8 @@ +const { withAxiom } = require('next-axiom') + +/** @type {import('next').NextConfig} */ +const nextConfig = withAxiom({ + reactStrictMode: true, +}) + +module.exports = nextConfig diff --git a/examples/with-axiom/package.json b/examples/with-axiom/package.json new file mode 100644 index 000000000000..5c1b7401446c --- /dev/null +++ b/examples/with-axiom/package.json @@ -0,0 +1,21 @@ +{ + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "latest", + "next-axiom": "^0.10.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "swr": "^1.3.0" + }, + "devDependencies": { + "@types/react": "^18.0.5", + "@types/react-dom": "^18.0.1", + "@types/node": "^16.11.26", + "typescript": "^4.7.4" + } +} diff --git a/examples/with-axiom/pages/_app.tsx b/examples/with-axiom/pages/_app.tsx new file mode 100644 index 000000000000..27fd9af82530 --- /dev/null +++ b/examples/with-axiom/pages/_app.tsx @@ -0,0 +1,11 @@ +import { log } from 'next-axiom' +import { AppProps } from 'next/app' +export { reportWebVitals } from 'next-axiom' + +log.info('Hello from frontend', { foo: 'bar' }) + +const MyApp = ({ Component, pageProps }: AppProps) => { + return +} + +export default MyApp diff --git a/examples/with-axiom/pages/api/hello.ts b/examples/with-axiom/pages/api/hello.ts new file mode 100644 index 000000000000..3515f82ab103 --- /dev/null +++ b/examples/with-axiom/pages/api/hello.ts @@ -0,0 +1,10 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' +import { log, withAxiom } from 'next-axiom' + +async function handler(req: NextApiRequest, res: NextApiResponse) { + log.info('Hello from function', { url: req.url }) + res.status(200).json({ name: 'John Doe' }) +} + +export default withAxiom(handler) diff --git a/examples/with-axiom/pages/index.tsx b/examples/with-axiom/pages/index.tsx new file mode 100644 index 000000000000..230ac54de96a --- /dev/null +++ b/examples/with-axiom/pages/index.tsx @@ -0,0 +1,31 @@ +import { GetStaticPropsContext } from 'next' +import { log } from 'next-axiom' +import useSWR from 'swr' + +export const getStaticProps = async (ctx: GetStaticPropsContext) => { + log.info('Hello from SSR', { ctx }) + return { + props: {}, + } +} + +const fetcher = async (...args: any[]) => { + log.info('Hello from SWR', { args }) + const res = await fetch.apply(null, [...args]) + return await res.json() +} + +const Home = () => { + const { data, error } = useSWR('/api/hello', fetcher) + + if (error) return
Failed to load
+ if (!data) return
Loading...
+ + return ( +
+

{data.name}

+
+ ) +} + +export default Home diff --git a/examples/with-axiom/tsconfig.json b/examples/with-axiom/tsconfig.json new file mode 100644 index 000000000000..b9cc80c0d4ec --- /dev/null +++ b/examples/with-axiom/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "incremental": true, + "esModuleInterop": true, + "module": "esnext", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/examples/with-compiled-css/next.config.js b/examples/with-compiled-css/next.config.js index 599c97127dac..72d5cd33f75e 100644 --- a/examples/with-compiled-css/next.config.js +++ b/examples/with-compiled-css/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { webpack: (config) => { config.module.rules.push({ diff --git a/examples/with-docker-multi-env/next.config.js b/examples/with-docker-multi-env/next.config.js index e97173b4b379..b6b4bc39ee7f 100644 --- a/examples/with-docker-multi-env/next.config.js +++ b/examples/with-docker-multi-env/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { output: 'standalone', } diff --git a/examples/with-docker/next.config.js b/examples/with-docker/next.config.js index e97173b4b379..b6b4bc39ee7f 100644 --- a/examples/with-docker/next.config.js +++ b/examples/with-docker/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { output: 'standalone', } diff --git a/examples/with-firebase-hosting/src/next.config.js b/examples/with-firebase-hosting/src/next.config.js index cb2b3964d285..ef671cdd1b05 100644 --- a/examples/with-firebase-hosting/src/next.config.js +++ b/examples/with-firebase-hosting/src/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { distDir: '../.next', } diff --git a/examples/with-http2/next.config.js b/examples/with-http2/next.config.js index a50ddf389e47..3bf3711250f4 100644 --- a/examples/with-http2/next.config.js +++ b/examples/with-http2/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { /* this needs to be set to false until a bug in the compression npm module gets fixed. reference: https://github.com/expressjs/compression/issues/122 diff --git a/examples/with-i18n-next-intl/next.config.js b/examples/with-i18n-next-intl/next.config.js index 4c9224d55b31..8309826c4993 100644 --- a/examples/with-i18n-next-intl/next.config.js +++ b/examples/with-i18n-next-intl/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { i18n: { locales: ['en', 'de'], diff --git a/examples/with-ionic-typescript/next.config.js b/examples/with-ionic-typescript/next.config.js index d0a390562980..2f607c52f456 100644 --- a/examples/with-ionic-typescript/next.config.js +++ b/examples/with-ionic-typescript/next.config.js @@ -1,5 +1,7 @@ const path = require('path') const CopyPlugin = require('copy-webpack-plugin') + +/** @type {import('next').NextConfig} */ module.exports = { webpack: (config) => { config.plugins.push( diff --git a/examples/with-lingui/next.config.js b/examples/with-lingui/next.config.js index c0c4fb7d9e63..165c1b213a11 100644 --- a/examples/with-lingui/next.config.js +++ b/examples/with-lingui/next.config.js @@ -1,5 +1,6 @@ const { locales, sourceLocale } = require('./lingui.config.js') +/** @type {import('next').NextConfig} */ module.exports = { i18n: { locales, diff --git a/examples/with-mysql/next.config.js b/examples/with-mysql/next.config.js index 0d6071006ab3..8b61df4e50f8 100644 --- a/examples/with-mysql/next.config.js +++ b/examples/with-mysql/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { reactStrictMode: true, } diff --git a/examples/with-mysql/tailwind.config.js b/examples/with-mysql/tailwind.config.js index 4cd61381b811..6aa9dd319121 100644 --- a/examples/with-mysql/tailwind.config.js +++ b/examples/with-mysql/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', diff --git a/examples/with-netlify-cms/next.config.js b/examples/with-netlify-cms/next.config.js index 1e266830a5b3..ebec7617e81c 100644 --- a/examples/with-netlify-cms/next.config.js +++ b/examples/with-netlify-cms/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { webpack: (configuration) => { configuration.module.rules.push({ diff --git a/examples/with-react-native-web/next.config.js b/examples/with-react-native-web/next.config.js index 0b2154dd451d..b082f9ab9ff4 100644 --- a/examples/with-react-native-web/next.config.js +++ b/examples/with-react-native-web/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { webpack: (config) => { config.resolve.alias = { diff --git a/examples/with-redis/tailwind.config.js b/examples/with-redis/tailwind.config.js index 211048ff7755..6c8131590350 100644 --- a/examples/with-redis/tailwind.config.js +++ b/examples/with-redis/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { mode: 'jit', purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], diff --git a/examples/with-sitemap/next.config.js b/examples/with-sitemap/next.config.js index 08a972d01c6d..a5932f873859 100644 --- a/examples/with-sitemap/next.config.js +++ b/examples/with-sitemap/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { webpack: (config, { isServer }) => { if (isServer) { diff --git a/examples/with-storybook/next.config.js b/examples/with-storybook/next.config.js index 0d6071006ab3..8b61df4e50f8 100644 --- a/examples/with-storybook/next.config.js +++ b/examples/with-storybook/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { reactStrictMode: true, } diff --git a/examples/with-styletron/next.config.js b/examples/with-styletron/next.config.js index 9facbdfb490d..a5d92455ea27 100644 --- a/examples/with-styletron/next.config.js +++ b/examples/with-styletron/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { webpack: function (config) { config.externals = config.externals || {} diff --git a/examples/with-tailwindcss-emotion/tailwind.config.js b/examples/with-tailwindcss-emotion/tailwind.config.js index 78133814b87f..9c00d8825b47 100644 --- a/examples/with-tailwindcss-emotion/tailwind.config.js +++ b/examples/with-tailwindcss-emotion/tailwind.config.js @@ -1,5 +1,6 @@ const colors = require('tailwindcss/colors') +/** @type {import('tailwindcss').Config} */ module.exports = { purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], darkMode: 'class', diff --git a/examples/with-typescript-graphql/next.config.js b/examples/with-typescript-graphql/next.config.js index 6285426ed3f7..970a2fb5ed55 100644 --- a/examples/with-typescript-graphql/next.config.js +++ b/examples/with-typescript-graphql/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { webpack(config, options) { config.module.rules.push({ diff --git a/examples/with-userbase/tailwind.config.js b/examples/with-userbase/tailwind.config.js index 211048ff7755..6c8131590350 100644 --- a/examples/with-userbase/tailwind.config.js +++ b/examples/with-userbase/tailwind.config.js @@ -1,3 +1,4 @@ +/** @type {import('tailwindcss').Config} */ module.exports = { mode: 'jit', purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], diff --git a/examples/with-webassembly/next.config.js b/examples/with-webassembly/next.config.js index e3274d822908..4d5f3d98c8b8 100644 --- a/examples/with-webassembly/next.config.js +++ b/examples/with-webassembly/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { webpack(config) { config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm' diff --git a/examples/with-why-did-you-render/next.config.js b/examples/with-why-did-you-render/next.config.js index cd716eed3681..83c06c85ed2b 100644 --- a/examples/with-why-did-you-render/next.config.js +++ b/examples/with-why-did-you-render/next.config.js @@ -1,5 +1,6 @@ const path = require('path') +/** @type {import('next').NextConfig} */ module.exports = { webpack(config, { dev, isServer }) { if (dev && !isServer) { diff --git a/examples/with-zones/blog/next.config.js b/examples/with-zones/blog/next.config.js index a7783b6e82fa..32a86880ab72 100644 --- a/examples/with-zones/blog/next.config.js +++ b/examples/with-zones/blog/next.config.js @@ -1,3 +1,4 @@ +/** @type {import('next').NextConfig} */ module.exports = { basePath: '/blog', } diff --git a/examples/with-zones/home/next.config.js b/examples/with-zones/home/next.config.js index d27449f1518a..a6bde7780383 100644 --- a/examples/with-zones/home/next.config.js +++ b/examples/with-zones/home/next.config.js @@ -1,5 +1,6 @@ const { BLOG_URL } = process.env +/** @type {import('next').NextConfig} */ module.exports = { async rewrites() { return [ diff --git a/lerna.json b/lerna.json index 57eb3bf48f24..044691dcebb3 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "12.2.6-canary.7" + "version": "12.2.6-canary.8" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index b44bb0d7864b..8068c3d98951 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "12.2.6-canary.7", + "version": "12.2.6-canary.8", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 8e24e00c031b..6d7a4af660cb 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "12.2.6-canary.7", + "version": "12.2.6-canary.8", "description": "ESLint configuration used by NextJS.", "main": "index.js", "license": "MIT", @@ -9,7 +9,7 @@ "directory": "packages/eslint-config-next" }, "dependencies": { - "@next/eslint-plugin-next": "12.2.6-canary.7", + "@next/eslint-plugin-next": "12.2.6-canary.8", "@rushstack/eslint-patch": "^1.1.3", "@typescript-eslint/parser": "^5.21.0", "eslint-import-resolver-node": "^0.3.6", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index f2226efd3117..55032e96a498 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "12.2.6-canary.7", + "version": "12.2.6-canary.8", "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 52578c7bf162..6487021d65b2 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "12.2.6-canary.7", + "version": "12.2.6-canary.8", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 5874126c6a23..b85a1b7d9f4e 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "12.2.6-canary.7", + "version": "12.2.6-canary.8", "license": "MIT", "dependencies": { "chalk": "4.1.0", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index a559a569712b..11322dbe40fe 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "12.2.6-canary.7", + "version": "12.2.6-canary.8", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index c5b2c1370b1c..245fe78a3c1a 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "12.2.6-canary.7", + "version": "12.2.6-canary.8", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 40258e8f791f..4ba2fcfa1983 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "12.2.6-canary.7", + "version": "12.2.6-canary.8", "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 48912ffaa1c2..4b3e6677264f 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "12.2.6-canary.7", + "version": "12.2.6-canary.8", "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 f0ad046e77b9..9e7f7819cf26 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "12.2.6-canary.7", + "version": "12.2.6-canary.8", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 7e226bb671a4..75beba2b8dbc 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "12.2.6-canary.7", + "version": "12.2.6-canary.8", "private": true, "scripts": { "build-native": "napi build --platform -p next-swc-napi --cargo-name next_swc_napi native --features plugin", diff --git a/packages/next/build/analysis/get-page-static-info.ts b/packages/next/build/analysis/get-page-static-info.ts index 6e2389803feb..cc18cc8d743a 100644 --- a/packages/next/build/analysis/get-page-static-info.ts +++ b/packages/next/build/analysis/get-page-static-info.ts @@ -1,5 +1,6 @@ import { isServerRuntime } from '../../server/config-shared' import type { NextConfig } from '../../server/config-shared' +import type { Middleware, RouteHas } from '../../lib/load-custom-routes' import { extractExportedConstValue, UnsupportedValueError, @@ -9,10 +10,17 @@ import { promises as fs } from 'fs' import { tryToParsePath } from '../../lib/try-to-parse-path' import * as Log from '../output/log' import { SERVER_RUNTIME } from '../../lib/constants' -import { ServerRuntime } from '../../types' +import { ServerRuntime } from 'next/types' +import { checkCustomRoutes } from '../../lib/load-custom-routes' -interface MiddlewareConfig { - pathMatcher: RegExp +export interface MiddlewareConfig { + matchers: MiddlewareMatcher[] +} + +export interface MiddlewareMatcher { + regexp: string + locale?: false + has?: RouteHas[] } export interface PageStaticInfo { @@ -81,55 +89,63 @@ async function tryToReadFile(filePath: string, shouldThrow: boolean) { } } -function getMiddlewareRegExpStrings( +function getMiddlewareMatchers( matcherOrMatchers: unknown, nextConfig: NextConfig -): string[] { +): MiddlewareMatcher[] { + let matchers: unknown[] = [] if (Array.isArray(matcherOrMatchers)) { - return matcherOrMatchers.flatMap((matcher) => - getMiddlewareRegExpStrings(matcher, nextConfig) - ) + matchers = matcherOrMatchers + } else { + matchers.push(matcherOrMatchers) } const { i18n } = nextConfig - if (typeof matcherOrMatchers !== 'string') { - throw new Error( - '`matcher` must be a path matcher or an array of path matchers' - ) - } + let routes = matchers.map( + (m) => (typeof m === 'string' ? { source: m } : m) as Middleware + ) - let matcher: string = matcherOrMatchers + // check before we process the routes and after to ensure + // they are still valid + checkCustomRoutes(routes, 'middleware') - if (!matcher.startsWith('/')) { - throw new Error('`matcher`: path matcher must start with /') - } - const isRoot = matcher === '/' + routes = routes.map((r) => { + let { source } = r - if (i18n?.locales) { - matcher = `/:nextInternalLocale([^/.]{1,})${isRoot ? '' : matcher}` - } + const isRoot = source === '/' - matcher = `/:nextData(_next/data/[^/]{1,})?${matcher}${ - isRoot - ? `(${nextConfig.i18n ? '|\\.json|' : ''}/?index|/?index\\.json)?` - : '(.json)?' - }` + if (i18n?.locales && r.locale !== false) { + source = `/:nextInternalLocale([^/.]{1,})${isRoot ? '' : source}` + } - if (nextConfig.basePath) { - matcher = `${nextConfig.basePath}${matcher}` - } - const parsedPage = tryToParsePath(matcher) + source = `/:nextData(_next/data/[^/]{1,})?${source}${ + isRoot + ? `(${nextConfig.i18n ? '|\\.json|' : ''}/?index|/?index\\.json)?` + : '(.json)?' + }` - if (parsedPage.error) { - throw new Error(`Invalid path matcher: ${matcher}`) - } + if (nextConfig.basePath) { + source = `${nextConfig.basePath}${source}` + } - const regexes = [parsedPage.regexStr].filter((x): x is string => !!x) - if (regexes.length < 1) { - throw new Error("Can't parse matcher") - } else { - return regexes - } + return { ...r, source } + }) + + checkCustomRoutes(routes, 'middleware') + + return routes.map((r) => { + const { source, ...rest } = r + const parsedPage = tryToParsePath(source) + + if (parsedPage.error || !parsedPage.regexStr) { + throw new Error(`Invalid source: ${source}`) + } + + return { + ...rest, + regexp: parsedPage.regexStr, + } + }) } function getMiddlewareConfig( @@ -139,15 +155,7 @@ function getMiddlewareConfig( const result: Partial = {} if (config.matcher) { - result.pathMatcher = new RegExp( - getMiddlewareRegExpStrings(config.matcher, nextConfig).join('|') - ) - - if (result.pathMatcher.source.length > 4096) { - throw new Error( - `generated matcher config must be less than 4096 characters.` - ) - } + result.matchers = getMiddlewareMatchers(config.matcher, nextConfig) } return result diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 5d1ce984286a..2d558ae91c17 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -4,6 +4,10 @@ import type { EdgeSSRLoaderQuery } from './webpack/loaders/next-edge-ssr-loader' import type { NextConfigComplete } from '../server/config-shared' import type { ServerlessLoaderQuery } from './webpack/loaders/next-serverless-loader' import type { webpack } from 'next/dist/compiled/webpack/webpack' +import type { + MiddlewareConfig, + MiddlewareMatcher, +} from './analysis/get-page-static-info' import type { LoadedEnvFiles } from '@next/env' import chalk from 'next/dist/compiled/chalk' import { posix, join } from 'path' @@ -42,6 +46,7 @@ import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep' import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' import { serverComponentRegex } from './webpack/loaders/utils' import { ServerRuntime } from '../types' +import { encodeMatchers } from './webpack/loaders/next-middleware-loader' type ObjectValue = T extends { [key: string]: infer V } ? V : never @@ -163,7 +168,7 @@ export function getEdgeServerEntry(opts: { isServerComponent: boolean page: string pages: { [page: string]: string } - middleware?: { pathMatcher?: RegExp } + middleware?: Partial pagesType?: 'app' | 'pages' | 'root' appDirLoader?: string }) { @@ -171,12 +176,9 @@ export function getEdgeServerEntry(opts: { const loaderParams: MiddlewareLoaderOptions = { absolutePagePath: opts.absolutePagePath, page: opts.page, - // pathMatcher can have special characters that break the loader params - // parsing so we base64 encode/decode the string - matcherRegexp: Buffer.from( - (opts.middleware?.pathMatcher && opts.middleware.pathMatcher.source) || - '' - ).toString('base64'), + matchers: opts.middleware?.matchers + ? encodeMatchers(opts.middleware.matchers) + : '', } return `next-middleware-loader?${stringify(loaderParams)}!` @@ -347,7 +349,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { const server: webpack.EntryObject = {} const client: webpack.EntryObject = {} const nestedMiddleware: string[] = [] - let middlewareRegex: string | undefined = undefined + let middlewareMatchers: MiddlewareMatcher[] | undefined = undefined const getEntryHandler = (mappings: Record, pagesType: 'app' | 'pages' | 'root') => @@ -402,7 +404,9 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { }) if (isMiddlewareFile(page)) { - middlewareRegex = staticInfo.middleware?.pathMatcher?.source || '.*' + middlewareMatchers = staticInfo.middleware?.matchers ?? [ + { regexp: '.*' }, + ] if (target === 'serverless') { throw new MiddlewareInServerlessTargetError() @@ -488,7 +492,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { client, server, edgeServer, - middlewareRegex, + middlewareMatchers, } } diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 46209a228e86..d311eb803671 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -856,7 +856,7 @@ export default async function build( runWebpackSpan, target, appDir, - middlewareRegex: entrypoints.middlewareRegex, + middlewareMatchers: entrypoints.middlewareMatchers, } const configs = await runWebpackSpan @@ -2413,7 +2413,7 @@ export default async function build( const { deviceSizes, imageSizes } = images ;(images as any).sizes = [...deviceSizes, ...imageSizes] ;(images as any).remotePatterns = ( - config?.experimental?.images?.remotePatterns || [] + config?.images?.remotePatterns || [] ).map((p: RemotePattern) => ({ // Should be the same as matchRemotePattern() protocol: p.protocol, diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 2641f066968f..02f1012f7497 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -54,6 +54,7 @@ import type { SWC_TARGET_TRIPLE, } from './webpack/plugins/telemetry-plugin' import type { Span } from '../trace' +import type { MiddlewareMatcher } from './analysis/get-page-static-info' import { withoutRSCExtensions } from './utils' import browserslist from 'next/dist/compiled/browserslist' import loadJsConfig from './load-jsconfig' @@ -90,7 +91,7 @@ export function getDefineEnv({ hasReactRoot, isNodeServer, isEdgeServer, - middlewareRegex, + middlewareMatchers, hasServerComponents, }: { dev?: boolean @@ -100,7 +101,7 @@ export function getDefineEnv({ hasReactRoot?: boolean isNodeServer?: boolean isEdgeServer?: boolean - middlewareRegex?: string + middlewareMatchers?: MiddlewareMatcher[] config: NextConfigComplete hasServerComponents?: boolean }) { @@ -144,8 +145,8 @@ export function getDefineEnv({ isEdgeServer ? 'edge' : 'nodejs' ), }), - 'process.env.__NEXT_MIDDLEWARE_REGEX': JSON.stringify( - middlewareRegex || '' + 'process.env.__NEXT_MIDDLEWARE_MATCHERS': JSON.stringify( + middlewareMatchers || [] ), 'process.env.__NEXT_MANUAL_CLIENT_BASE_PATH': JSON.stringify( config.experimental.manualClientBasePath @@ -195,14 +196,12 @@ export function getDefineEnv({ path: config.images.path, loader: config.images.loader, dangerouslyAllowSVG: config.images.dangerouslyAllowSVG, - experimentalUnoptimized: config?.experimental?.images?.unoptimized, - experimentalFuture: config.experimental?.images?.allowFutureImage, + unoptimized: config?.images?.unoptimized, ...(dev ? { // pass domains in development to allow validating on the client domains: config.images.domains, - experimentalRemotePatterns: - config.experimental?.images?.remotePatterns, + remotePatterns: config.images?.remotePatterns, } : {}), }), @@ -510,7 +509,7 @@ export default async function getBaseWebpackConfig( runWebpackSpan, target = COMPILER_NAMES.server, appDir, - middlewareRegex, + middlewareMatchers, }: { buildId: string config: NextConfigComplete @@ -525,7 +524,7 @@ export default async function getBaseWebpackConfig( runWebpackSpan: Span target?: string appDir?: string - middlewareRegex?: string + middlewareMatchers?: MiddlewareMatcher[] } ): Promise { const isClient = compilerType === COMPILER_NAMES.client @@ -1679,7 +1678,7 @@ export default async function getBaseWebpackConfig( hasReactRoot, isNodeServer, isEdgeServer, - middlewareRegex, + middlewareMatchers, hasServerComponents, }) ), diff --git a/packages/next/build/webpack/loaders/get-module-build-info.ts b/packages/next/build/webpack/loaders/get-module-build-info.ts index eccedc148468..fa43fa1701ee 100644 --- a/packages/next/build/webpack/loaders/get-module-build-info.ts +++ b/packages/next/build/webpack/loaders/get-module-build-info.ts @@ -1,3 +1,4 @@ +import type { MiddlewareMatcher } from '../../analysis/get-page-static-info' import { webpack } from 'next/dist/compiled/webpack/webpack' /** @@ -25,7 +26,7 @@ export interface RouteMeta { export interface EdgeMiddlewareMeta { page: string - matcherRegexp?: string + matchers?: MiddlewareMatcher[] } export interface EdgeSSRMeta { diff --git a/packages/next/build/webpack/loaders/next-flight-client-loader/index.ts b/packages/next/build/webpack/loaders/next-flight-client-loader/index.ts index d761f8f7df8f..6c27a5a49f2d 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-loader/index.ts @@ -5,31 +5,50 @@ * LICENSE file in the root directory of this source tree. */ +import path from 'path' import { checkExports } from '../../../analysis/get-page-static-info' import { parse } from '../../../swc' +function containsPath(parent: string, child: string) { + const relation = path.relative(parent, child) + return !!relation && !relation.startsWith('..') && !path.isAbsolute(relation) +} + export default async function transformSource( this: any, source: string ): Promise { - const { resourcePath } = this - - const transformedSource = source - if (typeof transformedSource !== 'string') { + if (typeof source !== 'string') { throw new Error('Expected source to have been transformed to a string.') } - const swcAST = await parse(transformedSource, { - filename: resourcePath, - isModule: 'unknown', - }) - const { ssg, ssr } = checkExports(swcAST) + const appDir = path.join(this.rootContext, 'app') + const isUnderAppDir = containsPath(appDir, this.resourcePath) + const filename = path.basename(this.resourcePath) + const isPageOrLayoutFile = /^(page|layout)\.client\.\w+$/.test(filename) + + const createError = (name: string) => + new Error( + `${name} is not supported in client components.\nFrom: ${this.resourcePath}` + ) + + if (isUnderAppDir && isPageOrLayoutFile) { + const swcAST = await parse(source, { + filename: this.resourcePath, + isModule: 'unknown', + }) + const { ssg, ssr } = checkExports(swcAST) + if (ssg) { + this.emitError(createError('getStaticProps')) + } + if (ssr) { + this.emitError(createError('getServerSideProps')) + } + } const output = ` const { createProxy } = require("next/dist/build/webpack/loaders/next-flight-client-loader/module-proxy")\n -module.exports = createProxy(${JSON.stringify( - resourcePath - )}, { ssr: ${ssr}, ssg: ${ssg} }) +module.exports = createProxy(${JSON.stringify(this.resourcePath)}) ` return output } diff --git a/packages/next/build/webpack/loaders/next-flight-client-loader/module-proxy.ts b/packages/next/build/webpack/loaders/next-flight-client-loader/module-proxy.ts index 80f7edf54b9b..4d9f6bcad03d 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-loader/module-proxy.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-loader/module-proxy.ts @@ -38,9 +38,6 @@ const proxyHandlers: ProxyHandler = { // whole object or just the default export. name: '', async: target.async, - - ssr: target.ssr, - ssg: target.ssg, } return true case 'then': @@ -57,9 +54,6 @@ const proxyHandlers: ProxyHandler = { filepath: target.filepath, name: '*', // Represents the whole object instead of a particular import. async: true, - - ssr: target.ssr, - ssg: target.ssg, } return Promise.resolve( resolve(new Proxy(moduleReference, proxyHandlers)) @@ -74,11 +68,6 @@ const proxyHandlers: ProxyHandler = { return then } break - - case 'ssg': - return target.ssg - case 'ssr': - return target.ssr default: break } @@ -102,18 +91,12 @@ const proxyHandlers: ProxyHandler = { }, } -export function createProxy( - moduleId: string, - { ssr, ssg }: { ssr: boolean; ssg: boolean } -) { +export function createProxy(moduleId: string) { const moduleReference = { $$typeof: MODULE_REFERENCE, filepath: moduleId, name: '*', // Represents the whole object instead of a particular import. async: false, - - ssr, - ssg, } return new Proxy(moduleReference, proxyHandlers) } diff --git a/packages/next/build/webpack/loaders/next-middleware-loader.ts b/packages/next/build/webpack/loaders/next-middleware-loader.ts index d6f0b2389c1f..f125a5780a18 100644 --- a/packages/next/build/webpack/loaders/next-middleware-loader.ts +++ b/packages/next/build/webpack/loaders/next-middleware-loader.ts @@ -1,3 +1,4 @@ +import type { MiddlewareMatcher } from '../../analysis/get-page-static-info' import { getModuleBuildInfo } from './get-module-build-info' import { stringifyRequest } from '../stringify-request' import { MIDDLEWARE_LOCATION_REGEXP } from '../../../lib/constants' @@ -5,23 +6,32 @@ import { MIDDLEWARE_LOCATION_REGEXP } from '../../../lib/constants' export type MiddlewareLoaderOptions = { absolutePagePath: string page: string - matcherRegexp?: string + matchers?: string +} + +// matchers can have special characters that break the loader params +// parsing so we base64 encode/decode the string +export function encodeMatchers(matchers: MiddlewareMatcher[]) { + return Buffer.from(JSON.stringify(matchers)).toString('base64') +} + +export function decodeMatchers(encodedMatchers: string) { + return JSON.parse( + Buffer.from(encodedMatchers, 'base64').toString() + ) as MiddlewareMatcher[] } export default function middlewareLoader(this: any) { const { absolutePagePath, page, - matcherRegexp: base64MatcherRegex, + matchers: encodedMatchers, }: MiddlewareLoaderOptions = this.getOptions() - const matcherRegexp = Buffer.from( - base64MatcherRegex || '', - 'base64' - ).toString() + const matchers = encodedMatchers ? decodeMatchers(encodedMatchers) : undefined const stringifiedPagePath = stringifyRequest(this, absolutePagePath) const buildInfo = getModuleBuildInfo(this._module) buildInfo.nextEdgeMiddleware = { - matcherRegexp, + matchers, page: page.replace(new RegExp(`/${MIDDLEWARE_LOCATION_REGEXP}$`), '') || '/', } diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index 11432fe8d730..82b94f8b4101 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -3,6 +3,7 @@ import type { EdgeMiddlewareMeta, } from '../loaders/get-module-build-info' import type { EdgeSSRMeta } from '../loaders/get-module-build-info' +import type { MiddlewareMatcher } from '../../analysis/get-page-static-info' import { getNamedMiddlewareRegex } from '../../../shared/lib/router/utils/route-regex' import { getModuleBuildInfo } from '../loaders/get-module-build-info' import { getSortedRoutes } from '../../../shared/lib/router/utils' @@ -23,13 +24,13 @@ export interface EdgeFunctionDefinition { files: string[] name: string page: string - regexp: string + matchers: MiddlewareMatcher[] wasm?: AssetBinding[] assets?: AssetBinding[] } export interface MiddlewareManifest { - version: 1 + version: 2 sortedMiddleware: string[] middleware: { [page: string]: EdgeFunctionDefinition } functions: { [page: string]: EdgeFunctionDefinition } @@ -49,7 +50,7 @@ const middlewareManifest: MiddlewareManifest = { sortedMiddleware: [], middleware: {}, functions: {}, - version: 1, + version: 2, } /** @@ -138,14 +139,16 @@ function getCreateAssets(params: { const { namedRegex } = getNamedMiddlewareRegex(page, { catchAll: !metadata.edgeSSR && !metadata.edgeApiFunction, }) - const regexp = metadata?.edgeMiddleware?.matcherRegexp || namedRegex + const matchers = metadata?.edgeMiddleware?.matchers ?? [ + { regexp: namedRegex }, + ] const edgeFunctionDefinition: EdgeFunctionDefinition = { env: Array.from(metadata.env), files: getEntryFiles(entrypoint.getFiles(), metadata), name: entrypoint.name, page: page, - regexp, + matchers, wasm: Array.from(metadata.wasmBindings, ([name, filePath]) => ({ name, filePath, diff --git a/packages/next/cli/next-dev.ts b/packages/next/cli/next-dev.ts index 9048fd028aed..17dd7d6ee583 100755 --- a/packages/next/cli/next-dev.ts +++ b/packages/next/cli/next-dev.ts @@ -2,7 +2,7 @@ import arg from 'next/dist/compiled/arg/index.js' import { existsSync, watchFile } from 'fs' import { startServer } from '../server/lib/start-server' -import { printAndExit } from '../server/lib/utils' +import { getPort, printAndExit } from '../server/lib/utils' import * as Log from '../build/output/log' import { startedDevelopmentServer } from '../build/output' import { cliCommand } from '../lib/commands' @@ -75,16 +75,11 @@ const nextDev: cliCommand = (argv) => { ) } } - const allowRetry = !args['--port'] - let port: number = - args['--port'] || (process.env.PORT && parseInt(process.env.PORT)) || 3000 - // we allow the server to use a random port while testing - // instead of attempting to find a random port and then hope - // it doesn't become occupied before we leverage it - if (process.env.__NEXT_FORCED_PORT) { - port = parseInt(process.env.__NEXT_FORCED_PORT, 10) || 0 - } + const port = getPort(args) + // If neither --port nor PORT were specified, it's okay to retry new ports. + const allowRetry = + args['--port'] === undefined && process.env.PORT === undefined // We do not set a default host value here to prevent breaking // some set-ups that rely on listening on other interfaces diff --git a/packages/next/cli/next-start.ts b/packages/next/cli/next-start.ts index fc7cfe39369b..4f6ddf84e656 100755 --- a/packages/next/cli/next-start.ts +++ b/packages/next/cli/next-start.ts @@ -2,7 +2,7 @@ import arg from 'next/dist/compiled/arg/index.js' import { startServer } from '../server/lib/start-server' -import { printAndExit } from '../server/lib/utils' +import { getPort, printAndExit } from '../server/lib/utils' import * as Log from '../build/output/log' import isError from '../lib/is-error' import { getProjectDir } from '../lib/get-project-dir' @@ -52,13 +52,8 @@ const nextStart: cliCommand = (argv) => { } const dir = getProjectDir(args._[0]) - let port: number = - args['--port'] || (process.env.PORT && parseInt(process.env.PORT)) || 3000 const host = args['--hostname'] || '0.0.0.0' - - if (process.env.__NEXT_FORCED_PORT) { - port = parseInt(process.env.__NEXT_FORCED_PORT, 10) || 0 - } + const port = getPort(args) const keepAliveTimeoutArg: number | undefined = args['--keepAliveTimeout'] if ( diff --git a/packages/next/client/future/image.tsx b/packages/next/client/future/image.tsx index 3031fcd7b9de..7dbf0807d745 100644 --- a/packages/next/client/future/image.tsx +++ b/packages/next/client/future/image.tsx @@ -15,11 +15,6 @@ import { import { ImageConfigContext } from '../../shared/lib/image-config-context' import { warnOnce } from '../../shared/lib/utils' -const { - experimentalFuture = false, - experimentalRemotePatterns = [], - experimentalUnoptimized, -} = (process.env.__NEXT_IMAGE_OPTS as any) || {} const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete const allImgs = new Map< string, @@ -100,9 +95,10 @@ function isStaticImport(src: string | StaticImport): src is StaticImport { export type ImageProps = Omit< JSX.IntrinsicElements['img'], - 'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading' + 'src' | 'srcSet' | 'ref' | 'alt' | 'width' | 'height' | 'loading' > & { src: string | StaticImport + alt: string width?: number | string height?: number | string fill?: boolean @@ -116,7 +112,7 @@ export type ImageProps = Omit< onLoadingComplete?: OnLoadingComplete } -type ImageElementProps = Omit & { +type ImageElementProps = Omit & { srcString: string imgAttributes: GenImgAttrsResult heightInt: number | undefined @@ -392,6 +388,11 @@ const ImageElement = ({ img ) } + if (img.getAttribute('alt') === null) { + console.error( + `Image is missing required "alt" property. Please add Alternative Text to describe the image for screen readers and search engines.` + ) + } } if (img.complete) { handleLoading( @@ -469,10 +470,7 @@ function defaultLoader({ ) } - if ( - !src.startsWith('/') && - (config.domains || experimentalRemotePatterns) - ) { + if (!src.startsWith('/') && (config.domains || config.remotePatterns)) { let parsedSrc: URL try { parsedSrc = new URL(src) @@ -486,7 +484,7 @@ function defaultLoader({ if (process.env.NODE_ENV !== 'test') { // We use dynamic require because this should only error in development const { hasMatch } = require('../../shared/lib/match-remote-pattern') - if (!hasMatch(config.domains, experimentalRemotePatterns, parsedSrc)) { + if (!hasMatch(config.domains, config.remotePatterns, parsedSrc)) { throw new Error( `Invalid src prop (${src}) on \`next/image\`, hostname "${parsedSrc.hostname}" is not configured under images in your \`next.config.js\`\n` + `See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host` @@ -524,11 +522,6 @@ export default function Image({ blurDataURL, ...all }: ImageProps) { - if (!experimentalFuture && process.env.NODE_ENV !== 'test') { - throw new Error( - `The "next/future/image" component is experimental and may be subject to breaking changes. To enable this experiment, please include \`experimental: { images: { allowFutureImage: true } }\` in your next.config.js file.` - ) - } const configContext = useContext(ImageConfigContext) const config: ImageConfig = useMemo(() => { const c = configEnv || configContext || imageConfigDefault @@ -594,7 +587,7 @@ export default function Image({ unoptimized = true isLazy = false } - if (experimentalUnoptimized) { + if (config.unoptimized) { unoptimized = true } diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index f4338203a4e3..9b385b4d7094 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -22,8 +22,6 @@ function normalizeSrc(src: string): string { return src[0] === '/' ? src.slice(1) : src } -const { experimentalRemotePatterns = [], experimentalUnoptimized } = - (process.env.__NEXT_IMAGE_OPTS as any) || {} const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete const loadedImageURLs = new Set() const allImgs = new Map< @@ -137,10 +135,7 @@ function defaultLoader({ ) } - if ( - !src.startsWith('/') && - (config.domains || experimentalRemotePatterns) - ) { + if (!src.startsWith('/') && (config.domains || config.remotePatterns)) { let parsedSrc: URL try { parsedSrc = new URL(src) @@ -154,7 +149,7 @@ function defaultLoader({ if (process.env.NODE_ENV !== 'test') { // We use dynamic require because this should only error in development const { hasMatch } = require('../shared/lib/match-remote-pattern') - if (!hasMatch(config.domains, experimentalRemotePatterns, parsedSrc)) { + if (!hasMatch(config.domains, config.remotePatterns, parsedSrc)) { throw new Error( `Invalid src prop (${src}) on \`next/image\`, hostname "${parsedSrc.hostname}" is not configured under images in your \`next.config.js\`\n` + `See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host` @@ -673,7 +668,7 @@ export default function Image({ if (typeof window !== 'undefined' && loadedImageURLs.has(src)) { isLazy = false } - if (experimentalUnoptimized) { + if (config.unoptimized) { unoptimized = true } diff --git a/packages/next/client/page-loader.ts b/packages/next/client/page-loader.ts index 5599b3106de4..644fffc2b905 100644 --- a/packages/next/client/page-loader.ts +++ b/packages/next/client/page-loader.ts @@ -1,5 +1,6 @@ import type { ComponentType } from 'react' import type { RouteLoader } from './route-loader' +import type { MiddlewareMatcher } from '../build/analysis/get-page-static-info' import { addBasePath } from './add-base-path' import { interpolateAs } from '../shared/lib/router/router' import getAssetPathFromRoute from '../shared/lib/router/utils/get-asset-path-from-route' @@ -11,7 +12,7 @@ import { createRouteLoader, getClientBuildManifest } from './route-loader' declare global { interface Window { - __DEV_MIDDLEWARE_MANIFEST?: { location?: string } + __DEV_MIDDLEWARE_MATCHERS?: MiddlewareMatcher[] __DEV_PAGES_MANIFEST?: { pages: string[] } __SSG_MANIFEST_CB?: () => void __SSG_MANIFEST?: Set @@ -30,7 +31,7 @@ export default class PageLoader { private assetPrefix: string private promisedSsgManifest: Promise> private promisedDevPagesManifest?: Promise - private promisedMiddlewareManifest?: Promise<{ location: string }> + private promisedMiddlewareMatchers?: Promise public routeLoader: RouteLoader @@ -80,32 +81,32 @@ export default class PageLoader { getMiddleware() { if (process.env.NODE_ENV === 'production') { - const middlewareRegex = process.env.__NEXT_MIDDLEWARE_REGEX - window.__MIDDLEWARE_MANIFEST = middlewareRegex - ? { location: middlewareRegex } + const middlewareMatchers = process.env.__NEXT_MIDDLEWARE_MATCHERS + window.__MIDDLEWARE_MATCHERS = middlewareMatchers + ? (middlewareMatchers as any as MiddlewareMatcher[]) : undefined - return window.__MIDDLEWARE_MANIFEST + return window.__MIDDLEWARE_MATCHERS } else { - if (window.__DEV_MIDDLEWARE_MANIFEST) { - return window.__DEV_MIDDLEWARE_MANIFEST + if (window.__DEV_MIDDLEWARE_MATCHERS) { + return window.__DEV_MIDDLEWARE_MATCHERS } else { - if (!this.promisedMiddlewareManifest) { + if (!this.promisedMiddlewareMatchers) { // TODO: Decide what should happen when fetching fails instead of asserting // @ts-ignore - this.promisedMiddlewareManifest = fetch( + this.promisedMiddlewareMatchers = fetch( `${this.assetPrefix}/_next/static/${this.buildId}/_devMiddlewareManifest.json` ) .then((res) => res.json()) - .then((manifest: { location?: string }) => { - window.__DEV_MIDDLEWARE_MANIFEST = manifest - return manifest + .then((matchers: MiddlewareMatcher[]) => { + window.__DEV_MIDDLEWARE_MATCHERS = matchers + return matchers }) .catch((err) => { console.log(`Failed to fetch _devMiddlewareManifest`, err) }) } // TODO Remove this assertion as this could be undefined - return this.promisedMiddlewareManifest! + return this.promisedMiddlewareMatchers! } } } diff --git a/packages/next/client/route-loader.ts b/packages/next/client/route-loader.ts index c540bef9ed58..22eb2ae151e0 100644 --- a/packages/next/client/route-loader.ts +++ b/packages/next/client/route-loader.ts @@ -1,4 +1,5 @@ import type { ComponentType } from 'react' +import type { MiddlewareMatcher } from '../build/analysis/get-page-static-info' import getAssetPathFromRoute from '../shared/lib/router/utils/get-asset-path-from-route' import { __unsafeCreateTrustedScriptURL } from './trusted-types' import { requestIdleCallback } from './request-idle-callback' @@ -13,7 +14,7 @@ declare global { interface Window { __BUILD_MANIFEST?: Record __BUILD_MANIFEST_CB?: Function - __MIDDLEWARE_MANIFEST?: { location: string } + __MIDDLEWARE_MATCHERS?: MiddlewareMatcher[] __MIDDLEWARE_MANIFEST_CB?: Function } } diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index 8751b05fcc4e..e5c56a41e6ae 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -322,8 +322,7 @@ export default async function exportApp( const { i18n, - images: { loader = 'default' }, - experimental, + images: { loader = 'default', unoptimized }, } = nextConfig if (i18n && !options.buildExport) { @@ -345,7 +344,7 @@ export default async function exportApp( if ( isNextImageImported && loader === 'default' && - !experimental?.images?.unoptimized && + !unoptimized && !hasNextSupport ) { throw new Error( diff --git a/packages/next/lib/eslint/hasEslintConfiguration.ts b/packages/next/lib/eslint/hasEslintConfiguration.ts index 113e72926192..6c600f545478 100644 --- a/packages/next/lib/eslint/hasEslintConfiguration.ts +++ b/packages/next/lib/eslint/hasEslintConfiguration.ts @@ -33,12 +33,10 @@ export async function hasEslintConfiguration( } return { ...configObject, exists: true } } else if (packageJsonConfig?.eslintConfig) { - if (Object.entries(packageJsonConfig?.eslintConfig).length === 0) { - return { - ...configObject, - emptyPkgJsonConfig: true, - } + if (Object.keys(packageJsonConfig?.eslintConfig).length) { + return { ...configObject, exists: true } } + return { ...configObject, emptyPkgJsonConfig: true } } return configObject } diff --git a/packages/next/lib/load-custom-routes.ts b/packages/next/lib/load-custom-routes.ts index f9b6ee393c55..0658b6fd1ee1 100644 --- a/packages/next/lib/load-custom-routes.ts +++ b/packages/next/lib/load-custom-routes.ts @@ -52,6 +52,12 @@ export type Redirect = { } ) +export type Middleware = { + source: string + locale?: false + has?: RouteHas[] +} + const allowedHasTypes = new Set(['header', 'cookie', 'query', 'host']) const namedGroupsRegex = /\(\?<([a-zA-Z][a-zA-Z0-9]*)>/g @@ -111,9 +117,9 @@ function checkHeader(route: Header): string[] { export type RouteType = 'rewrite' | 'redirect' | 'header' -function checkCustomRoutes( - routes: Redirect[] | Header[] | Rewrite[], - type: RouteType +export function checkCustomRoutes( + routes: Redirect[] | Header[] | Rewrite[] | Middleware[], + type: RouteType | 'middleware' ): void { if (!Array.isArray(routes)) { console.error( @@ -127,17 +133,20 @@ function checkCustomRoutes( let hadInvalidStatus = false let hadInvalidHas = false - const allowedKeys = new Set(['source', 'basePath', 'locale', 'has']) + const allowedKeys = new Set(['source', 'locale', 'has']) if (type === 'rewrite') { + allowedKeys.add('basePath') allowedKeys.add('destination') } if (type === 'redirect') { + allowedKeys.add('basePath') allowedKeys.add('statusCode') allowedKeys.add('permanent') allowedKeys.add('destination') } if (type === 'header') { + allowedKeys.add('basePath') allowedKeys.add('headers') } @@ -146,9 +155,11 @@ function checkCustomRoutes( console.error( `The route ${JSON.stringify( route - )} is not a valid object with \`source\` and \`${ - type === 'header' ? 'headers' : 'destination' - }\`` + )} is not a valid object with \`source\`${ + type !== 'middleware' + ? ` and \`${type === 'header' ? 'headers' : 'destination'}\`` + : '' + }` ) numInvalidRoutes++ continue @@ -175,7 +186,11 @@ function checkCustomRoutes( const invalidKeys = keys.filter((key) => !allowedKeys.has(key)) const invalidParts: string[] = [] - if (typeof route.basePath !== 'undefined' && route.basePath !== false) { + if ( + 'basePath' in route && + typeof route.basePath !== 'undefined' && + route.basePath !== false + ) { invalidParts.push('`basePath` must be undefined or false') } @@ -237,7 +252,7 @@ function checkCustomRoutes( if (type === 'header') { invalidParts.push(...checkHeader(route as Header)) - } else { + } else if (type !== 'middleware') { let _route = route as Rewrite | Redirect if (!_route.destination) { invalidParts.push('`destination` is missing') diff --git a/packages/next/package.json b/packages/next/package.json index 75b8d2b1e3a0..b0d5dd4c1477 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "12.2.6-canary.7", + "version": "12.2.6-canary.8", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -70,7 +70,7 @@ ] }, "dependencies": { - "@next/env": "12.2.6-canary.7", + "@next/env": "12.2.6-canary.8", "@swc/helpers": "0.4.3", "caniuse-lite": "^1.0.30001332", "postcss": "8.4.14", @@ -121,11 +121,11 @@ "@hapi/accept": "5.0.2", "@napi-rs/cli": "2.7.0", "@napi-rs/triples": "1.1.0", - "@next/polyfill-module": "12.2.6-canary.7", - "@next/polyfill-nomodule": "12.2.6-canary.7", - "@next/react-dev-overlay": "12.2.6-canary.7", - "@next/react-refresh-utils": "12.2.6-canary.7", - "@next/swc": "12.2.6-canary.7", + "@next/polyfill-module": "12.2.6-canary.8", + "@next/polyfill-nomodule": "12.2.6-canary.8", + "@next/react-dev-overlay": "12.2.6-canary.8", + "@next/react-refresh-utils": "12.2.6-canary.8", + "@next/swc": "12.2.6-canary.8", "@segment/ajv-human-errors": "2.1.2", "@taskr/clear": "1.1.0", "@taskr/esnext": "1.1.0", diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index c27393ec3a8c..b8ba02cc10f9 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -639,20 +639,6 @@ export async function renderToHTMLOrFlight( const isClientComponentModule = layoutOrPageMod && !layoutOrPageMod.hasOwnProperty('__next_rsc__') - // Only server components can have getServerSideProps / getStaticProps - // TODO-APP: friendly error with correct stacktrace. Potentially this can be part of the compiler instead. - if (isClientComponentModule) { - if (layoutOrPageMod.ssr) { - throw new Error( - 'getServerSideProps is not supported on Client Components' - ) - } - - if (layoutOrPageMod.ssg) { - throw new Error('getStaticProps is not supported on Client Components') - } - } - /** * The React Component to render. */ diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 56b986dcd2e2..29b732fcd3a1 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -5,6 +5,7 @@ import type { DynamicRoutes, PageChecker, Route } from './router' import type { FontManifest } from './font-utils' import type { LoadComponentsReturnType } from './load-components' import type { RouteMatch } from '../shared/lib/router/utils/route-matcher' +import type { MiddlewareRouteMatch } from '../shared/lib/router/utils/middleware-route-matcher' import type { Params } from '../shared/lib/router/utils/route-matcher' import type { NextConfig, NextConfigComplete } from './config-shared' import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta' @@ -68,6 +69,7 @@ import { getLocaleRedirect } from '../shared/lib/i18n/get-locale-redirect' import { getHostname } from '../shared/lib/get-hostname' import { parseUrl as parseUrlUtil } from '../shared/lib/router/utils/parse-url' import { getNextPathnameInfo } from '../shared/lib/router/utils/get-next-pathname-info' +import { MiddlewareMatcher } from '../build/analysis/get-page-static-info' export type FindComponentsResult = { components: LoadComponentsReturnType @@ -80,6 +82,12 @@ export interface RoutingItem { re?: RegExp } +export interface MiddlewareRoutingItem { + page: string + match: MiddlewareRouteMatch + matchers?: MiddlewareMatcher[] +} + export interface Options { /** * Object containing the configuration next.config.js diff --git a/packages/next/server/config-schema.ts b/packages/next/server/config-schema.ts index 56def7e1fdef..3cf35582ee5e 100644 --- a/packages/next/server/config-schema.ts +++ b/packages/next/server/config-schema.ts @@ -270,44 +270,6 @@ const configSchema = { gzipSize: { type: 'boolean', }, - images: { - additionalProperties: false, - properties: { - allowFutureImage: { - type: 'boolean', - }, - remotePatterns: { - items: { - additionalProperties: false, - properties: { - hostname: { - minLength: 1, - type: 'string', - }, - pathname: { - minLength: 1, - type: 'string', - }, - port: { - minLength: 1, - type: 'string', - }, - protocol: { - // automatic typing doesn't like enum - enum: ['http', 'https'] as any, - type: 'string', - }, - }, - type: 'object', - }, - type: 'array', - }, - unoptimized: { - type: 'boolean', - }, - }, - type: 'object', - }, incrementalCacheHandlerPath: { type: 'string', }, @@ -487,6 +449,35 @@ const configSchema = { images: { additionalProperties: false, properties: { + remotePatterns: { + items: { + additionalProperties: false, + properties: { + hostname: { + minLength: 1, + type: 'string', + }, + pathname: { + minLength: 1, + type: 'string', + }, + port: { + minLength: 1, + type: 'string', + }, + protocol: { + // automatic typing doesn't like enum + enum: ['http', 'https'] as any, + type: 'string', + }, + }, + type: 'object', + }, + type: 'array', + }, + unoptimized: { + type: 'boolean', + }, contentSecurityPolicy: { minLength: 1, type: 'string', diff --git a/packages/next/server/config-shared.ts b/packages/next/server/config-shared.ts index fe2238449320..61d92edc6bd0 100644 --- a/packages/next/server/config-shared.ts +++ b/packages/next/server/config-shared.ts @@ -5,7 +5,6 @@ import { ImageConfig, ImageConfigComplete, imageConfigDefault, - RemotePattern, } from '../shared/lib/image-config' import { ServerRuntime } from 'next/types' @@ -117,11 +116,6 @@ export interface ExperimentalConfig { fullySpecified?: boolean urlImports?: NonNullable['buildHttp'] outputFileTracingRoot?: string - images?: { - remotePatterns?: RemotePattern[] - unoptimized?: boolean - allowFutureImage?: boolean - } modularizeImports?: Record< string, { @@ -572,9 +566,6 @@ export const defaultConfig: NextConfig = { serverComponents: false, fullySpecified: false, outputFileTracingRoot: process.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT || '', - images: { - remotePatterns: [], - }, swcTraceProfiling: false, forceSwcTransforms: false, swcPlugins: undefined, diff --git a/packages/next/server/config.ts b/packages/next/server/config.ts index 70730acb1ab5..e5c1260a8d10 100644 --- a/packages/next/server/config.ts +++ b/packages/next/server/config.ts @@ -256,7 +256,7 @@ function assignDefaults(userConfig: { [key: string]: any }) { } } - const remotePatterns = result.experimental?.images?.remotePatterns + const remotePatterns = result?.images?.remotePatterns if (remotePatterns) { if (!Array.isArray(remotePatterns)) { throw new Error( @@ -437,7 +437,7 @@ function assignDefaults(userConfig: { [key: string]: any }) { ) } - const unoptimized = result.experimental?.images?.unoptimized + const unoptimized = result?.images?.unoptimized if ( typeof unoptimized !== 'undefined' && typeof unoptimized !== 'boolean' diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index b6c66e8c58c9..7a96c47dbef0 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -9,7 +9,8 @@ import type { ParsedUrlQuery } from 'querystring' import type { Server as HTTPServer } from 'http' import type { UrlWithParsedQuery } from 'url' import type { BaseNextRequest, BaseNextResponse } from '../base-http' -import type { RoutingItem } from '../base-server' +import type { MiddlewareRoutingItem, RoutingItem } from '../base-server' +import type { MiddlewareMatcher } from '../../build/analysis/get-page-static-info' import crypto from 'crypto' import fs from 'fs' @@ -34,6 +35,7 @@ import { } from '../../shared/lib/constants' import Server, { WrappedBuildError } from '../next-server' import { getRouteMatcher } from '../../shared/lib/router/utils/route-matcher' +import { getMiddlewareRouteMatcher } from '../../shared/lib/router/utils/middleware-route-matcher' import { normalizePagePath } from '../../shared/lib/page-path/normalize-page-path' import { absolutePathToPage } from '../../shared/lib/page-path/absolute-path-to-page' import Router from '../router' @@ -60,10 +62,7 @@ import { } from 'next/dist/compiled/@next/react-dev-overlay/dist/middleware' import * as Log from '../../build/output/log' import isError, { getProperError } from '../../lib/is-error' -import { - getMiddlewareRegex, - getRouteRegex, -} from '../../shared/lib/router/utils/route-regex' +import { getRouteRegex } from '../../shared/lib/router/utils/route-regex' import { getSortedRoutes, isDynamicRoute } from '../../shared/lib/router/utils' import { runDependingOnPageType } from '../../build/entries' import { NodeNextResponse, NodeNextRequest } from '../base-http/node' @@ -106,7 +105,7 @@ export default class DevServer extends Server { private pagesDir?: string private appDir?: string private actualMiddlewareFile?: string - private middleware?: RoutingItem + private middleware?: MiddlewareRoutingItem private edgeFunctions?: RoutingItem[] private verifyingTypeScript?: boolean private usingTypeScript?: boolean @@ -309,11 +308,12 @@ export default class DevServer extends Server { let enabledTypeScript = this.usingTypeScript wp.on('aggregated', async () => { - let middlewareMatcher: RegExp | undefined + let middlewareMatchers: MiddlewareMatcher[] | undefined const routedPages: string[] = [] const knownFiles = wp.getTimeInfoEntries() const appPaths: Record = {} const edgeRoutesSet = new Set() + let envChange = false let tsconfigChange = false @@ -373,9 +373,9 @@ export default class DevServer extends Server { if (isMiddlewareFile(rootFile)) { this.actualMiddlewareFile = rootFile - middlewareMatcher = - staticInfo.middleware?.pathMatcher || new RegExp('.*') - edgeRoutesSet.add('/') + middlewareMatchers = staticInfo.middleware?.matchers || [ + { regexp: '.*' }, + ] continue } @@ -543,31 +543,29 @@ export default class DevServer extends Server { } this.appPathRoutes = appPaths - this.edgeFunctions = [] const edgeRoutes = Array.from(edgeRoutesSet) - getSortedRoutes(edgeRoutes).forEach((page) => { - let appPath = this.getOriginalAppPath(page) + this.edgeFunctions = getSortedRoutes(edgeRoutes).map((page) => { + const appPath = this.getOriginalAppPath(page) if (typeof appPath === 'string') { page = appPath } - const isRootMiddleware = page === '/' && !!middlewareMatcher - - const middlewareRegex = isRootMiddleware - ? { re: middlewareMatcher!, groups: {} } - : getMiddlewareRegex(page, { catchAll: false }) - const routeItem = { - match: getRouteMatcher(middlewareRegex), + const edgeRegex = getRouteRegex(page) + return { + match: getRouteMatcher(edgeRegex), page, - re: middlewareRegex.re, - } - if (isRootMiddleware) { - this.middleware = routeItem - } else { - this.edgeFunctions!.push(routeItem) + re: edgeRegex.re, } }) + this.middleware = middlewareMatchers + ? { + match: getMiddlewareRouteMatcher(middlewareMatchers), + page: '/', + matchers: middlewareMatchers, + } + : undefined + try { // we serve a separate manifest with all pages for the client in // dev mode so that we can match a page after a rewrite on the client @@ -843,6 +841,7 @@ export default class DevServer extends Server { response: BaseNextResponse parsedUrl: ParsedUrl parsed: UrlWithParsedQuery + middlewareList: MiddlewareRoutingItem[] }) { try { const result = await super.runMiddleware({ @@ -1174,17 +1173,7 @@ export default class DevServer extends Server { fn: async (_req, res) => { res.statusCode = 200 res.setHeader('Content-Type', 'application/json; charset=utf-8') - res - .body( - JSON.stringify( - this.middleware - ? { - location: this.middleware.re!.source, - } - : {} - ) - ) - .send() + res.body(JSON.stringify(this.getMiddleware()?.matchers ?? [])).send() return { finished: true, } diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index 68ca350f9d3b..ea89c37615a6 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -161,7 +161,7 @@ export class ImageOptimizerCache { minimumCacheTTL = 60, formats = ['image/webp'], } = imageData - const remotePatterns = nextConfig.experimental.images?.remotePatterns || [] + const remotePatterns = nextConfig.images?.remotePatterns || [] const { url, w, q } = query let href: string diff --git a/packages/next/server/lib/utils.ts b/packages/next/server/lib/utils.ts index 578992683c8e..b149393d25ad 100644 --- a/packages/next/server/lib/utils.ts +++ b/packages/next/server/lib/utils.ts @@ -1,3 +1,5 @@ +import type arg from 'next/dist/compiled/arg/index.js' + export function printAndExit(message: string, code = 1) { if (code === 0) { console.log(message) @@ -12,3 +14,16 @@ export function getNodeOptionsWithoutInspect() { const NODE_INSPECT_RE = /--inspect(-brk)?(=\S+)?( |$)/ return (process.env.NODE_OPTIONS || '').replace(NODE_INSPECT_RE, '') } + +export function getPort(args: arg.Result): number { + if (typeof args['--port'] === 'number') { + return args['--port'] + } + + const parsed = process.env.PORT && parseInt(process.env.PORT, 10) + if (typeof parsed === 'number' && !Number.isNaN(parsed)) { + return parsed + } + + return 3000 +} diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index bda0394519c5..5e39ac360fc7 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -22,6 +22,7 @@ import type { Params, RouteMatch, } from '../shared/lib/router/utils/route-matcher' +import type { MiddlewareRouteMatch } from '../shared/lib/router/utils/middleware-route-matcher' import type { NextConfig } from './config-shared' import type { DynamicRoutes, PageChecker } from './router' @@ -71,6 +72,7 @@ import BaseServer, { Options, FindComponentsResult, prepareServerlessUrl, + MiddlewareRoutingItem, RoutingItem, NoFallbackError, RequestContext, @@ -86,6 +88,7 @@ import { relativizeURL } from '../shared/lib/router/utils/relativize-url' import { prepareDestination } from '../shared/lib/router/utils/prepare-destination' import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path' import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher' +import { getMiddlewareRouteMatcher } from '../shared/lib/router/utils/middleware-route-matcher' import { loadEnvConfig } from '@next/env' import { getCustomRoute, stringifyQuery } from './server-route-utils' import { urlQueryToSearchParams } from '../shared/lib/router/utils/querystring' @@ -124,28 +127,90 @@ export interface NodeRequestHandler { const MiddlewareMatcherCache = new WeakMap< MiddlewareManifest['middleware'][string], + MiddlewareRouteMatch +>() + +const EdgeMatcherCache = new WeakMap< + MiddlewareManifest['functions'][string], RouteMatch >() function getMiddlewareMatcher( info: MiddlewareManifest['middleware'][string] -): RouteMatch { +): MiddlewareRouteMatch { const stored = MiddlewareMatcherCache.get(info) if (stored) { return stored } - if (typeof info.regexp !== 'string' || !info.regexp) { + if (!Array.isArray(info.matchers)) { throw new Error( - `Invariant: invalid regexp for middleware ${JSON.stringify(info)}` + `Invariant: invalid matchers for middleware ${JSON.stringify(info)}` ) } - const matcher = getRouteMatcher({ re: new RegExp(info.regexp), groups: {} }) + const matcher = getMiddlewareRouteMatcher(info.matchers) MiddlewareMatcherCache.set(info, matcher) return matcher } +/** + * Hardcoded every possible error status code that could be thrown by "serveStatic" method + * This is done by searching "this.error" inside "send" module's source code: + * https://github.com/pillarjs/send/blob/master/index.js + * https://github.com/pillarjs/send/blob/develop/index.js + */ +const POSSIBLE_ERROR_CODE_FROM_SERVE_STATIC = new Set([ + // send module will throw 500 when header is already sent or fs.stat error happens + // https://github.com/pillarjs/send/blob/53f0ab476145670a9bdd3dc722ab2fdc8d358fc6/index.js#L392 + // Note: we will use Next.js built-in 500 page to handle 500 errors + // 500, + + // send module will throw 404 when file is missing + // https://github.com/pillarjs/send/blob/53f0ab476145670a9bdd3dc722ab2fdc8d358fc6/index.js#L421 + // Note: we will use Next.js built-in 404 page to handle 404 errors + // 404, + + // send module will throw 403 when redirecting to a directory without enabling directory listing + // https://github.com/pillarjs/send/blob/53f0ab476145670a9bdd3dc722ab2fdc8d358fc6/index.js#L484 + // Note: Next.js throws a different error (without status code) for directory listing + // 403, + + // send module will throw 400 when fails to normalize the path + // https://github.com/pillarjs/send/blob/53f0ab476145670a9bdd3dc722ab2fdc8d358fc6/index.js#L520 + 400, + + // send module will throw 412 with conditional GET request + // https://github.com/pillarjs/send/blob/53f0ab476145670a9bdd3dc722ab2fdc8d358fc6/index.js#L632 + 412, + + // send module will throw 416 when range is not satisfiable + // https://github.com/pillarjs/send/blob/53f0ab476145670a9bdd3dc722ab2fdc8d358fc6/index.js#L669 + 416, +]) + +function getEdgeMatcher( + info: MiddlewareManifest['functions'][string] +): RouteMatch { + const stored = EdgeMatcherCache.get(info) + if (stored) { + return stored + } + + if (!Array.isArray(info.matchers) || info.matchers.length !== 1) { + throw new Error( + `Invariant: invalid matchers for middleware ${JSON.stringify(info)}` + ) + } + + const matcher = getRouteMatcher({ + re: new RegExp(info.matchers[0].regexp), + groups: {}, + }) + EdgeMatcherCache.set(info, matcher) + return matcher +} + export default class NextNodeServer extends BaseServer { private imageResponseCache?: ResponseCache @@ -1349,8 +1414,11 @@ export default class NextNodeServer extends BaseServer { const err = error as Error & { code?: string; statusCode?: number } if (err.code === 'ENOENT' || err.statusCode === 404) { this.render404(req, res, parsedUrl) - } else if (err.statusCode === 412) { - res.statusCode = 412 + } else if ( + typeof err.statusCode === 'number' && + POSSIBLE_ERROR_CODE_FROM_SERVE_STATIC.has(err.statusCode) + ) { + res.statusCode = err.statusCode return this.renderError(err, req, res, path) } else { throw err @@ -1508,7 +1576,7 @@ export default class NextNodeServer extends BaseServer { } /** Returns the middleware routing item if there is one. */ - protected getMiddleware(): RoutingItem | undefined { + protected getMiddleware(): MiddlewareRoutingItem | undefined { const manifest = this.getMiddlewareManifest() const middleware = manifest?.middleware?.['/'] if (!middleware) { @@ -1528,7 +1596,7 @@ export default class NextNodeServer extends BaseServer { } return Object.keys(manifest.functions).map((page) => ({ - match: getMiddlewareMatcher(manifest.functions[page]), + match: getEdgeMatcher(manifest.functions[page]), page, })) } @@ -1654,10 +1722,6 @@ export default class NextNodeServer extends BaseServer { } } - const allHeaders = new Headers() - let result: FetchEventResult | null = null - const method = (params.request.method || 'GET').toUpperCase() - const middleware = this.getMiddleware() if (!middleware) { return { finished: false } @@ -1666,52 +1730,54 @@ export default class NextNodeServer extends BaseServer { return { finished: false } } - if (middleware && middleware.match(normalizedPathname)) { - await this.ensureMiddleware() - const middlewareInfo = this.getEdgeFunctionInfo({ - page: middleware.page, - middleware: true, - }) + await this.ensureMiddleware() + const middlewareInfo = this.getEdgeFunctionInfo({ + page: middleware.page, + middleware: true, + }) - if (!middlewareInfo) { - throw new MiddlewareNotFoundError() - } + if (!middlewareInfo) { + throw new MiddlewareNotFoundError() + } - result = await run({ - distDir: this.distDir, - name: middlewareInfo.name, - paths: middlewareInfo.paths, - env: middlewareInfo.env, - edgeFunctionEntry: middlewareInfo, - request: { - headers: params.request.headers, - method, - nextConfig: { - basePath: this.nextConfig.basePath, - i18n: this.nextConfig.i18n, - trailingSlash: this.nextConfig.trailingSlash, - }, - url: url, - page: page, - body: getRequestMeta(params.request, '__NEXT_CLONABLE_BODY'), + const method = (params.request.method || 'GET').toUpperCase() + + const result = await run({ + distDir: this.distDir, + name: middlewareInfo.name, + paths: middlewareInfo.paths, + env: middlewareInfo.env, + edgeFunctionEntry: middlewareInfo, + request: { + headers: params.request.headers, + method, + nextConfig: { + basePath: this.nextConfig.basePath, + i18n: this.nextConfig.i18n, + trailingSlash: this.nextConfig.trailingSlash, }, - useCache: !this.nextConfig.experimental.runtime, - onWarning: params.onWarning, - }) + url: url, + page: page, + body: getRequestMeta(params.request, '__NEXT_CLONABLE_BODY'), + }, + useCache: !this.nextConfig.experimental.runtime, + onWarning: params.onWarning, + }) - for (let [key, value] of result.response.headers) { - if (key !== 'x-middleware-next') { - allHeaders.append(key, value) - } - } + const allHeaders = new Headers() - if (!this.renderOpts.dev) { - result.waitUntil.catch((error) => { - console.error(`Uncaught: middleware waitUntil errored`, error) - }) + for (let [key, value] of result.response.headers) { + if (key !== 'x-middleware-next') { + allHeaders.append(key, value) } } + if (!this.renderOpts.dev) { + result.waitUntil.catch((error) => { + console.error(`Uncaught: middleware waitUntil errored`, error) + }) + } + if (!result) { this.render404(params.request, params.response, params.parsed) return { finished: true } @@ -1751,7 +1817,7 @@ export default class NextNodeServer extends BaseServer { const normalizedPathname = removeTrailingSlash( parsed.pathname || '' ) - if (!middleware.match(normalizedPathname)) { + if (!middleware.match(normalizedPathname, req, parsedUrl.query)) { return { finished: false } } diff --git a/packages/next/shared/lib/image-config.ts b/packages/next/shared/lib/image-config.ts index d65b538522a1..cfa172dc0b58 100644 --- a/packages/next/shared/lib/image-config.ts +++ b/packages/next/shared/lib/image-config.ts @@ -74,6 +74,12 @@ export type ImageConfigComplete = { /** @see [Dangerously Allow SVG](https://nextjs.org/docs/api-reference/next/image#dangerously-allow-svg) */ contentSecurityPolicy: string + + /** @see [Remote Patterns](https://nextjs.org/docs/api-reference/next/image#remote-patterns) */ + remotePatterns: RemotePattern[] + + /** @see [Unoptimized](https://nextjs.org/docs/api-reference/next/image#unoptimized) */ + unoptimized: boolean } export type ImageConfig = Partial @@ -89,4 +95,6 @@ export const imageConfigDefault: ImageConfigComplete = { formats: ['image/webp'], dangerouslyAllowSVG: false, contentSecurityPolicy: `script-src 'none'; frame-src 'none'; sandbox;`, + remotePatterns: [], + unoptimized: false, } diff --git a/packages/next/shared/lib/router/router.ts b/packages/next/shared/lib/router/router.ts index 2880d35a3393..93d6b285e164 100644 --- a/packages/next/shared/lib/router/router.ts +++ b/packages/next/shared/lib/router/router.ts @@ -91,21 +91,27 @@ interface MiddlewareEffectParams { router: Router } -function matchesMiddleware( +export async function matchesMiddleware( options: MiddlewareEffectParams ): Promise { - return Promise.resolve(options.router.pageLoader.getMiddleware()).then( - (middleware) => { - const { pathname: asPathname } = parsePath(options.asPath) - const cleanedAs = hasBasePath(asPathname) - ? removeBasePath(asPathname) - : asPathname - - const regex = middleware?.location - return ( - !!regex && new RegExp(regex).test(addLocale(cleanedAs, options.locale)) - ) - } + const matchers = await Promise.resolve( + options.router.pageLoader.getMiddleware() + ) + if (!matchers) return false + + const { pathname: asPathname } = parsePath(options.asPath) + // remove basePath first since path prefix has to be in the order of `/${basePath}/${locale}` + const cleanedAs = hasBasePath(asPathname) + ? removeBasePath(asPathname) + : asPathname + const asWithBasePathAndLocale = addBasePath( + addLocale(cleanedAs, options.locale) + ) + + // Check only path match on client. Matching "has" should be done on server + // where we can access more info such as headers, HttpOnly cookie, etc. + return matchers.some((m) => + new RegExp(m.regexp).test(asWithBasePathAndLocale) ) } diff --git a/packages/next/shared/lib/router/utils/middleware-route-matcher.ts b/packages/next/shared/lib/router/utils/middleware-route-matcher.ts new file mode 100644 index 000000000000..87465e3d8a63 --- /dev/null +++ b/packages/next/shared/lib/router/utils/middleware-route-matcher.ts @@ -0,0 +1,40 @@ +import type { BaseNextRequest } from '../../../../server/base-http' +import type { MiddlewareMatcher } from '../../../../build/analysis/get-page-static-info' +import type { Params } from './route-matcher' +import { matchHas } from './prepare-destination' + +export interface MiddlewareRouteMatch { + ( + pathname: string | null | undefined, + request: BaseNextRequest, + query: Params + ): boolean +} + +export function getMiddlewareRouteMatcher( + matchers: MiddlewareMatcher[] +): MiddlewareRouteMatch { + return ( + pathname: string | null | undefined, + req: BaseNextRequest, + query: Params + ) => { + for (const matcher of matchers) { + const routeMatch = new RegExp(matcher.regexp).exec(pathname!) + if (!routeMatch) { + continue + } + + if (matcher.has) { + const hasParams = matchHas(req, matcher.has, query) + if (!hasParams) { + continue + } + } + + return true + } + + return false + } +} diff --git a/packages/next/shared/lib/router/utils/route-regex.ts b/packages/next/shared/lib/router/utils/route-regex.ts index 0f6069555087..133ed1d994d3 100644 --- a/packages/next/shared/lib/router/utils/route-regex.ts +++ b/packages/next/shared/lib/router/utils/route-regex.ts @@ -144,36 +144,7 @@ export function getNamedRouteRegex(normalizedRoute: string) { } /** - * From a middleware normalized route this function generates a regular - * expression for it. Temporarly we are using this to generate Edge Function - * routes too. In such cases the route should not include a trailing catch-all. - * For these cases the option `catchAll` should be set to false. - */ -export function getMiddlewareRegex( - normalizedRoute: string, - options?: { - catchAll?: boolean - } -): RouteRegex { - const { parameterizedRoute, groups } = getParametrizedRoute(normalizedRoute) - const { catchAll = true } = options ?? {} - if (parameterizedRoute === '/') { - let catchAllRegex = catchAll ? '.*' : '' - return { - groups: {}, - re: new RegExp(`^/${catchAllRegex}$`), - } - } - - let catchAllGroupedRegex = catchAll ? '(?:(/.*)?)' : '' - return { - groups: groups, - re: new RegExp(`^${parameterizedRoute}${catchAllGroupedRegex}$`), - } -} - -/** - * A server version for getMiddlewareRegex that generates a named regexp. + * Generates a named regexp. * This is intended to be using for build time only. */ export function getNamedMiddlewareRegex( diff --git a/packages/next/telemetry/events/version.ts b/packages/next/telemetry/events/version.ts index 8810773b5249..8a308e45a849 100644 --- a/packages/next/telemetry/events/version.ts +++ b/packages/next/telemetry/events/version.ts @@ -81,7 +81,7 @@ export function eventCliSession( return [] } - const { images, i18n, experimental } = nextConfig || {} + const { images, i18n } = nextConfig || {} const payload: EventCliSessionStarted = { nextVersion: process.env.__NEXT_VERSION, @@ -95,15 +95,15 @@ export function eventCliSession( hasWebpackConfig: typeof nextConfig?.webpack === 'function', hasBabelConfig: hasBabelConfig(dir), imageEnabled: !!images, - imageFutureEnabled: !!experimental.images?.allowFutureImage, + imageFutureEnabled: !!images, basePathEnabled: !!nextConfig?.basePath, i18nEnabled: !!i18n, locales: i18n?.locales ? i18n.locales.join(',') : null, localeDomainsCount: i18n?.domains ? i18n.domains.length : null, localeDetectionEnabled: !i18n ? null : i18n.localeDetection !== false, imageDomainsCount: images?.domains ? images.domains.length : null, - imageRemotePatternsCount: experimental?.images?.remotePatterns - ? experimental.images.remotePatterns.length + imageRemotePatternsCount: images?.remotePatterns + ? images.remotePatterns.length : null, imageSizes: images?.imageSizes ? images.imageSizes.join(',') : null, imageLoader: images?.loader, diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index b31065e23675..4955d77cb12a 100644 --- a/packages/react-dev-overlay/package.json +++ b/packages/react-dev-overlay/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-dev-overlay", - "version": "12.2.6-canary.7", + "version": "12.2.6-canary.8", "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 9cd9abe590f0..7147d242b639 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "12.2.6-canary.7", + "version": "12.2.6-canary.8", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dfe5387e501f..8489ff56bb83 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -363,7 +363,7 @@ importers: packages/eslint-config-next: specifiers: - '@next/eslint-plugin-next': 12.2.6-canary.7 + '@next/eslint-plugin-next': 12.2.6-canary.8 '@rushstack/eslint-patch': ^1.1.3 '@typescript-eslint/parser': ^5.21.0 eslint-import-resolver-node: ^0.3.6 @@ -419,12 +419,12 @@ importers: '@hapi/accept': 5.0.2 '@napi-rs/cli': 2.7.0 '@napi-rs/triples': 1.1.0 - '@next/env': 12.2.6-canary.7 - '@next/polyfill-module': 12.2.6-canary.7 - '@next/polyfill-nomodule': 12.2.6-canary.7 - '@next/react-dev-overlay': 12.2.6-canary.7 - '@next/react-refresh-utils': 12.2.6-canary.7 - '@next/swc': 12.2.6-canary.7 + '@next/env': 12.2.6-canary.8 + '@next/polyfill-module': 12.2.6-canary.8 + '@next/polyfill-nomodule': 12.2.6-canary.8 + '@next/react-dev-overlay': 12.2.6-canary.8 + '@next/react-refresh-utils': 12.2.6-canary.8 + '@next/swc': 12.2.6-canary.8 '@segment/ajv-human-errors': 2.1.2 '@swc/helpers': 0.4.3 '@taskr/clear': 1.1.0 diff --git a/test/e2e/app-dir/app/app/client-with-errors/get-server-side-props/page.client.js b/test/e2e/app-dir/app/app/client-with-errors/get-server-side-props/page.client.js index 8b4ae46ca94b..899f4d9b0697 100644 --- a/test/e2e/app-dir/app/app/client-with-errors/get-server-side-props/page.client.js +++ b/test/e2e/app-dir/app/app/client-with-errors/get-server-side-props/page.client.js @@ -1,7 +1,5 @@ -export function getServerSideProps() { - return { props: {} } -} +// export function getServerSideProps() { { props: {} } } export default function Page() { - return null + return 'client-gssp' } diff --git a/test/e2e/app-dir/app/app/client-with-errors/get-static-props/page.client.js b/test/e2e/app-dir/app/app/client-with-errors/get-static-props/page.client.js index 5acfaee644a8..717782b1a38e 100644 --- a/test/e2e/app-dir/app/app/client-with-errors/get-static-props/page.client.js +++ b/test/e2e/app-dir/app/app/client-with-errors/get-static-props/page.client.js @@ -1,7 +1,5 @@ -export function getStaticProps() { - return { props: {} } -} +// export function getStaticProps() { return { props: {} }} export default function Page() { - return null + return 'client-gsp' } diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index e22823c3d79d..51d41c4931d9 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -1,6 +1,6 @@ import { createNext, FileRef } from 'e2e-utils' import { NextInstance } from 'test/lib/next-modes/base' -import { fetchViaHTTP, renderViaHTTP, waitFor } from 'next-test-utils' +import { check, fetchViaHTTP, renderViaHTTP, waitFor } from 'next-test-utils' import path from 'path' import cheerio from 'cheerio' import webdriver from 'next-webdriver' @@ -1023,31 +1023,64 @@ describe('app dir', () => { }) }) - it('should throw an error when getStaticProps is used', async () => { - const res = await fetchViaHTTP( - next.url, - '/client-with-errors/get-static-props' - ) - expect(res.status).toBe(500) - expect(await res.text()).toContain( - isDev - ? 'getStaticProps is not supported on Client Components' - : 'Internal Server Error' - ) - }) + if (isDev) { + it('should throw an error when getServerSideProps is used', async () => { + const pageFile = + 'app/client-with-errors/get-server-side-props/page.client.js' + const content = await next.readFile(pageFile) + const uncomment = content.replace( + '// export function getServerSideProps', + 'export function getServerSideProps' + ) + await next.patchFile(pageFile, uncomment) + const res = await fetchViaHTTP( + next.url, + '/client-with-errors/get-server-side-props' + ) + await next.patchFile(pageFile, content) - it('should throw an error when getServerSideProps is used', async () => { - const res = await fetchViaHTTP( - next.url, - '/client-with-errors/get-server-side-props' - ) - expect(res.status).toBe(500) - expect(await res.text()).toContain( - isDev - ? 'getServerSideProps is not supported on Client Components' - : 'Internal Server Error' - ) - }) + await check(async () => { + const { status } = await fetchViaHTTP( + next.url, + '/client-with-errors/get-server-side-props' + ) + return status + }, /200/) + + expect(res.status).toBe(500) + expect(await res.text()).toContain( + 'getServerSideProps is not supported in client components' + ) + }) + + it('should throw an error when getStaticProps is used', async () => { + const pageFile = + 'app/client-with-errors/get-static-props/page.client.js' + const content = await next.readFile(pageFile) + const uncomment = content.replace( + '// export function getStaticProps', + 'export function getStaticProps' + ) + await next.patchFile(pageFile, uncomment) + const res = await fetchViaHTTP( + next.url, + '/client-with-errors/get-static-props' + ) + await next.patchFile(pageFile, content) + await check(async () => { + const { status } = await fetchViaHTTP( + next.url, + '/client-with-errors/get-static-props' + ) + return status + }, /200/) + + expect(res.status).toBe(500) + expect(await res.text()).toContain( + 'getStaticProps is not supported in client components' + ) + }) + } }) describe('css support', () => { diff --git a/test/e2e/middleware-custom-matchers-basepath/app/middleware.js b/test/e2e/middleware-custom-matchers-basepath/app/middleware.js new file mode 100644 index 000000000000..0760a6e4ae2a --- /dev/null +++ b/test/e2e/middleware-custom-matchers-basepath/app/middleware.js @@ -0,0 +1,17 @@ +import { NextResponse } from 'next/server' + +export default function middleware(request) { + const nextUrl = request.nextUrl.clone() + nextUrl.pathname = '/' + const res = NextResponse.rewrite(nextUrl) + res.headers.set('X-From-Middleware', 'true') + return res +} + +export const config = { + matcher: [ + { + source: '/hello', + }, + ], +} diff --git a/test/e2e/middleware-custom-matchers-basepath/app/next.config.js b/test/e2e/middleware-custom-matchers-basepath/app/next.config.js new file mode 100644 index 000000000000..ee95502b605d --- /dev/null +++ b/test/e2e/middleware-custom-matchers-basepath/app/next.config.js @@ -0,0 +1,3 @@ +module.exports = { + basePath: '/docs', +} diff --git a/test/e2e/middleware-custom-matchers-basepath/app/pages/index.js b/test/e2e/middleware-custom-matchers-basepath/app/pages/index.js new file mode 100644 index 000000000000..accd5b17adff --- /dev/null +++ b/test/e2e/middleware-custom-matchers-basepath/app/pages/index.js @@ -0,0 +1,14 @@ +export default (props) => ( + <> +

home

+
{props.fromMiddleware}
+ +) + +export async function getServerSideProps({ res }) { + return { + props: { + fromMiddleware: res.getHeader('x-from-middleware') || null, + }, + } +} diff --git a/test/e2e/middleware-custom-matchers-basepath/app/pages/routes.js b/test/e2e/middleware-custom-matchers-basepath/app/pages/routes.js new file mode 100644 index 000000000000..4e8bb9d0cb80 --- /dev/null +++ b/test/e2e/middleware-custom-matchers-basepath/app/pages/routes.js @@ -0,0 +1,11 @@ +import Link from 'next/link' + +export default (props) => ( + +) diff --git a/test/e2e/middleware-custom-matchers-basepath/test/index.test.ts b/test/e2e/middleware-custom-matchers-basepath/test/index.test.ts new file mode 100644 index 000000000000..ff367b978700 --- /dev/null +++ b/test/e2e/middleware-custom-matchers-basepath/test/index.test.ts @@ -0,0 +1,56 @@ +/* eslint-env jest */ +/* eslint-disable jest/no-standalone-expect */ + +import { join } from 'path' +import webdriver from 'next-webdriver' +import { fetchViaHTTP } from 'next-test-utils' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' + +const itif = (condition: boolean) => (condition ? it : it.skip) + +const isModeDeploy = process.env.NEXT_TEST_MODE === 'deploy' + +describe('Middleware custom matchers basePath', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: new FileRef(join(__dirname, '../app')), + }) + }) + afterAll(() => next.destroy()) + + // FIXME + // See https://linear.app/vercel/issue/EC-170/middleware-rewrite-of-nextjs-with-basepath-does-not-work-on-vercel + itif(!isModeDeploy)('should match', async () => { + for (const path of [ + '/docs/hello', + `/docs/_next/data/${next.buildId}/hello.json`, + ]) { + const res = await fetchViaHTTP(next.url, path) + expect(res.status).toBe(200) + expect(res.headers.get('x-from-middleware')).toBeDefined() + } + }) + + it.each(['/hello', '/invalid/docs/hello'])( + 'should not match', + async (path) => { + const res = await fetchViaHTTP(next.url, path) + expect(res.status).toBe(404) + } + ) + + // FIXME: + // See https://linear.app/vercel/issue/EC-160/header-value-set-on-middleware-is-not-propagated-on-client-request-of + itif(!isModeDeploy)('should match has query on client routing', async () => { + const browser = await webdriver(next.url, '/docs/routes') + await browser.eval('window.__TEST_NO_RELOAD = true') + await browser.elementById('hello').click() + const fromMiddleware = await browser.elementById('from-middleware').text() + expect(fromMiddleware).toBe('true') + const noReload = await browser.eval('window.__TEST_NO_RELOAD') + expect(noReload).toBe(true) + }) +}) diff --git a/test/e2e/middleware-custom-matchers-i18n/app/middleware.js b/test/e2e/middleware-custom-matchers-i18n/app/middleware.js new file mode 100644 index 000000000000..bdd67e48df85 --- /dev/null +++ b/test/e2e/middleware-custom-matchers-i18n/app/middleware.js @@ -0,0 +1,21 @@ +import { NextResponse } from 'next/server' + +export default function middleware(request) { + const nextUrl = request.nextUrl.clone() + nextUrl.pathname = '/' + const res = NextResponse.rewrite(nextUrl) + res.headers.set('X-From-Middleware', 'true') + return res +} + +export const config = { + matcher: [ + { + source: '/hello', + }, + { + source: '/nl-NL/about', + locale: false, + }, + ], +} diff --git a/test/e2e/middleware-custom-matchers-i18n/app/next.config.js b/test/e2e/middleware-custom-matchers-i18n/app/next.config.js new file mode 100644 index 000000000000..97c6addb6f92 --- /dev/null +++ b/test/e2e/middleware-custom-matchers-i18n/app/next.config.js @@ -0,0 +1,6 @@ +module.exports = { + i18n: { + locales: ['en', 'nl-NL'], + defaultLocale: 'en', + }, +} diff --git a/test/e2e/middleware-custom-matchers-i18n/app/pages/index.js b/test/e2e/middleware-custom-matchers-i18n/app/pages/index.js new file mode 100644 index 000000000000..accd5b17adff --- /dev/null +++ b/test/e2e/middleware-custom-matchers-i18n/app/pages/index.js @@ -0,0 +1,14 @@ +export default (props) => ( + <> +

home

+
{props.fromMiddleware}
+ +) + +export async function getServerSideProps({ res }) { + return { + props: { + fromMiddleware: res.getHeader('x-from-middleware') || null, + }, + } +} diff --git a/test/e2e/middleware-custom-matchers-i18n/app/pages/routes.js b/test/e2e/middleware-custom-matchers-i18n/app/pages/routes.js new file mode 100644 index 000000000000..14f5eeb2af01 --- /dev/null +++ b/test/e2e/middleware-custom-matchers-i18n/app/pages/routes.js @@ -0,0 +1,26 @@ +import Link from 'next/link' + +export default (props) => ( + +) diff --git a/test/e2e/middleware-custom-matchers-i18n/test/index.test.ts b/test/e2e/middleware-custom-matchers-i18n/test/index.test.ts new file mode 100644 index 000000000000..274e81b7e18d --- /dev/null +++ b/test/e2e/middleware-custom-matchers-i18n/test/index.test.ts @@ -0,0 +1,55 @@ +/* eslint-env jest */ +/* eslint-disable jest/no-standalone-expect */ + +import { join } from 'path' +import webdriver from 'next-webdriver' +import { fetchViaHTTP } from 'next-test-utils' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' + +const itif = (condition: boolean) => (condition ? it : it.skip) + +const isModeDeploy = process.env.NEXT_TEST_MODE === 'deploy' + +describe('Middleware custom matchers i18n', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: new FileRef(join(__dirname, '../app')), + }) + }) + afterAll(() => next.destroy()) + + it.each(['/hello', '/en/hello', '/nl-NL/hello', '/nl-NL/about'])( + 'should match', + async (path) => { + const res = await fetchViaHTTP(next.url, path) + expect(res.status).toBe(200) + expect(res.headers.get('x-from-middleware')).toBeDefined() + } + ) + + it.each(['/invalid/hello', '/hello/invalid', '/about', '/en/about'])( + 'should not match', + async (path) => { + const res = await fetchViaHTTP(next.url, path) + expect(res.status).toBe(404) + } + ) + + // FIXME: + // See https://linear.app/vercel/issue/EC-160/header-value-set-on-middleware-is-not-propagated-on-client-request-of + itif(!isModeDeploy).each(['hello', 'en_hello', 'nl-NL_hello', 'nl-NL_about'])( + 'should match has query on client routing', + async (id) => { + const browser = await webdriver(next.url, '/routes') + await browser.eval('window.__TEST_NO_RELOAD = true') + await browser.elementById(id).click() + const fromMiddleware = await browser.elementById('from-middleware').text() + expect(fromMiddleware).toBe('true') + const noReload = await browser.eval('window.__TEST_NO_RELOAD') + expect(noReload).toBe(true) + } + ) +}) diff --git a/test/e2e/middleware-custom-matchers/app/middleware.js b/test/e2e/middleware-custom-matchers/app/middleware.js new file mode 100644 index 000000000000..fdea3f1f6f0f --- /dev/null +++ b/test/e2e/middleware-custom-matchers/app/middleware.js @@ -0,0 +1,61 @@ +import { NextResponse } from 'next/server' + +export default function middleware(request) { + const res = NextResponse.rewrite(new URL('/', request.url)) + res.headers.set('X-From-Middleware', 'true') + return res +} + +export const config = { + matcher: [ + { source: '/source-match' }, + { + source: '/has-match-1', + has: [ + { + type: 'header', + key: 'x-my-header', + value: '(?.*)', + }, + ], + }, + { + source: '/has-match-2', + has: [ + { + type: 'query', + key: 'my-query', + }, + ], + }, + { + source: '/has-match-3', + has: [ + { + type: 'cookie', + key: 'loggedIn', + value: '(?true)', + }, + ], + }, + { + source: '/has-match-4', + has: [ + { + type: 'host', + value: 'example.com', + }, + ], + }, + { + source: '/has-match-5', + has: [ + { + type: 'header', + key: 'hasParam', + value: 'with-params', + }, + ], + }, + ], +} diff --git a/test/e2e/middleware-custom-matchers/app/pages/index.js b/test/e2e/middleware-custom-matchers/app/pages/index.js new file mode 100644 index 000000000000..accd5b17adff --- /dev/null +++ b/test/e2e/middleware-custom-matchers/app/pages/index.js @@ -0,0 +1,14 @@ +export default (props) => ( + <> +

home

+
{props.fromMiddleware}
+ +) + +export async function getServerSideProps({ res }) { + return { + props: { + fromMiddleware: res.getHeader('x-from-middleware') || null, + }, + } +} diff --git a/test/e2e/middleware-custom-matchers/app/pages/routes.js b/test/e2e/middleware-custom-matchers/app/pages/routes.js new file mode 100644 index 000000000000..9cd510fe3d75 --- /dev/null +++ b/test/e2e/middleware-custom-matchers/app/pages/routes.js @@ -0,0 +1,16 @@ +import Link from 'next/link' + +export default (props) => ( + +) diff --git a/test/e2e/middleware-custom-matchers/test/index.test.ts b/test/e2e/middleware-custom-matchers/test/index.test.ts new file mode 100644 index 000000000000..0c33cf5e2f6e --- /dev/null +++ b/test/e2e/middleware-custom-matchers/test/index.test.ts @@ -0,0 +1,145 @@ +/* eslint-env jest */ +/* eslint-disable jest/no-standalone-expect */ +import { join } from 'path' +import webdriver from 'next-webdriver' +import { fetchViaHTTP } from 'next-test-utils' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' + +const itif = (condition: boolean) => (condition ? it : it.skip) + +const isModeDeploy = process.env.NEXT_TEST_MODE === 'deploy' + +describe('Middleware custom matchers', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: new FileRef(join(__dirname, '../app')), + }) + }) + afterAll(() => next.destroy()) + + const runTests = () => { + it('should match source path', async () => { + const res = await fetchViaHTTP(next.url, '/source-match') + expect(res.status).toBe(200) + expect(res.headers.get('x-from-middleware')).toBeDefined() + }) + + it('should match has header', async () => { + const res = await fetchViaHTTP(next.url, '/has-match-1', undefined, { + headers: { + 'x-my-header': 'hello world!!', + }, + }) + expect(res.status).toBe(200) + expect(res.headers.get('x-from-middleware')).toBeDefined() + + const res2 = await fetchViaHTTP(next.url, '/has-match-1') + expect(res2.status).toBe(404) + }) + + it('should match has query', async () => { + const res = await fetchViaHTTP(next.url, '/has-match-2', { + 'my-query': 'hellooo', + }) + expect(res.status).toBe(200) + expect(res.headers.get('x-from-middleware')).toBeDefined() + + const res2 = await fetchViaHTTP(next.url, '/has-match-2') + expect(res2.status).toBe(404) + }) + + it('should match has cookie', async () => { + const res = await fetchViaHTTP(next.url, '/has-match-3', undefined, { + headers: { + cookie: 'loggedIn=true', + }, + }) + expect(res.status).toBe(200) + expect(res.headers.get('x-from-middleware')).toBeDefined() + + const res2 = await fetchViaHTTP(next.url, '/has-match-3', undefined, { + headers: { + cookie: 'loggedIn=false', + }, + }) + expect(res2.status).toBe(404) + }) + + // Cannot modify host when testing with real deployment + itif(!isModeDeploy)('should match has host', async () => { + const res1 = await fetchViaHTTP(next.url, '/has-match-4') + expect(res1.status).toBe(404) + + const res = await fetchViaHTTP(next.url, '/has-match-4', undefined, { + headers: { + host: 'example.com', + }, + }) + + expect(res.status).toBe(200) + expect(res.headers.get('x-from-middleware')).toBeDefined() + + const res2 = await fetchViaHTTP(next.url, '/has-match-4', undefined, { + headers: { + host: 'example.org', + }, + }) + expect(res2.status).toBe(404) + }) + + it('should match has header value', async () => { + const res = await fetchViaHTTP(next.url, '/has-match-5', undefined, { + headers: { + hasParam: 'with-params', + }, + }) + expect(res.status).toBe(200) + expect(res.headers.get('x-from-middleware')).toBeDefined() + + const res2 = await fetchViaHTTP(next.url, '/has-match-5', undefined, { + headers: { + hasParam: 'without-params', + }, + }) + expect(res2.status).toBe(404) + }) + + // FIXME: Test fails on Vercel deployment for now. + // See https://linear.app/vercel/issue/EC-160/header-value-set-on-middleware-is-not-propagated-on-client-request-of + itif(!isModeDeploy)( + 'should match has query on client routing', + async () => { + const browser = await webdriver(next.url, '/routes') + await browser.eval('window.__TEST_NO_RELOAD = true') + await browser.elementById('has-match-2').click() + const fromMiddleware = await browser + .elementById('from-middleware') + .text() + expect(fromMiddleware).toBe('true') + const noReload = await browser.eval('window.__TEST_NO_RELOAD') + expect(noReload).toBe(true) + } + ) + + itif(!isModeDeploy)( + 'should match has cookie on client routing', + async () => { + const browser = await webdriver(next.url, '/routes') + await browser.addCookie({ name: 'loggedIn', value: 'true' }) + await browser.refresh() + await browser.eval('window.__TEST_NO_RELOAD = true') + await browser.elementById('has-match-3').click() + const fromMiddleware = await browser + .elementById('from-middleware') + .text() + expect(fromMiddleware).toBe('true') + const noReload = await browser.eval('window.__TEST_NO_RELOAD') + expect(noReload).toBe(true) + } + ) + } + runTests() +}) diff --git a/test/e2e/middleware-general/test/index.test.ts b/test/e2e/middleware-general/test/index.test.ts index 44adbdf0f1ae..a99d002b1201 100644 --- a/test/e2e/middleware-general/test/index.test.ts +++ b/test/e2e/middleware-general/test/index.test.ts @@ -99,8 +99,8 @@ describe('Middleware Runtime', () => { next.url, `/_next/static/${next.buildId}/_devMiddlewareManifest.json` ) - const { location } = await res.json() - expect(location).toBe('.*') + const matchers = await res.json() + expect(matchers).toEqual([{ regexp: '.*' }]) }) } @@ -119,7 +119,7 @@ describe('Middleware Runtime', () => { files: ['server/edge-runtime-webpack.js', 'server/middleware.js'], name: 'middleware', page: '/', - regexp: '^/.*$', + matchers: [{ regexp: '^/.*$' }], wasm: [], assets: [], }, diff --git a/test/e2e/middleware-matcher/index.test.ts b/test/e2e/middleware-matcher/index.test.ts index 812e19c69475..2049c560eb1a 100644 --- a/test/e2e/middleware-matcher/index.test.ts +++ b/test/e2e/middleware-matcher/index.test.ts @@ -94,20 +94,19 @@ describe('Middleware can set the matcher in its config', () => { expect(response.headers.get('X-From-Middleware')).toBe('true') }) - it('should load matches in client manifest correctly', async () => { + it('should load matches in client matchers correctly', async () => { const browser = await webdriver(next.url, '/') await check(async () => { - const manifest = await browser.eval( + const matchers = await browser.eval( (global as any).isNextDev - ? 'window.__DEV_MIDDLEWARE_MANIFEST' - : 'window.__MIDDLEWARE_MANIFEST' + ? 'window.__DEV_MIDDLEWARE_MATCHERS' + : 'window.__MIDDLEWARE_MATCHERS' ) - const { location } = manifest - return location && - location.includes('with-middleware') && - location.includes('another-middleware') + return matchers && + matchers.some((m) => m.regexp.includes('with-middleware')) && + matchers.some((m) => m.regexp.includes('another-middleware')) ? 'success' : 'failed' }, 'success') diff --git a/test/e2e/middleware-trailing-slash/test/index.test.ts b/test/e2e/middleware-trailing-slash/test/index.test.ts index 4277ec9ae80f..97b85a525e71 100644 --- a/test/e2e/middleware-trailing-slash/test/index.test.ts +++ b/test/e2e/middleware-trailing-slash/test/index.test.ts @@ -59,7 +59,7 @@ describe('Middleware Runtime trailing slash', () => { name: 'middleware', env: [], page: '/', - regexp: '^/.*$', + matchers: [{ regexp: '^/.*$' }], wasm: [], assets: [], }, diff --git a/test/e2e/no-eslint-warn-with-no-eslint-config/index.test.ts b/test/e2e/no-eslint-warn-with-no-eslint-config/index.test.ts index d24e7755b4ed..ab31e1f3100b 100644 --- a/test/e2e/no-eslint-warn-with-no-eslint-config/index.test.ts +++ b/test/e2e/no-eslint-warn-with-no-eslint-config/index.test.ts @@ -65,5 +65,23 @@ describe('no-eslint-warn-with-no-eslint-config', () => { await next.patchFile('package.json', origPkgJson) } }) + + it('should not warn with eslint config in package.json', async () => { + await next.stop() + const origPkgJson = await next.readFile('package.json') + const pkgJson = JSON.parse(origPkgJson) + pkgJson.eslintConfig = { rules: { semi: 'off' } } + + try { + await next.patchFile('package.json', JSON.stringify(pkgJson)) + await next.start() + + expect(next.cliOutput).not.toContain( + 'No ESLint configuration detected. Run next lint to begin setup' + ) + } finally { + await next.patchFile('package.json', origPkgJson) + } + }) } }) diff --git a/test/e2e/switchable-runtime/index.test.ts b/test/e2e/switchable-runtime/index.test.ts index b7d1ab0efb96..f21f94dba24b 100644 --- a/test/e2e/switchable-runtime/index.test.ts +++ b/test/e2e/switchable-runtime/index.test.ts @@ -67,7 +67,7 @@ describe('Switchable runtime', () => { `/_next/static/${next.buildId}/_devMiddlewareManifest.json` ) const devMiddlewareManifest = await res.json() - expect(devMiddlewareManifest).toEqual({}) + expect(devMiddlewareManifest).toEqual([]) }) it('should sort edge SSR routes correctly', async () => { @@ -184,7 +184,7 @@ describe('Switchable runtime', () => { ], name: 'pages/api/hello', page: '/api/hello', - regexp: '^/api/hello$', + matchers: [{ regexp: '^/api/hello$' }], wasm: [], }, '/api/edge': { @@ -195,7 +195,7 @@ describe('Switchable runtime', () => { ], name: 'pages/api/edge', page: '/api/edge', - regexp: '^/api/edge$', + matchers: [{ regexp: '^/api/edge$' }], wasm: [], }, }, @@ -328,7 +328,7 @@ describe('Switchable runtime', () => { ], name: 'pages/api/hello', page: '/api/hello', - regexp: '^/api/hello$', + matchers: [{ regexp: '^/api/hello$' }], wasm: [], }, '/api/edge': { @@ -339,7 +339,7 @@ describe('Switchable runtime', () => { ], name: 'pages/api/edge', page: '/api/edge', - regexp: '^/api/edge$', + matchers: [{ regexp: '^/api/edge$' }], wasm: [], }, }, diff --git a/test/integration/cli/test/index.test.js b/test/integration/cli/test/index.test.js index 6e5aeb0451fd..4d228a31f569 100644 --- a/test/integration/cli/test/index.test.js +++ b/test/integration/cli/test/index.test.js @@ -178,6 +178,34 @@ describe('CLI Usage', () => { expect(output).toMatch(new RegExp(`http://localhost:${port}`)) }) + test('--port 0', async () => { + const output = await runNextCommandDev([dir, '--port', '0'], true) + const matches = /on 0.0.0.0:(\d+)/.exec(output) + expect(matches).not.toBe(null) + + const port = parseInt(matches[1]) + // Regression test: port 0 was interpreted as if no port had been + // provided, falling back to 3000. + expect(port).not.toBe(3000) + + expect(output).toMatch(new RegExp(`http://localhost:${port}`)) + }) + + test('PORT=0', async () => { + const output = await runNextCommandDev([dir], true, { + env: { PORT: 0 }, + }) + const matches = /on 0.0.0.0:(\d+)/.exec(output) + expect(matches).not.toBe(null) + + const port = parseInt(matches[1]) + // Regression test: port 0 was interpreted as if no port had been + // provided, falling back to 3000. + expect(port).not.toBe(3000) + + expect(output).toMatch(new RegExp(`http://localhost:${port}`)) + }) + test("NODE_OPTIONS='--inspect'", async () => { // this test checks that --inspect works by launching a single debugger for the main Next.js process, // not for its subprocesses diff --git a/test/integration/custom-routes/test/index.test.js b/test/integration/custom-routes/test/index.test.js index cd5fc57f5ddb..885683cc40be 100644 --- a/test/integration/custom-routes/test/index.test.js +++ b/test/integration/custom-routes/test/index.test.js @@ -986,7 +986,7 @@ const runTests = (isDev = false) => { host: '1', }) - const res2 = await fetchViaHTTP(appPort, '/has-rewrite-3') + const res2 = await fetchViaHTTP(appPort, '/has-rewrite-4') expect(res2.status).toBe(404) }) diff --git a/test/integration/export-image-loader/test/index.test.js b/test/integration/export-image-loader/test/index.test.js index b6cd36f14345..4f221aff2ffa 100644 --- a/test/integration/export-image-loader/test/index.test.js +++ b/test/integration/export-image-loader/test/index.test.js @@ -87,10 +87,8 @@ describe('Export with unoptimized next/image component', () => { await nextConfig.replace( '{ /* replaceme */ }', JSON.stringify({ - experimental: { - images: { - unoptimized: true, - }, + images: { + unoptimized: true, }, }) ) diff --git a/test/integration/file-serving/test/index.test.js b/test/integration/file-serving/test/index.test.js index cb886b6fd238..7b09bb29cc63 100644 --- a/test/integration/file-serving/test/index.test.js +++ b/test/integration/file-serving/test/index.test.js @@ -68,6 +68,16 @@ const runTests = () => { expect(res.headers.get('content-type')).toBe('image/avif') }) + it('should serve correct error code', async () => { + // vercel-icon-dark.avif is downloaded from https://vercel.com/design and transformed to avif on avif.io + const res = await fetchViaHTTP(appPort, '/vercel-icon-dark.avif', '', { + headers: { + Range: 'bytes=1000000000-', + }, + }) + expect(res.status).toBe(416) // 416 Range Not Satisfiable + }) + // checks against traversal requests from // https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Directory%20Traversal/Intruder/traversals-8-deep-exotic-encoding.txt diff --git a/test/integration/image-component/unicode/next.config.js b/test/integration/image-component/unicode/next.config.js index e4ff44f0fe11..dd7057b8a353 100644 --- a/test/integration/image-component/unicode/next.config.js +++ b/test/integration/image-component/unicode/next.config.js @@ -1,7 +1,5 @@ module.exports = { - experimental: { - images: { - remotePatterns: [{ hostname: 'image-optimization-test.vercel.app' }], - }, + images: { + remotePatterns: [{ hostname: 'image-optimization-test.vercel.app' }], }, } diff --git a/test/integration/image-component/unoptimized/next.config.js b/test/integration/image-component/unoptimized/next.config.js index 5733bb27b41f..c0746d48e67a 100644 --- a/test/integration/image-component/unoptimized/next.config.js +++ b/test/integration/image-component/unoptimized/next.config.js @@ -1,7 +1,5 @@ module.exports = { - experimental: { - images: { - unoptimized: true, - }, + images: { + unoptimized: true, }, } diff --git a/test/integration/image-future/asset-prefix/next.config.js b/test/integration/image-future/asset-prefix/next.config.js index 7659d462ee86..4c428503af6b 100644 --- a/test/integration/image-future/asset-prefix/next.config.js +++ b/test/integration/image-future/asset-prefix/next.config.js @@ -1,9 +1,4 @@ module.exports = { assetPrefix: 'https://example.vercel.sh/pre', // Intentionally omit `domains` and `remotePatterns` - experimental: { - images: { - allowFutureImage: true, - }, - }, } diff --git a/test/integration/image-future/base-path/next.config.js b/test/integration/image-future/base-path/next.config.js index e6d292fc572f..ee95502b605d 100644 --- a/test/integration/image-future/base-path/next.config.js +++ b/test/integration/image-future/base-path/next.config.js @@ -1,8 +1,3 @@ module.exports = { basePath: '/docs', - experimental: { - images: { - allowFutureImage: true, - }, - }, } diff --git a/test/integration/image-future/default/next.config.js b/test/integration/image-future/default/next.config.js deleted file mode 100644 index 853011cb81ac..000000000000 --- a/test/integration/image-future/default/next.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - experimental: { - images: { - allowFutureImage: true, - }, - }, -} diff --git a/test/integration/image-future/default/pages/missing-alt.js b/test/integration/image-future/default/pages/missing-alt.js new file mode 100644 index 000000000000..7b48115e4306 --- /dev/null +++ b/test/integration/image-future/default/pages/missing-alt.js @@ -0,0 +1,13 @@ +import React from 'react' +import Image from 'next/future/image' +import testJPG from '../public/test.jpg' + +export default function Page() { + return ( +
+

Missing alt

+ + +
+ ) +} diff --git a/test/integration/image-future/default/test/index.test.ts b/test/integration/image-future/default/test/index.test.ts index cdbe51f5f460..c5b3deb535aa 100644 --- a/test/integration/image-future/default/test/index.test.ts +++ b/test/integration/image-future/default/test/index.test.ts @@ -685,6 +685,16 @@ function runTests(mode) { ) }) + it('should show missing alt error', async () => { + const browser = await webdriver(appPort, '/missing-alt') + + expect(await hasRedbox(browser)).toBe(false) + + await check(async () => { + return (await browser.log()).map((log) => log.message).join('\n') + }, /Image is missing required "alt" property/gm) + }) + it('should show error when missing width prop', async () => { const browser = await webdriver(appPort, '/missing-width') diff --git a/test/integration/image-future/image-from-node-modules/next.config.js b/test/integration/image-future/image-from-node-modules/next.config.js index 196f6a328994..a62705edb891 100644 --- a/test/integration/image-future/image-from-node-modules/next.config.js +++ b/test/integration/image-future/image-from-node-modules/next.config.js @@ -2,9 +2,4 @@ module.exports = { images: { domains: ['i.imgur.com'], }, - experimental: { - images: { - allowFutureImage: true, - }, - }, } diff --git a/test/integration/image-future/react-virtualized/next.config.js b/test/integration/image-future/react-virtualized/next.config.js deleted file mode 100644 index 853011cb81ac..000000000000 --- a/test/integration/image-future/react-virtualized/next.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - experimental: { - images: { - allowFutureImage: true, - }, - }, -} diff --git a/test/integration/image-future/svgo-webpack/next.config.js b/test/integration/image-future/svgo-webpack/next.config.js index 64102572c230..c1e4c555b45f 100644 --- a/test/integration/image-future/svgo-webpack/next.config.js +++ b/test/integration/image-future/svgo-webpack/next.config.js @@ -10,9 +10,4 @@ module.exports = { return config }, - experimental: { - images: { - allowFutureImage: true, - }, - }, } diff --git a/test/integration/image-future/trailing-slash/next.config.js b/test/integration/image-future/trailing-slash/next.config.js index c97b48f013a8..ce3f975d0eac 100644 --- a/test/integration/image-future/trailing-slash/next.config.js +++ b/test/integration/image-future/trailing-slash/next.config.js @@ -1,8 +1,3 @@ module.exports = { trailingSlash: true, - experimental: { - images: { - allowFutureImage: true, - }, - }, } diff --git a/test/integration/image-future/typescript/next.config.js b/test/integration/image-future/typescript/next.config.js index b58d7a90bd11..7e24969d36ee 100644 --- a/test/integration/image-future/typescript/next.config.js +++ b/test/integration/image-future/typescript/next.config.js @@ -3,9 +3,4 @@ module.exports = { domains: ['image-optimization-test.vercel.app'], // disableStaticImages: true, }, - experimental: { - images: { - allowFutureImage: true, - }, - }, } diff --git a/test/integration/image-future/typescript/pages/invalid.tsx b/test/integration/image-future/typescript/pages/invalid.tsx index 85a60daa6773..9de4e34c16fb 100644 --- a/test/integration/image-future/typescript/pages/invalid.tsx +++ b/test/integration/image-future/typescript/pages/invalid.tsx @@ -5,20 +5,34 @@ const Invalid = () => { return (

Invalid TS

- + invalid-src invalid-width + /> invalid-placeholder + /> +

This is the invalid usage

) diff --git a/test/integration/image-future/typescript/pages/valid.tsx b/test/integration/image-future/typescript/pages/valid.tsx index 2bc8a517a95e..3dc5a311f9d4 100644 --- a/test/integration/image-future/typescript/pages/valid.tsx +++ b/test/integration/image-future/typescript/pages/valid.tsx @@ -12,12 +12,14 @@ const Page = () => {

Valid TS

width-and-height-num width-and-height-str { /> quality-str { /> data-protocol placeholder-and-blur-data-url - + no-width-and-height object-src-with-placeholder - - + object-src-with-svg + object-src-with-avif { await nextConfig.replace( '{ /* replaceme */ }', JSON.stringify({ - experimental: { - images: { - remotePatterns: Array.from({ length: 51 }).map((_) => ({ - hostname: 'example.com', - })), - }, + images: { + remotePatterns: Array.from({ length: 51 }).map((_) => ({ + hostname: 'example.com', + })), }, }) ) @@ -81,10 +79,8 @@ describe('Image Optimizer', () => { await nextConfig.replace( '{ /* replaceme */ }', JSON.stringify({ - experimental: { - images: { - remotePatterns: [{ hostname: 'example.com', foo: 'bar' }], - }, + images: { + remotePatterns: [{ hostname: 'example.com', foo: 'bar' }], }, }) ) @@ -108,10 +104,8 @@ describe('Image Optimizer', () => { await nextConfig.replace( '{ /* replaceme */ }', JSON.stringify({ - experimental: { - images: { - remotePatterns: [{ protocol: 'https' }], - }, + images: { + remotePatterns: [{ protocol: 'https' }], }, }) ) diff --git a/test/integration/invalid-custom-routes/test/index.test.js b/test/integration/invalid-custom-routes/test/index.test.js index ef42bc9262db..67db498fc398 100644 --- a/test/integration/invalid-custom-routes/test/index.test.js +++ b/test/integration/invalid-custom-routes/test/index.test.js @@ -143,10 +143,6 @@ const runTests = () => { `\`destination\` is missing for route {"source":"/hello","permanent":false}` ) - expect(stderr).toContain( - `\`destination\` is missing for route {"source":"/hello","permanent":false}` - ) - expect(stderr).toContain( `\`source\` is not a string for route {"source":123,"destination":"/another","permanent":false}` ) @@ -163,14 +159,6 @@ const runTests = () => { `\`permanent\` is not set to \`true\` or \`false\` for route {"source":"/hello","destination":"/another","permanent":"yes"}` ) - expect(stderr).toContain( - `\`permanent\` is not set to \`true\` or \`false\` for route {"source":"/hello","destination":"/another","permanent":"yes"}` - ) - - expect(stderr).toContain( - `\`permanent\` is not set to \`true\` or \`false\` for route {"source":"/hello","destination":"/another","permanent":"yes"}` - ) - expect(stderr).toContain( `\`destination\` has unnamed params :0 for route {"source":"/hello/world/(.*)","destination":"/:0","permanent":true}` ) diff --git a/test/integration/invalid-middleware-matchers/pages/index.js b/test/integration/invalid-middleware-matchers/pages/index.js new file mode 100644 index 000000000000..0957a987fc2f --- /dev/null +++ b/test/integration/invalid-middleware-matchers/pages/index.js @@ -0,0 +1 @@ +export default () => 'hi' diff --git a/test/integration/invalid-middleware-matchers/test/index.test.js b/test/integration/invalid-middleware-matchers/test/index.test.js new file mode 100644 index 000000000000..0c918b0fa65a --- /dev/null +++ b/test/integration/invalid-middleware-matchers/test/index.test.js @@ -0,0 +1,169 @@ +/* eslint-env jest */ + +import fs from 'fs-extra' +import { join } from 'path' +import { fetchViaHTTP, findPort, launchApp, nextBuild } from 'next-test-utils' + +let appDir = join(__dirname, '..') +const middlewarePath = join(appDir, 'middleware.js') + +const writeMiddleware = async (matchers) => { + await fs.writeFile( + middlewarePath, + ` + import { NextResponse } from 'next/server' + + export default function middleware() { + return NextResponse.next() + } + + export const config = { + matcher: ${JSON.stringify(matchers)}, + } + ` + ) +} + +let getStderr + +const runTests = () => { + it('should error when source length is exceeded', async () => { + await writeMiddleware([{ source: `/${Array(4096).join('a')}` }]) + const stderr = await getStderr() + expect(stderr).toContain( + '`source` exceeds max built length of 4096 for route {"source":"/aaaaaaaaaaaaaaaaaa' + ) + }) + + it('should error during next build for invalid matchers', async () => { + await writeMiddleware([ + { + // missing source + }, + { + // invalid source + source: 123, + }, + // missing forward slash in source + 'hello', + { + // extra field + source: '/hello', + destination: '/not-allowed', + }, + + // invalid objects + null, + // invalid has items + { + source: '/hello', + has: [ + { + type: 'cookiee', + key: 'loggedIn', + }, + ], + }, + { + source: '/hello', + has: [ + { + type: 'headerr', + }, + { + type: 'queryr', + key: 'hello', + }, + ], + }, + { + source: '/hello', + basePath: false, + }, + { + source: '/hello', + locale: true, + }, + ]) + const stderr = await getStderr() + + expect(stderr).toContain(`\`source\` is missing for route {}`) + + expect(stderr).toContain( + `\`source\` is not a string for route {"source":123}` + ) + + expect(stderr).toContain( + `\`source\` does not start with / for route {"source":"hello"}` + ) + + expect(stderr).toContain( + `invalid field: destination for route {"source":"/hello","destination":"/not-allowed"}` + ) + + expect(stderr).toContain( + `The route null is not a valid object with \`source\`` + ) + + expect(stderr).toContain('Invalid `has` item:') + expect(stderr).toContain( + `invalid type "cookiee" for {"type":"cookiee","key":"loggedIn"}` + ) + expect(stderr).toContain( + `invalid \`has\` item found for route {"source":"/hello","has":[{"type":"cookiee","key":"loggedIn"}]}` + ) + + expect(stderr).toContain('Invalid `has` items:') + expect(stderr).toContain( + `invalid type "headerr", invalid key "undefined" for {"type":"headerr"}` + ) + expect(stderr).toContain( + `invalid type "queryr" for {"type":"queryr","key":"hello"}` + ) + expect(stderr).toContain( + `invalid \`has\` items found for route {"source":"/hello","has":[{"type":"headerr"},{"type":"queryr","key":"hello"}]}` + ) + expect(stderr).toContain(`Valid \`has\` object shape is {`) + expect(stderr).toContain( + `invalid field: basePath for route {"source":"/hello","basePath":false}` + ) + expect(stderr).toContain( + '`locale` must be undefined or false for route {"source":"/hello","locale":true}' + ) + }) +} + +describe('Errors on invalid custom middleware matchers', () => { + afterAll(() => fs.remove(middlewarePath)) + + describe('dev mode', () => { + beforeAll(() => { + getStderr = async () => { + let stderr = '' + const port = await findPort() + await launchApp(appDir, port, { + onStderr(msg) { + stderr += msg + }, + }) + await fetchViaHTTP(port, '/') + // suppress error + .catch(() => {}) + return stderr + } + }) + + runTests() + }) + + describe('production mode', () => { + beforeAll(() => { + getStderr = async () => { + const { stderr } = await nextBuild(appDir, [], { stderr: true }) + return stderr + } + }) + + runTests() + }) +}) diff --git a/test/integration/telemetry/next.config.i18n-images b/test/integration/telemetry/next.config.i18n-images index 4b7a8d7ce6ef..0a7be628c6de 100644 --- a/test/integration/telemetry/next.config.i18n-images +++ b/test/integration/telemetry/next.config.i18n-images @@ -4,11 +4,7 @@ module.exports = phase => { formats: ['image/avif', 'image/webp'], imageSizes: [64, 128, 256, 512, 1024], domains: ['example.com', 'another.com'], - }, - experimental: { - images: { - remotePatterns: [{ protocol: 'https', hostname: '**.example.com' }], - }, + remotePatterns: [{ protocol: 'https', hostname: '**.example.com' }], }, i18n: { locales: ['en','nl','fr'], diff --git a/test/integration/telemetry/test/index.test.js b/test/integration/telemetry/test/index.test.js index 1750ad835551..60a493fbad67 100644 --- a/test/integration/telemetry/test/index.test.js +++ b/test/integration/telemetry/test/index.test.js @@ -503,7 +503,7 @@ describe('Telemetry CLI', () => { expect(event1).toMatch(/"localeDomainsCount": 2/) expect(event1).toMatch(/"localeDetectionEnabled": true/) expect(event1).toMatch(/"imageEnabled": true/) - expect(event1).toMatch(/"imageFutureEnabled": false/) + expect(event1).toMatch(/"imageFutureEnabled": true/) expect(event1).toMatch(/"imageDomainsCount": 2/) expect(event1).toMatch(/"imageRemotePatternsCount": 1/) expect(event1).toMatch(/"imageSizes": "64,128,256,512,1024"/) diff --git a/test/lib/next-modes/next-dev.ts b/test/lib/next-modes/next-dev.ts index f7161c7b7a9c..067d8632a08c 100644 --- a/test/lib/next-modes/next-dev.ts +++ b/test/lib/next-modes/next-dev.ts @@ -37,8 +37,8 @@ export class NextDevInstance extends NextInstance { ...process.env, ...this.env, NODE_ENV: '' as any, + PORT: this.forcedPort || '0', __NEXT_TEST_MODE: '1', - __NEXT_FORCED_PORT: this.forcedPort || '0', __NEXT_TEST_WITH_DEVTOOL: '1', }, }) diff --git a/test/lib/next-modes/next-start.ts b/test/lib/next-modes/next-start.ts index 11515fce56ed..51672583a40e 100644 --- a/test/lib/next-modes/next-start.ts +++ b/test/lib/next-modes/next-start.ts @@ -48,8 +48,8 @@ export class NextStartInstance extends NextInstance { ...process.env, ...this.env, NODE_ENV: '' as any, + PORT: this.forcedPort || '0', __NEXT_TEST_MODE: '1', - __NEXT_FORCED_PORT: this.forcedPort || '0', }, } let buildArgs = ['yarn', 'next', 'build'] diff --git a/test/production/typescript-basic/app/next.config.js b/test/production/typescript-basic/app/next.config.js index de288dcdbbd1..1bde45b6d1d9 100644 --- a/test/production/typescript-basic/app/next.config.js +++ b/test/production/typescript-basic/app/next.config.js @@ -2,10 +2,8 @@ * @type {import('next').NextConfig} */ const nextConfig = { - experimental: { - images: { - allowFutureImage: true, - }, + images: { + dangerouslyAllowSVG: true, }, } diff --git a/test/production/typescript-basic/app/pages/image-import.tsx b/test/production/typescript-basic/app/pages/image-import.tsx index 5fe97473ec0c..1cd82addc5e9 100644 --- a/test/production/typescript-basic/app/pages/image-import.tsx +++ b/test/production/typescript-basic/app/pages/image-import.tsx @@ -6,9 +6,9 @@ export default function Page() { return ( <>

Example Image Usage

- +
- + ) }