From 7102050ec74482467a65d077601177e8cac5876c Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 21 Oct 2022 15:31:53 -0400 Subject: [PATCH 1/3] Add support for `images.loaderFile` config (#41585) This PR adds a new configure property, `images.loaderFile` that allow you to define a path to a file with an exported image loader function. This is useful when migrating from `next/legacy/image` to `next/image` because it lets you configure the loader for every instance of `next/image` once, similar to the legacy "built-in loaders". --- docs/api-reference/next/image.md | 25 +++ docs/basic-features/image-optimization.md | 8 +- errors/invalid-images-config.md | 2 + packages/next/build/webpack-config.ts | 6 + packages/next/client/image.tsx | 99 +++--------- packages/next/server/config-schema.ts | 4 + packages/next/server/config.ts | 32 +++- packages/next/shared/lib/image-config.ts | 8 +- packages/next/shared/lib/image-loader.ts | 65 ++++++++ .../export-image-loader-legacy/next.config.js | 2 + .../export-image-loader-legacy/pages/index.js | 10 ++ .../test/index.test.js | 142 ++++++++++++++++++ .../export-image-loader/dummy-loader.js | 3 + .../export-image-loader/test/index.test.js | 61 ++++++++ .../image-optimizer/test/index.test.ts | 50 ++++++ .../loader-config/dummy-loader.js | 3 + .../loader-config/next.config.js | 6 + .../loader-config/pages/index.js | 35 +++++ .../loader-config/public/logo.png | Bin 0 -> 1545 bytes .../loader-config/test/index.test.ts | 76 ++++++++++ 20 files changed, 549 insertions(+), 88 deletions(-) create mode 100644 packages/next/shared/lib/image-loader.ts create mode 100644 test/integration/export-image-loader-legacy/next.config.js create mode 100644 test/integration/export-image-loader-legacy/pages/index.js create mode 100644 test/integration/export-image-loader-legacy/test/index.test.js create mode 100644 test/integration/export-image-loader/dummy-loader.js create mode 100644 test/integration/next-image-new/loader-config/dummy-loader.js create mode 100644 test/integration/next-image-new/loader-config/next.config.js create mode 100644 test/integration/next-image-new/loader-config/pages/index.js create mode 100644 test/integration/next-image-new/loader-config/public/logo.png create mode 100644 test/integration/next-image-new/loader-config/test/index.test.ts diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index f81736ac2fc52..508e3e358896f 100644 --- a/docs/api-reference/next/image.md +++ b/docs/api-reference/next/image.md @@ -111,6 +111,8 @@ const MyImage = (props) => { } ``` +Alternatively, you can use the [loaderFile](#loader-configuration) configuration in next.config.js to configure every instance of `next/image` in your application, without passing a prop. + ### fill A boolean that causes the image to fill the parent element instead of setting [`width`](#width) and [`height`](#height). @@ -343,6 +345,29 @@ module.exports = { } ``` +### Loader Configuration + +If you want to use a cloud provider to optimize images instead of using the Next.js built-in Image Optimization API, you can configure the `loaderFile` in your `next.config.js` like the following: + +```js +module.exports = { + images: { + loader: 'custom', + loaderFile: './my/image/loader.js', + }, +} +``` + +This must point to a file relative to the root of your Next.js application. The file must export a default function that returns a string, for example: + +```js +export default function myImageLoader({ src, width, quality }) { + return `https://example.com/${src}?w=${width}&q=${quality || 75}` +} +``` + +Alternatively, you can use the [`loader` prop](#loader) to configure each instance of `next/image`. + ## Advanced The following configuration is for advanced use cases and is usually not necessary. If you choose to configure the properties below, you will override any changes to the Next.js defaults in future updates. diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index 355d8c26260ba..8f28cd14b1d23 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -99,13 +99,13 @@ To protect your application from malicious users, you must define a list of remo ### Loaders -Note that in the [example earlier](#remote-images), a partial URL (`"/me.png"`) is provided for a remote image. This is possible because of the `next/image` [loader](/docs/api-reference/next/image.md#loader) architecture. +Note that in the [example earlier](#remote-images), a partial URL (`"/me.png"`) is provided for a remote image. This is possible because of the loader architecture. A loader is a function that generates the URLs for your image. It modifies the provided `src`, and generates multiple URLs to request the image at different sizes. These multiple URLs are used in the automatic [srcset](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/srcset) generation, so that visitors to your site will be served an image that is the right size for their viewport. -The default loader for Next.js applications uses the built-in Image Optimization API, which optimizes images from anywhere on the web, and then serves them directly from the Next.js web server. If you would like to serve your images directly from a CDN or image server, you can use one of the [built-in loaders](/docs/api-reference/next/image.md#built-in-loaders) or write your own with a few lines of JavaScript. +The default loader for Next.js applications uses the built-in Image Optimization API, which optimizes images from anywhere on the web, and then serves them directly from the Next.js web server. If you would like to serve your images directly from a CDN or image server, you can write your own loader function with a few lines of JavaScript. -Loaders can be defined per-image, or at the application level. +You can define a loader per-image with the [`loader` prop](/docs/api-reference/next/image.md#loader), or at the application level with the [`loaderFile` configuration](https://nextjs.org/docs/api-reference/next/image#loader-configuration). ### Priority @@ -151,7 +151,7 @@ Because `next/image` is designed to guarantee good performance results, it canno > > If you are accessing images from a source without knowledge of the images' sizes, there are several things you can do: > -> **Use `fill``** +> **Use `fill`** > > The [`fill`](/docs/api-reference/next/image#fill) prop allows your image to be sized by its parent element. Consider using CSS to give the image's parent element space on the page along [`sizes`](/docs/api-reference/next/image#sizes) prop to match any media query break points. You can also use [`object-fit`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) with `fill`, `contain`, or `cover`, and [`object-position`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) to define how the image should occupy that space. > diff --git a/errors/invalid-images-config.md b/errors/invalid-images-config.md index 553c79facc747..badaa4f3e1a88 100644 --- a/errors/invalid-images-config.md +++ b/errors/invalid-images-config.md @@ -21,6 +21,8 @@ module.exports = { path: '/_next/image', // loader can be 'default', 'imgix', 'cloudinary', 'akamai', or 'custom' loader: 'default', + // file with `export default function loader({src, width, quality})` + loaderFile: '', // disable static imports for image files disableStaticImages: false, // minimumCacheTTL is in seconds, must be integer 0 or more diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index d74fea3d98337..b3a309077bb0a 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -866,6 +866,12 @@ export default async function getBaseWebpackConfig( } : undefined), + ...(config.images.loaderFile + ? { + 'next/dist/shared/lib/image-loader': config.images.loaderFile, + } + : undefined), + next: NEXT_PROJECT_ROOT, ...(hasServerComponents diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 9e5ba10ecebbd..da4910cadd5a5 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -16,6 +16,8 @@ import { } from '../shared/lib/image-config' import { ImageConfigContext } from '../shared/lib/image-config-context' import { warnOnce } from '../shared/lib/utils' +// @ts-ignore - This is replaced by webpack alias +import defaultLoader from 'next/dist/shared/lib/image-loader' const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete const allImgs = new Map< @@ -468,70 +470,6 @@ const ImageElement = ({ ) } -function defaultLoader({ - config, - src, - width, - quality, -}: ImageLoaderPropsWithConfig): string { - if (process.env.NODE_ENV !== 'production') { - const missingValues = [] - - // these should always be provided but make sure they are - if (!src) missingValues.push('src') - if (!width) missingValues.push('width') - - if (missingValues.length > 0) { - throw new Error( - `Next Image Optimization requires ${missingValues.join( - ', ' - )} to be provided. Make sure you pass them as props to the \`next/image\` component. Received: ${JSON.stringify( - { src, width, quality } - )}` - ) - } - - if (src.startsWith('//')) { - throw new Error( - `Failed to parse src "${src}" on \`next/image\`, protocol-relative URL (//) must be changed to an absolute URL (http:// or https://)` - ) - } - - if (!src.startsWith('/') && (config.domains || config.remotePatterns)) { - let parsedSrc: URL - try { - parsedSrc = new URL(src) - } catch (err) { - console.error(err) - throw new Error( - `Failed to parse src "${src}" on \`next/image\`, if using relative image it must start with a leading slash "/" or be an absolute URL (http:// or https://)` - ) - } - - 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, 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` - ) - } - } - } - } - - if (src.endsWith('.svg') && !config.dangerouslyAllowSVG) { - // Special case to make svg serve as-is to avoid proxying - // through the built-in Image Optimization API. - return src - } - - return `${config.path}?url=${encodeURIComponent(src)}&w=${width}&q=${ - quality || 75 - }` -} - export default function Image({ src, sizes, @@ -559,20 +497,29 @@ export default function Image({ }, [configContext]) let rest: Partial = all + let loader: ImageLoaderWithConfig = rest.loader || defaultLoader - let loader: ImageLoaderWithConfig = defaultLoader - if ('loader' in rest) { - if (rest.loader) { - const customImageLoader = rest.loader - loader = (obj) => { - const { config: _, ...opts } = obj - // The config object is internal only so we must - // not pass it to the user-defined loader() - return customImageLoader(opts) - } + // Remove property so it's not spread on element + delete rest.loader + + if ('__next_img_default' in loader) { + // This special value indicates that the user + // didn't define a "loader" prop or config. + if (config.loader === 'custom') { + throw new Error( + `Image with src "${src}" is missing "loader" prop.` + + `\nRead more: https://nextjs.org/docs/messages/next-image-missing-loader` + ) + } + } else { + // The user defined a "loader" prop or config. + // Since the config object is internal only, we + // must not pass it to the user-defined "loader". + const customImageLoader = loader as ImageLoader + loader = (obj) => { + const { config: _, ...opts } = obj + return customImageLoader(opts) } - // Remove property so it's not spread on - delete rest.loader } let staticSrc = '' diff --git a/packages/next/server/config-schema.ts b/packages/next/server/config-schema.ts index 29d08ee22729b..eeb8b94d6b01d 100644 --- a/packages/next/server/config-schema.ts +++ b/packages/next/server/config-schema.ts @@ -585,6 +585,10 @@ const configSchema = { enum: VALID_LOADERS as any, type: 'string', }, + loaderFile: { + minLength: 1, + type: 'string', + }, minimumCacheTTL: { type: 'number', }, diff --git a/packages/next/server/config.ts b/packages/next/server/config.ts index 88fb42dd095b6..83ee128ac16fd 100644 --- a/packages/next/server/config.ts +++ b/packages/next/server/config.ts @@ -1,4 +1,5 @@ -import { basename, extname, relative, isAbsolute, resolve } from 'path' +import { existsSync } from 'fs' +import { basename, extname, join, relative, isAbsolute, resolve } from 'path' import { pathToFileURL } from 'url' import { Agent as HttpAgent } from 'http' import { Agent as HttpsAgent } from 'https' @@ -76,7 +77,7 @@ export function setHttpClientAndAgentOptions(options: NextConfig) { ;(global as any).__NEXT_HTTPS_AGENT = new HttpsAgent(options.httpAgentOptions) } -function assignDefaults(userConfig: { [key: string]: any }) { +function assignDefaults(dir: string, userConfig: { [key: string]: any }) { const configFileName = userConfig.configFileName if (typeof userConfig.exportTrailingSlash !== 'undefined') { console.warn( @@ -379,7 +380,7 @@ function assignDefaults(userConfig: { [key: string]: any }) { images.path === imageConfigDefault.path ) { throw new Error( - `Specified images.loader property (${images.loader}) also requires images.path property to be assigned to a URL prefix.\nSee more info here: https://nextjs.org/docs/api-reference/next/image#loader-configuration` + `Specified images.loader property (${images.loader}) also requires images.path property to be assigned to a URL prefix.\nSee more info here: https://nextjs.org/docs/api-reference/next/legacy/image#loader-configuration` ) } @@ -398,6 +399,22 @@ function assignDefaults(userConfig: { [key: string]: any }) { images.path = `${result.basePath}${images.path}` } + if (images.loaderFile) { + if (images.loader !== 'default' && images.loader !== 'custom') { + throw new Error( + `Specified images.loader property (${images.loader}) cannot be used with images.loaderFile property. Please set images.loader to "custom".` + ) + } + const absolutePath = join(dir, images.loaderFile) + if (!existsSync(absolutePath)) { + throw new Error( + `Specified images.loaderFile does not exist at "${absolutePath}".` + ) + } + images.loader = 'custom' + images.loaderFile = absolutePath + } + if ( images.minimumCacheTTL && (!Number.isInteger(images.minimumCacheTTL) || images.minimumCacheTTL < 0) @@ -739,7 +756,7 @@ export default async function loadConfig( let configFileName = 'next.config.js' if (customConfig) { - return assignDefaults({ + return assignDefaults(dir, { configOrigin: 'server', configFileName, ...customConfig, @@ -818,7 +835,7 @@ export default async function loadConfig( : canonicalBase) || '' } - return assignDefaults({ + return assignDefaults(dir, { configOrigin: relative(dir, path), configFile: path, configFileName, @@ -846,7 +863,10 @@ export default async function loadConfig( // always call assignDefaults to ensure settings like // reactRoot can be updated correctly even with no next.config.js - const completeConfig = assignDefaults(defaultConfig) as NextConfigComplete + const completeConfig = assignDefaults( + dir, + defaultConfig + ) as NextConfigComplete completeConfig.configFileName = configFileName setHttpClientAndAgentOptions(completeConfig) return completeConfig diff --git a/packages/next/shared/lib/image-config.ts b/packages/next/shared/lib/image-config.ts index cfa172dc0b589..d72a4caacaab7 100644 --- a/packages/next/shared/lib/image-config.ts +++ b/packages/next/shared/lib/image-config.ts @@ -49,12 +49,15 @@ export type ImageConfigComplete = { /** @see [Image sizing documentation](https://nextjs.org/docs/basic-features/image-optimization#image-sizing) */ imageSizes: number[] - /** @see [Image loaders configuration](https://nextjs.org/docs/basic-features/image-optimization#loaders) */ + /** @see [Image loaders configuration](https://nextjs.org/docs/api-reference/next/legacy/image#loader) */ loader: LoaderValue - /** @see [Image loader configuration](https://nextjs.org/docs/api-reference/next/image#loader-configuration) */ + /** @see [Image loader configuration](https://nextjs.org/docs/api-reference/next/legacy/image#loader-configuration) */ path: string + /** @see [Image loader configuration](https://nextjs.org/docs/api-reference/next/image#loader-configuration) */ + loaderFile: string + /** * @see [Image domains configuration](https://nextjs.org/docs/api-reference/next/image#domains) */ @@ -89,6 +92,7 @@ export const imageConfigDefault: ImageConfigComplete = { imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], path: '/_next/image', loader: 'default', + loaderFile: '', domains: [], disableStaticImages: false, minimumCacheTTL: 60, diff --git a/packages/next/shared/lib/image-loader.ts b/packages/next/shared/lib/image-loader.ts new file mode 100644 index 0000000000000..ef9d5b6cba90c --- /dev/null +++ b/packages/next/shared/lib/image-loader.ts @@ -0,0 +1,65 @@ +// TODO: change "any" to actual type +function defaultLoader({ config, src, width, quality }: any): string { + if (process.env.NODE_ENV !== 'production') { + const missingValues = [] + + // these should always be provided but make sure they are + if (!src) missingValues.push('src') + if (!width) missingValues.push('width') + + if (missingValues.length > 0) { + throw new Error( + `Next Image Optimization requires ${missingValues.join( + ', ' + )} to be provided. Make sure you pass them as props to the \`next/image\` component. Received: ${JSON.stringify( + { src, width, quality } + )}` + ) + } + + if (src.startsWith('//')) { + throw new Error( + `Failed to parse src "${src}" on \`next/image\`, protocol-relative URL (//) must be changed to an absolute URL (http:// or https://)` + ) + } + + if (!src.startsWith('/') && (config.domains || config.remotePatterns)) { + let parsedSrc: URL + try { + parsedSrc = new URL(src) + } catch (err) { + console.error(err) + throw new Error( + `Failed to parse src "${src}" on \`next/image\`, if using relative image it must start with a leading slash "/" or be an absolute URL (http:// or https://)` + ) + } + + if (process.env.NODE_ENV !== 'test') { + // We use dynamic require because this should only error in development + const { hasMatch } = require('./match-remote-pattern') + 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` + ) + } + } + } + } + + if (src.endsWith('.svg') && !config.dangerouslyAllowSVG) { + // Special case to make svg serve as-is to avoid proxying + // through the built-in Image Optimization API. + return src + } + + return `${config.path}?url=${encodeURIComponent(src)}&w=${width}&q=${ + quality || 75 + }` +} + +// We use this to determine if the import is the default loader +// or a custom loader defined by the user in next.config.js +defaultLoader.__next_img_default = true + +export default defaultLoader diff --git a/test/integration/export-image-loader-legacy/next.config.js b/test/integration/export-image-loader-legacy/next.config.js new file mode 100644 index 0000000000000..6b05babba9373 --- /dev/null +++ b/test/integration/export-image-loader-legacy/next.config.js @@ -0,0 +1,2 @@ +// prettier-ignore +module.exports = { /* replaceme */ } diff --git a/test/integration/export-image-loader-legacy/pages/index.js b/test/integration/export-image-loader-legacy/pages/index.js new file mode 100644 index 0000000000000..78287bf05e936 --- /dev/null +++ b/test/integration/export-image-loader-legacy/pages/index.js @@ -0,0 +1,10 @@ +import Image from 'next/legacy/image' + +const loader = undefined + +export default () => ( +
+

Should succeed during export

+ icon +
+) diff --git a/test/integration/export-image-loader-legacy/test/index.test.js b/test/integration/export-image-loader-legacy/test/index.test.js new file mode 100644 index 0000000000000..a15d38215ec7f --- /dev/null +++ b/test/integration/export-image-loader-legacy/test/index.test.js @@ -0,0 +1,142 @@ +/* eslint-env jest */ + +import fs from 'fs-extra' +import { join } from 'path' +import cheerio from 'cheerio' +import { nextBuild, nextExport, File } from 'next-test-utils' + +const appDir = join(__dirname, '../') +const outdir = join(appDir, 'out') +const nextConfig = new File(join(appDir, 'next.config.js')) +const pagesIndexJs = new File(join(appDir, 'pages', 'index.js')) + +describe('Export with cloudinary loader next/legacy/image component', () => { + beforeAll(async () => { + await nextConfig.replace( + '{ /* replaceme */ }', + JSON.stringify({ + images: { + loader: 'cloudinary', + path: 'https://example.com/', + }, + }) + ) + }) + it('should build successfully', async () => { + await fs.remove(join(appDir, '.next')) + const { code } = await nextBuild(appDir) + if (code !== 0) throw new Error(`build failed with status ${code}`) + }) + + it('should export successfully', async () => { + const { code } = await nextExport(appDir, { outdir }) + if (code !== 0) throw new Error(`export failed with status ${code}`) + }) + + it('should contain img element in html output', async () => { + const html = await fs.readFile(join(outdir, 'index.html')) + const $ = cheerio.load(html) + expect($('img[alt="icon"]').attr('alt')).toBe('icon') + }) + + afterAll(async () => { + await nextConfig.restore() + }) +}) + +describe('Export with custom loader next/legacy/image component', () => { + beforeAll(async () => { + await nextConfig.replace( + '{ /* replaceme */ }', + JSON.stringify({ + images: { + loader: 'custom', + }, + }) + ) + await pagesIndexJs.replace( + 'loader = undefined', + 'loader = ({src}) => "/custom" + src' + ) + }) + it('should build successfully', async () => { + await fs.remove(join(appDir, '.next')) + const { code } = await nextBuild(appDir) + if (code !== 0) throw new Error(`build failed with status ${code}`) + }) + + it('should export successfully', async () => { + const { code } = await nextExport(appDir, { outdir }) + if (code !== 0) throw new Error(`export failed with status ${code}`) + }) + + it('should contain img element with same src in html output', async () => { + const html = await fs.readFile(join(outdir, 'index.html')) + const $ = cheerio.load(html) + expect($('img[src="/custom/o.png"]')).toBeDefined() + }) + + afterAll(async () => { + await nextConfig.restore() + await pagesIndexJs.restore() + }) +}) + +describe('Export with custom loader config but no loader prop on next/legacy/image', () => { + beforeAll(async () => { + await nextConfig.replace( + '{ /* replaceme */ }', + JSON.stringify({ + images: { + loader: 'custom', + }, + }) + ) + }) + it('should fail build', async () => { + await fs.remove(join(appDir, '.next')) + const { code, stderr } = await nextBuild(appDir, [], { stderr: true }) + expect(code).toBe(1) + expect(stderr).toContain( + 'Error: Image with src "/i.png" is missing "loader" prop' + ) + }) + + afterAll(async () => { + await nextConfig.restore() + await pagesIndexJs.restore() + }) +}) + +describe('Export with unoptimized next/legacy/image component', () => { + beforeAll(async () => { + await nextConfig.replace( + '{ /* replaceme */ }', + JSON.stringify({ + images: { + unoptimized: true, + }, + }) + ) + }) + it('should build successfully', async () => { + await fs.remove(join(appDir, '.next')) + const { code } = await nextBuild(appDir) + if (code !== 0) throw new Error(`build failed with status ${code}`) + }) + + it('should export successfully', async () => { + const { code } = await nextExport(appDir, { outdir }) + if (code !== 0) throw new Error(`export failed with status ${code}`) + }) + + it('should contain img element with same src in html output', async () => { + const html = await fs.readFile(join(outdir, 'index.html')) + const $ = cheerio.load(html) + expect($('img[src="/o.png"]')).toBeDefined() + }) + + afterAll(async () => { + await nextConfig.restore() + }) +}) diff --git a/test/integration/export-image-loader/dummy-loader.js b/test/integration/export-image-loader/dummy-loader.js new file mode 100644 index 0000000000000..63c101aa95528 --- /dev/null +++ b/test/integration/export-image-loader/dummy-loader.js @@ -0,0 +1,3 @@ +export default function dummyLoader({ src, width, quality }) { + return `${src}#w:${width},q:${quality || 50}` +} diff --git a/test/integration/export-image-loader/test/index.test.js b/test/integration/export-image-loader/test/index.test.js index 4f221aff2ffae..be3db688fa968 100644 --- a/test/integration/export-image-loader/test/index.test.js +++ b/test/integration/export-image-loader/test/index.test.js @@ -82,6 +82,67 @@ describe('Export with custom loader next/image component', () => { }) }) +describe('Export with custom loader config but no loader prop on next/image', () => { + beforeAll(async () => { + await nextConfig.replace( + '{ /* replaceme */ }', + JSON.stringify({ + images: { + loader: 'custom', + }, + }) + ) + }) + it('should fail build', async () => { + await fs.remove(join(appDir, '.next')) + const { code, stderr } = await nextBuild(appDir, [], { stderr: true }) + expect(code).toBe(1) + expect(stderr).toContain( + 'Error: Image with src "/i.png" is missing "loader" prop' + ) + }) + + afterAll(async () => { + await nextConfig.restore() + await pagesIndexJs.restore() + }) +}) + +describe('Export with loaderFile config next/image component', () => { + beforeAll(async () => { + await nextConfig.replace( + '{ /* replaceme */ }', + JSON.stringify({ + images: { + loader: 'custom', + loaderFile: './dummy-loader.js', + }, + }) + ) + }) + it('should build successfully', async () => { + await fs.remove(join(appDir, '.next')) + const { code } = await nextBuild(appDir) + if (code !== 0) throw new Error(`build failed with status ${code}`) + }) + + it('should export successfully', async () => { + const { code } = await nextExport(appDir, { outdir }) + if (code !== 0) throw new Error(`export failed with status ${code}`) + }) + + it('should contain img element with same src in html output', async () => { + const html = await fs.readFile(join(outdir, 'index.html')) + const $ = cheerio.load(html) + expect($('img[src="/i.png#w:32,q:50"]')).toBeDefined() + }) + + afterAll(async () => { + await nextConfig.restore() + await pagesIndexJs.restore() + }) +}) + describe('Export with unoptimized next/image component', () => { beforeAll(async () => { await nextConfig.replace( diff --git a/test/integration/image-optimizer/test/index.test.ts b/test/integration/image-optimizer/test/index.test.ts index 4182101099c4b..cc31e3e7433bd 100644 --- a/test/integration/image-optimizer/test/index.test.ts +++ b/test/integration/image-optimizer/test/index.test.ts @@ -274,6 +274,56 @@ describe('Image Optimizer', () => { ) }) + it('should error when images.loader and images.loaderFile are both assigned', async () => { + await nextConfig.replace( + '{ /* replaceme */ }', + JSON.stringify({ + images: { + loader: 'imgix', + path: 'https://example.com', + loaderFile: './dummy.js', + }, + }) + ) + let stderr = '' + + app = await launchApp(appDir, await findPort(), { + onStderr(msg) { + stderr += msg || '' + }, + }) + await waitFor(1000) + await killApp(app).catch(() => {}) + await nextConfig.restore() + + expect(stderr).toContain( + `Specified images.loader property (imgix) cannot be used with images.loaderFile property. Please set images.loader to "custom".` + ) + }) + + it('should error when images.loaderFile does not exist', async () => { + await nextConfig.replace( + '{ /* replaceme */ }', + JSON.stringify({ + images: { + loaderFile: './fakefile.js', + }, + }) + ) + let stderr = '' + + app = await launchApp(appDir, await findPort(), { + onStderr(msg) { + stderr += msg || '' + }, + }) + await waitFor(1000) + await killApp(app).catch(() => {}) + await nextConfig.restore() + + expect(stderr).toContain(`Specified images.loaderFile does not exist at`) + }) + it('should error when images.dangerouslyAllowSVG is not a boolean', async () => { await nextConfig.replace( '{ /* replaceme */ }', diff --git a/test/integration/next-image-new/loader-config/dummy-loader.js b/test/integration/next-image-new/loader-config/dummy-loader.js new file mode 100644 index 0000000000000..63c101aa95528 --- /dev/null +++ b/test/integration/next-image-new/loader-config/dummy-loader.js @@ -0,0 +1,3 @@ +export default function dummyLoader({ src, width, quality }) { + return `${src}#w:${width},q:${quality || 50}` +} diff --git a/test/integration/next-image-new/loader-config/next.config.js b/test/integration/next-image-new/loader-config/next.config.js new file mode 100644 index 0000000000000..55555d3c3cdfc --- /dev/null +++ b/test/integration/next-image-new/loader-config/next.config.js @@ -0,0 +1,6 @@ +module.exports = { + images: { + loader: 'custom', + loaderFile: './dummy-loader.js', + }, +} diff --git a/test/integration/next-image-new/loader-config/pages/index.js b/test/integration/next-image-new/loader-config/pages/index.js new file mode 100644 index 0000000000000..e246516278ae4 --- /dev/null +++ b/test/integration/next-image-new/loader-config/pages/index.js @@ -0,0 +1,35 @@ +import React from 'react' +import Image from 'next/image' + +function loader({ src, width, quality }) { + return `${src}?wid=${width}&qual=${quality || 35}` +} + +const Page = () => { + return ( +
+

Loader Config

+ img1 +

Scroll down...

+
+

Loader Prop

+ img2 +
+ ) +} + +export default Page diff --git a/test/integration/next-image-new/loader-config/public/logo.png b/test/integration/next-image-new/loader-config/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e14fafc5cf3bc63b70914ad20467f40f7fecd572 GIT binary patch literal 1545 zcmbVM{Xf$Q9A6%~&O#i9S`$MamWNGTo{nv7c{rq_?QHVUW~E6OQi@{ZJcmZ|?7l@Q zzFgO>LgSLHn=t)6BZh%t7EPMfk1SL z1Y86JvZ4In(b7~asTB_EYLbzJ#fBxtCqp2a7u(A{gEak&&i%OE5K&=dA02(f0EgVb zDQO?EwAgXhbPx#1STW3~N_6+*i-&gO&5gIVD)qtd)=yh(VkE{hpxOq=E?Uo-)5z*x z!Au!iA$YiLAm+*0qggP>?VsKD-2i&HQxQ3+OqX*8S}wK5H8(1QM_f{Jya%lp;-fFQ z-RxdA9ea)1aI;`EXvn#9J~1_}n?bl%WsA3~x1yF~ZJY?F%5TY1f>Os{GDi>X>C?IS zC87Oo3ZX}KJ*U`mZ%63leZQDa&ij+|L2Ig&kv$8+G!kJ)!A>IpI0!SpvZ=R*dmxwE z_A02!zif^Xi?D&?&%f0Tzbc>bI(#PkQsao89{0s~R(I*hM>py`YIH=n8s(l<+!VhFb)fj#H;uE`npo7 zY;0_#QmGRY6Algzb}0{05Qr9vi1UjyHCq}CIyy~&Xo)lk4660;XBm=IbzH;Vwux!6 z@U`%Q<6`U_r^#vHXzMH%_g}z&^bvih;Naksl&3F)p7Kn#$+goa*xhsUD|t?H%CawT z>JQ8!^fPzDF6c8waZPU1$^P~{X*y_EN`KC=6nc}~iEX#>ud*u)-GT=qZK~K!#eMKri|K2@v zeX7|gqiZ-a27vkY(m>jlb*A45J^WhNqUd5svx=i!WlyGoDxyIkDCJw8 zl1RKs=y0j+xtSIh@AZ-SU-~z%d7|iJXK0I}nj!QZ_;_V0t%N>WpH)B+RT91Kkuhzx zSp{CL@O&X!puOb5enarY#IKV0$GfaZ<5QCF#q6Ih66Bl1Pk?cT!sCl5^YK4KUf8=r z`aO#WUfA<6@Z|tBgFYm!h8b-eKV4c&$3bTW&<9YGGZ&`xG#9~EHI4;**~o$2bOc^F z)xqxjhTZjF)wtZ04Ns<6mIBW?61;SKUp&Ix#QrYF;SY_@rCeH2X2*tJ$*pAIHb zh#ej+0ZbcVCs7JzV7TsL6Jyyhc?vBAKW|d~E=#`(Epz?bhZI(;xeQ`sbe2CXvFp-!)9gAPmnDWWTsf>26XSP@ zv&2i`WrNZNf%ZoawxTiv7?Jj|6+NW@o>r`=449DMidcqyfhe1CUhQqXbvCSyC1#>! z&TQ9Zpp%MX zY5qJSn%bSF+=@PAVhp9?wWsW-al19&OZPE literal 0 HcmV?d00001 diff --git a/test/integration/next-image-new/loader-config/test/index.test.ts b/test/integration/next-image-new/loader-config/test/index.test.ts new file mode 100644 index 0000000000000..fecd5d4f8bb22 --- /dev/null +++ b/test/integration/next-image-new/loader-config/test/index.test.ts @@ -0,0 +1,76 @@ +/* eslint-env jest */ + +import { + findPort, + killApp, + launchApp, + nextBuild, + nextStart, +} from 'next-test-utils' +import webdriver from 'next-webdriver' +import { join } from 'path' + +const appDir = join(__dirname, '../') + +let appPort +let app +let browser + +function runTests() { + it('should add "src" to img1 based on the loader config', async () => { + expect(await browser.elementById('img1').getAttribute('src')).toBe( + '/logo.png#w:828,q:50' + ) + }) + + it('should add "srcset" to img1 based on the loader config', async () => { + expect(await browser.elementById('img1').getAttribute('srcset')).toBe( + '/logo.png#w:640,q:50 1x, /logo.png#w:828,q:50 2x' + ) + }) + + it('should add "src" to img2 based on the loader prop', async () => { + expect(await browser.elementById('img2').getAttribute('src')).toBe( + '/logo.png?wid=640&qual=35' + ) + }) + + it('should add "srcset" to img2 based on the loader prop', async () => { + expect(await browser.elementById('img2').getAttribute('srcset')).toBe( + '/logo.png?wid=256&qual=35 1x, /logo.png?wid=640&qual=35 2x' + ) + }) +} + +describe('Image Loader Config', () => { + describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + browser = await webdriver(appPort, '/') + }) + afterAll(() => { + killApp(app) + if (browser) { + browser.close() + } + }) + runTests() + }) + + describe('server mode', () => { + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + browser = await webdriver(appPort, '/') + }) + afterAll(() => { + killApp(app) + if (browser) { + browser.close() + } + }) + runTests() + }) +}) From 14c9376899f0c5c74c7e96928d09070d762fd174 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Sat, 22 Oct 2022 00:20:36 +0200 Subject: [PATCH 2/3] BREAKING CHANGE: Remove React 17 (#41629) Next.js 13 will require React 18. In this PR I've only updated the peerDependency and removed the test runs in GH actions. Further cleanup will follow later, this allows us to remove the code supporting it later. ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com> --- .github/workflows/build_test_deploy.yml | 288 ------------------ bench/minimal-server/start.js | 1 - docs/upgrading.md | 2 + examples/auth-with-stytch/package.json | 4 +- examples/cms-drupal/package.json | 4 +- examples/cms-keystonejs-embedded/package.json | 4 +- examples/with-eslint/package.json | 4 +- examples/with-formspree/package.json | 4 +- examples/with-jotai/package.json | 4 +- examples/with-mdx/package.json | 4 +- examples/with-mysql/package.json | 4 +- examples/with-next-sass/package.json | 4 +- examples/with-playwright/package.json | 4 +- package.json | 1 - packages/next/build/webpack-config.ts | 6 +- packages/next/package.json | 4 +- test/e2e/app-dir/app-alias.test.ts | 5 - test/e2e/app-dir/app-edge.test.ts | 5 - test/e2e/app-dir/app-middleware.test.ts | 5 - test/e2e/app-dir/app-static.test.ts | 4 - test/e2e/app-dir/asset-prefix.test.ts | 4 - .../app-dir/back-button-download-bug.test.ts | 4 - test/e2e/app-dir/index.test.ts | 4 - test/e2e/app-dir/next-font.test.ts | 4 - test/e2e/app-dir/next-image.test.ts | 4 - test/e2e/app-dir/prefetching.test.ts | 4 - test/e2e/app-dir/rendering.test.ts | 5 - test/e2e/app-dir/root-layout.test.ts | 4 - test/e2e/app-dir/trailingslash.test.ts | 4 - test/e2e/app-dir/vercel-analytics.test.ts | 4 - test/e2e/app-dir/with-babel.test.ts | 4 - .../index.test.ts | 5 - test/e2e/next-script/index.test.ts | 38 ++- .../test/index.test.js | 5 +- .../react-18-invalid-config/.gitignore | 1 - .../react-18-invalid-config/components/foo.js | 1 - .../react-18-invalid-config/index.test.js | 64 ---- .../react-18-invalid-config/next.config.js | 1 - .../react-18-invalid-config/pages/dynamic.js | 12 - .../react-18-invalid-config/pages/index.js | 3 - test/lib/react-channel-require-hook.js | 2 +- test/production/jest/index.test.ts | 6 - .../production/jest/new-link-behavior.test.ts | 6 - test/production/jest/relay/relay-jest.test.ts | 6 - .../remove-react-properties-jest.test.ts | 6 - 45 files changed, 45 insertions(+), 517 deletions(-) delete mode 100644 test/integration/react-18-invalid-config/.gitignore delete mode 100644 test/integration/react-18-invalid-config/components/foo.js delete mode 100644 test/integration/react-18-invalid-config/index.test.js delete mode 100644 test/integration/react-18-invalid-config/next.config.js delete mode 100644 test/integration/react-18-invalid-config/pages/dynamic.js delete mode 100644 test/integration/react-18-invalid-config/pages/index.js diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index 1d369657b7b77..dd96a4a13bc69 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -316,82 +316,6 @@ jobs: path: | test/traces - testDevReact17: - name: Test Development (react v17) - runs-on: ubuntu-latest - needs: [build, build-native-test] - env: - NEXT_TELEMETRY_DISABLED: 1 - NEXT_TEST_JOB: 1 - NEXT_TEST_REACT_VERSION: ^17 - strategy: - fail-fast: false - matrix: - group: [1, 2] - steps: - - name: Setup node - uses: actions/setup-node@v3 - if: ${{needs.build.outputs.docsChange == 'nope'}} - with: - node-version: 16 - check-latest: true - - - run: echo ${{needs.build.outputs.docsChange}} - - # https://github.com/actions/virtual-environments/issues/1187 - - name: tune linux network - run: sudo ethtool -K eth0 tx off rx off - - - uses: actions/cache@v3 - if: ${{needs.build.outputs.docsChange == 'nope'}} - id: restore-build - with: - path: ./* - key: ${{ github.sha }}-${{ github.run_number }} - - - uses: actions/download-artifact@v3 - if: ${{needs.build.outputs.docsChange == 'nope'}} - with: - name: next-swc-test-binary - path: packages/next-swc/native - - - run: npm i -g pnpm@${PNPM_VERSION} - if: ${{needs.build.outputs.docsChange == 'nope'}} - - - run: npm i -g playwright-chromium@1.22.2 && npx playwright install-deps - timeout-minutes: 10 - if: ${{needs.build.outputs.docsChange == 'nope'}} - - - run: npx @replayio/playwright install chromium - if: ${{needs.build.outputs.docsChange == 'nope'}} - - - run: node run-tests.js --type development --timings -g ${{ matrix.group }}/2 - name: Run test/development - if: ${{needs.build.outputs.docsChange == 'nope'}} - env: - RECORD_REPLAY_METADATA_TEST_RUN_TITLE: testDevReact17 / Group ${{ matrix.group }} - RECORD_ALL_CONTENT: 1 - RECORD_REPLAY: 1 - RECORD_REPLAY_TEST_METRICS: 1 - RECORD_REPLAY_WEBHOOK_URL: ${{ secrets.RECORD_REPLAY_WEBHOOK_URL }} - - - uses: replayio/action-upload@v0.4.5 - if: always() - with: - api-key: rwk_iKsQnEoQwKd31WAJxgN9ARPFuAlyXlVrDH4uhYpRnti - public: true - filter: ${{ 'function($v) { $v.metadata.test.result = "failed" }' }} - - - name: Upload test trace - if: always() - uses: actions/upload-artifact@v3 - with: - name: test-trace - if-no-files-found: ignore - retention-days: 2 - path: | - test/traces - testDevE2E: name: Test Development (E2E) runs-on: ubuntu-latest @@ -471,84 +395,6 @@ jobs: path: | test/traces - testDevE2EReact17: - name: Test Development (E2E) (react v17) - runs-on: ubuntu-latest - needs: [build, build-native-test] - env: - NEXT_TELEMETRY_DISABLED: 1 - NEXT_TEST_JOB: 1 - NEXT_TEST_REACT_VERSION: ^17 - TEST_TIMINGS_TOKEN: ${{ secrets.TEST_TIMINGS_TOKEN }} - strategy: - fail-fast: false - matrix: - group: [1, 2, 3] - steps: - - name: Setup node - uses: actions/setup-node@v3 - if: ${{needs.build.outputs.docsChange == 'nope'}} - with: - node-version: 16 - check-latest: true - - - run: echo ${{needs.build.outputs.docsChange}} - - # https://github.com/actions/virtual-environments/issues/1187 - - name: tune linux network - run: sudo ethtool -K eth0 tx off rx off - - - uses: actions/cache@v3 - if: ${{needs.build.outputs.docsChange == 'nope'}} - id: restore-build - with: - path: ./* - key: ${{ github.sha }}-${{ github.run_number }} - - - uses: actions/download-artifact@v3 - if: ${{needs.build.outputs.docsChange == 'nope'}} - with: - name: next-swc-test-binary - path: packages/next-swc/native - - - run: npm i -g pnpm@${PNPM_VERSION} - if: ${{needs.build.outputs.docsChange == 'nope'}} - - - run: npm i -g playwright-chromium@1.22.2 && npx playwright install-deps - timeout-minutes: 10 - if: ${{needs.build.outputs.docsChange == 'nope'}} - - - run: npx @replayio/playwright install chromium - if: ${{needs.build.outputs.docsChange == 'nope'}} - - - run: node run-tests.js --type e2e --timings -g ${{ matrix.group }}/3 - name: Run test/e2e (dev) - if: ${{needs.build.outputs.docsChange == 'nope'}} - env: - RECORD_REPLAY_METADATA_TEST_RUN_TITLE: testDevE2EReact17 / Group ${{ matrix.group }} - RECORD_ALL_CONTENT: 1 - RECORD_REPLAY: 1 - NEXT_TEST_MODE: dev - RECORD_REPLAY_TEST_METRICS: 1 - RECORD_REPLAY_WEBHOOK_URL: ${{ secrets.RECORD_REPLAY_WEBHOOK_URL }} - - - uses: replayio/action-upload@v0.4.5 - if: always() - with: - api-key: rwk_iKsQnEoQwKd31WAJxgN9ARPFuAlyXlVrDH4uhYpRnti - public: true - filter: ${{ 'function($v) { $v.metadata.test.result = "failed" }' }} - - - name: Upload test trace - if: always() - uses: actions/upload-artifact@v3 - with: - name: test-trace - if-no-files-found: ignore - retention-days: 2 - path: | - test/traces - testProd: name: Test Production runs-on: ubuntu-latest @@ -616,72 +462,6 @@ jobs: public: true filter: ${{ 'function($v) { $v.metadata.test.result = "failed" }' }} - testProdReact17: - name: Test Production (react v17) - runs-on: ubuntu-latest - needs: [build, build-native-test] - env: - NEXT_TELEMETRY_DISABLED: 1 - NEXT_TEST_JOB: 1 - NEXT_TEST_REACT_VERSION: ^17 - strategy: - fail-fast: false - matrix: - group: [1, 2] - steps: - - name: Setup node - uses: actions/setup-node@v3 - if: ${{needs.build.outputs.docsChange == 'nope'}} - with: - node-version: 16 - check-latest: true - - - run: echo ${{needs.build.outputs.docsChange}} - - # https://github.com/actions/virtual-environments/issues/1187 - - name: tune linux network - run: sudo ethtool -K eth0 tx off rx off - - - uses: actions/cache@v3 - if: ${{needs.build.outputs.docsChange == 'nope'}} - id: restore-build - with: - path: ./* - key: ${{ github.sha }}-${{ github.run_number }} - - - uses: actions/download-artifact@v3 - if: ${{needs.build.outputs.docsChange == 'nope'}} - with: - name: next-swc-test-binary - path: packages/next-swc/native - - - run: npm i -g pnpm@${PNPM_VERSION} - if: ${{needs.build.outputs.docsChange == 'nope'}} - - - run: npm i -g playwright-chromium@1.22.2 && npx playwright install-deps - timeout-minutes: 10 - if: ${{needs.build.outputs.docsChange == 'nope'}} - - - run: npx @replayio/playwright install chromium - if: ${{needs.build.outputs.docsChange == 'nope'}} - - - run: node run-tests.js --type production --timings -g ${{ matrix.group }}/2 - name: Run test/production - if: ${{needs.build.outputs.docsChange == 'nope'}} - env: - RECORD_REPLAY_METADATA_TEST_RUN_TITLE: testProdReact17 / Group ${{ matrix.group }} - RECORD_ALL_CONTENT: 1 - RECORD_REPLAY: 1 - RECORD_REPLAY_TEST_METRICS: 1 - RECORD_REPLAY_WEBHOOK_URL: ${{ secrets.RECORD_REPLAY_WEBHOOK_URL }} - - - uses: replayio/action-upload@v0.4.5 - if: always() - with: - api-key: rwk_iKsQnEoQwKd31WAJxgN9ARPFuAlyXlVrDH4uhYpRnti - public: true - filter: ${{ 'function($v) { $v.metadata.test.result = "failed" }' }} - testProdE2E: name: Test Production (E2E) runs-on: ubuntu-latest @@ -751,74 +531,6 @@ jobs: public: true filter: ${{ 'function($v) { $v.metadata.test.result = "failed" }' }} - testProdE2EReact17: - name: Test Production (E2E) (react v17) - runs-on: ubuntu-latest - needs: [build, build-native-test] - env: - NEXT_TELEMETRY_DISABLED: 1 - NEXT_TEST_JOB: 1 - NEXT_TEST_REACT_VERSION: ^17 - TEST_TIMINGS_TOKEN: ${{ secrets.TEST_TIMINGS_TOKEN }} - strategy: - fail-fast: false - matrix: - group: [1, 2, 3] - steps: - - name: Setup node - uses: actions/setup-node@v3 - if: ${{needs.build.outputs.docsChange == 'nope'}} - with: - node-version: ${{ matrix.node }} - check-latest: true - - - run: echo ${{needs.build.outputs.docsChange}} - - # https://github.com/actions/virtual-environments/issues/1187 - - name: tune linux network - run: sudo ethtool -K eth0 tx off rx off - - - uses: actions/cache@v3 - if: ${{needs.build.outputs.docsChange == 'nope'}} - id: restore-build - with: - path: ./* - key: ${{ github.sha }}-${{ github.run_number }} - - - uses: actions/download-artifact@v3 - if: ${{needs.build.outputs.docsChange == 'nope'}} - with: - name: next-swc-test-binary - path: packages/next-swc/native - - - run: npm i -g pnpm@${PNPM_VERSION} - if: ${{needs.build.outputs.docsChange == 'nope'}} - - - run: npm i -g playwright-chromium@1.22.2 && npx playwright install-deps - timeout-minutes: 10 - if: ${{needs.build.outputs.docsChange == 'nope'}} - - - run: npx @replayio/playwright install chromium - if: ${{needs.build.outputs.docsChange == 'nope'}} - - - run: node run-tests.js --type e2e --timings -g ${{ matrix.group }}/3 - name: Run test/e2e (production) - if: ${{needs.build.outputs.docsChange == 'nope'}} - env: - RECORD_REPLAY_METADATA_TEST_RUN_TITLE: testProdE2EReact17 / Group ${{ matrix.group }} - RECORD_ALL_CONTENT: 1 - RECORD_REPLAY: 1 - NEXT_TEST_MODE: start - RECORD_REPLAY_TEST_METRICS: 1 - RECORD_REPLAY_WEBHOOK_URL: ${{ secrets.RECORD_REPLAY_WEBHOOK_URL }} - - - uses: replayio/action-upload@v0.4.5 - if: always() - with: - api-key: rwk_iKsQnEoQwKd31WAJxgN9ARPFuAlyXlVrDH4uhYpRnti - public: true - filter: ${{ 'function($v) { $v.metadata.test.result = "failed" }' }} - testIntegration: name: Test Integration runs-on: ubuntu-latest diff --git a/bench/minimal-server/start.js b/bench/minimal-server/start.js index ceee4f2e235a9..da3f91839d0c3 100644 --- a/bench/minimal-server/start.js +++ b/bench/minimal-server/start.js @@ -1,4 +1,3 @@ -process.env.__NEXT_REACT_CHANNEL = 'exp' process.env.NODE_ENV = 'production' require('../../test/lib/react-channel-require-hook') diff --git a/docs/upgrading.md b/docs/upgrading.md index f26370f29ed47..4827f10d00e2f 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -10,6 +10,8 @@ The [Supported Browsers](/docs/basic-features/supported-browsers-features.md) ha The minimum Node.js version has been bumped from 12.22.0 to 14.0.0, since 12.x has reached end-of-life. +The minimum React version has been bumped from 17.0.2 to 18.2.0. + The `swcMinify` configuration property was changed from `false` to `true`. See [Next.js Compiler](/docs/advanced-features/compiler.md) for more info. The `next/image` import was renamed to `next/legacy/image`. The `next/future/image` import was renamed to `next/image`. diff --git a/examples/auth-with-stytch/package.json b/examples/auth-with-stytch/package.json index b5bc962e228da..f34efb0de7939 100644 --- a/examples/auth-with-stytch/package.json +++ b/examples/auth-with-stytch/package.json @@ -9,8 +9,8 @@ "@stytch/stytch-react": "^3.0.3", "next": "12.0.7", "next-iron-session": "^4.2.0", - "react": "17.0.2", - "react-dom": "17.0.2", + "react": "18.2.0", + "react-dom": "18.2.0", "stytch": "^3.6.1" }, "devDependencies": { diff --git a/examples/cms-drupal/package.json b/examples/cms-drupal/package.json index bb693224fb396..c93fc4ed075fa 100644 --- a/examples/cms-drupal/package.json +++ b/examples/cms-drupal/package.json @@ -10,8 +10,8 @@ "date-fns": "2.28.0", "next": "latest", "next-drupal": "latest", - "react": "17.0.2", - "react-dom": "17.0.2" + "react": "18.2.0", + "react-dom": "18.2.0" }, "devDependencies": { "autoprefixer": "10.4.2", diff --git a/examples/cms-keystonejs-embedded/package.json b/examples/cms-keystonejs-embedded/package.json index 785cf691dd33c..2be4a5098b8ec 100644 --- a/examples/cms-keystonejs-embedded/package.json +++ b/examples/cms-keystonejs-embedded/package.json @@ -10,8 +10,8 @@ "@keystone-next/fields": "^9.0.0", "@keystone-next/keystone": "^18.0.0", "next": "10.2.2", - "react": "17.0.2", - "react-dom": "17.0.2" + "react": "18.2.0", + "react-dom": "18.2.0" }, "devDependencies": { "@types/react": "^17.0.6", diff --git a/examples/with-eslint/package.json b/examples/with-eslint/package.json index e9eab360f8b63..a2977eecec192 100644 --- a/examples/with-eslint/package.json +++ b/examples/with-eslint/package.json @@ -8,8 +8,8 @@ }, "dependencies": { "next": "latest", - "react": "^17.0.2", - "react-dom": "^17.0.2" + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { "eslint": "^7.24.0", diff --git a/examples/with-formspree/package.json b/examples/with-formspree/package.json index 97fbff8ea2ee5..7df56dfb41ef9 100644 --- a/examples/with-formspree/package.json +++ b/examples/with-formspree/package.json @@ -8,7 +8,7 @@ "dependencies": { "@formspree/react": "latest", "next": "latest", - "react": "17.0.2", - "react-dom": "17.0.2" + "react": "18.2.0", + "react-dom": "18.2.0" } } diff --git a/examples/with-jotai/package.json b/examples/with-jotai/package.json index a6c7163464753..d66058245fe33 100644 --- a/examples/with-jotai/package.json +++ b/examples/with-jotai/package.json @@ -8,8 +8,8 @@ "dependencies": { "jotai": "1.7.3", "next": "latest", - "react": "17.0.2", - "react-dom": "17.0.2" + "react": "18.2.0", + "react-dom": "18.2.0" }, "devDependencies": { "@types/react": "17.0.16", diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index 7e2bee5fad391..dd2aeec334866 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -10,7 +10,7 @@ "@mdx-js/react": "^1.6.18", "@next/mdx": "^9.1.1", "next": "latest", - "react": "^17.0.2", - "react-dom": "^17.0.2" + "react": "^18.2.0", + "react-dom": "^18.2.0" } } diff --git a/examples/with-mysql/package.json b/examples/with-mysql/package.json index 0284f6baa105a..47080a1648ca0 100644 --- a/examples/with-mysql/package.json +++ b/examples/with-mysql/package.json @@ -9,8 +9,8 @@ "dependencies": { "@prisma/client": "3.10.0", "next": "latest", - "react": "17.0.2", - "react-dom": "17.0.2" + "react": "18.2.0", + "react-dom": "18.2.0" }, "devDependencies": { "autoprefixer": "10.4.2", diff --git a/examples/with-next-sass/package.json b/examples/with-next-sass/package.json index 3a5972bfdcb25..f488a52f871ca 100644 --- a/examples/with-next-sass/package.json +++ b/examples/with-next-sass/package.json @@ -7,8 +7,8 @@ }, "dependencies": { "next": "latest", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", "sass": "1.26.3" } } diff --git a/examples/with-playwright/package.json b/examples/with-playwright/package.json index 9c98887da6a2e..555dec415467d 100644 --- a/examples/with-playwright/package.json +++ b/examples/with-playwright/package.json @@ -8,8 +8,8 @@ }, "dependencies": { "next": "latest", - "react": "17.0.2", - "react-dom": "17.0.2" + "react": "18.2.0", + "react-dom": "18.2.0" }, "devDependencies": { "@playwright/test": "^1.15.0" diff --git a/package.json b/package.json index ee7fcf8a55073..f35326d372399 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "lint-staged": "lint-staged", "next-with-deps": "./scripts/next-with-deps.sh", "next": "node --trace-deprecation --enable-source-maps packages/next/dist/bin/next", - "next-react-17": "__NEXT_REACT_CHANNEL=17 node --trace-deprecation --enable-source-maps -r ./test/lib/react-channel-require-hook.js packages/next/dist/bin/next", "next-no-sourcemaps": "node --trace-deprecation packages/next/dist/bin/next", "clean-trace-jaeger": "rm -rf test/integration/basic/.next && TRACE_TARGET=JAEGER node --trace-deprecation --enable-source-maps packages/next/dist/bin/next build test/integration/basic", "debug": "node --inspect packages/next/dist/bin/next", diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index b3a309077bb0a..375f2c760c338 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -574,11 +574,7 @@ export default async function getBaseWebpackConfig( // Only error in first one compiler (client) once if (isClient) { if (!hasReactRoot) { - if (config.experimental.runtime) { - throw new Error( - '`experimental.runtime` requires React 18 to be installed.' - ) - } + throw new Error('Next.js requires React 18.2.0 to be installed.') } } diff --git a/packages/next/package.json b/packages/next/package.json index 070c756fd81af..063a463b05ca8 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -84,8 +84,8 @@ "peerDependencies": { "fibers": ">= 3.1.0", "node-sass": "^6.0.0 || ^7.0.0", - "react": "^17.0.2 || ^18.0.0-0", - "react-dom": "^17.0.2 || ^18.0.0-0", + "react": "^18.0.0-0", + "react-dom": "^18.0.0-0", "sass": "^1.3.0" }, "peerDependenciesMeta": { diff --git a/test/e2e/app-dir/app-alias.test.ts b/test/e2e/app-dir/app-alias.test.ts index 97cfe86fab391..b6c0bf0697988 100644 --- a/test/e2e/app-dir/app-alias.test.ts +++ b/test/e2e/app-dir/app-alias.test.ts @@ -9,11 +9,6 @@ describe('app-dir alias handling', () => { return } - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } - let next: NextInstance beforeAll(async () => { diff --git a/test/e2e/app-dir/app-edge.test.ts b/test/e2e/app-dir/app-edge.test.ts index 94d1c17b81e48..f6c317e6894e5 100644 --- a/test/e2e/app-dir/app-edge.test.ts +++ b/test/e2e/app-dir/app-edge.test.ts @@ -9,11 +9,6 @@ describe('app-dir edge SSR', () => { return } - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } - let next: NextInstance beforeAll(async () => { diff --git a/test/e2e/app-dir/app-middleware.test.ts b/test/e2e/app-dir/app-middleware.test.ts index b92ffa9352e9e..91b79a72f52af 100644 --- a/test/e2e/app-dir/app-middleware.test.ts +++ b/test/e2e/app-dir/app-middleware.test.ts @@ -12,11 +12,6 @@ describe('app-dir with middleware', () => { return } - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } - let next: NextInstance afterAll(() => next.destroy()) diff --git a/test/e2e/app-dir/app-static.test.ts b/test/e2e/app-dir/app-static.test.ts index 6ace29735bca2..790e7af672390 100644 --- a/test/e2e/app-dir/app-static.test.ts +++ b/test/e2e/app-dir/app-static.test.ts @@ -15,10 +15,6 @@ describe('app-dir static/dynamic handling', () => { return } - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } let next: NextInstance beforeAll(async () => { diff --git a/test/e2e/app-dir/asset-prefix.test.ts b/test/e2e/app-dir/asset-prefix.test.ts index 412ade3ce57ea..d4df4f1cc3414 100644 --- a/test/e2e/app-dir/asset-prefix.test.ts +++ b/test/e2e/app-dir/asset-prefix.test.ts @@ -11,10 +11,6 @@ describe('app-dir assetPrefix handling', () => { return } - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } let next: NextInstance beforeAll(async () => { diff --git a/test/e2e/app-dir/back-button-download-bug.test.ts b/test/e2e/app-dir/back-button-download-bug.test.ts index 6b33d5e9fff21..707a6062bc07b 100644 --- a/test/e2e/app-dir/back-button-download-bug.test.ts +++ b/test/e2e/app-dir/back-button-download-bug.test.ts @@ -9,10 +9,6 @@ describe('app-dir back button download bug', () => { return } - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } let next: NextInstance beforeAll(async () => { diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index f9de21f69d17b..53987c63fc717 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -21,10 +21,6 @@ describe('app dir', () => { return } - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } let next: NextInstance function runTests() { diff --git a/test/e2e/app-dir/next-font.test.ts b/test/e2e/app-dir/next-font.test.ts index faab97b58644d..36d7f31907efb 100644 --- a/test/e2e/app-dir/next-font.test.ts +++ b/test/e2e/app-dir/next-font.test.ts @@ -12,10 +12,6 @@ describe('app dir next-font', () => { return } - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } let next: NextInstance beforeAll(async () => { diff --git a/test/e2e/app-dir/next-image.test.ts b/test/e2e/app-dir/next-image.test.ts index 871170b5d5ed4..017ed7d2eca16 100644 --- a/test/e2e/app-dir/next-image.test.ts +++ b/test/e2e/app-dir/next-image.test.ts @@ -11,10 +11,6 @@ describe('app dir next-image', () => { return } - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } let next: NextInstance beforeAll(async () => { diff --git a/test/e2e/app-dir/prefetching.test.ts b/test/e2e/app-dir/prefetching.test.ts index 317bec839e846..c50594b4d18e1 100644 --- a/test/e2e/app-dir/prefetching.test.ts +++ b/test/e2e/app-dir/prefetching.test.ts @@ -11,10 +11,6 @@ describe('app dir prefetching', () => { return } - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } let next: NextInstance beforeAll(async () => { diff --git a/test/e2e/app-dir/rendering.test.ts b/test/e2e/app-dir/rendering.test.ts index 494a8494eadd3..28350408f17e6 100644 --- a/test/e2e/app-dir/rendering.test.ts +++ b/test/e2e/app-dir/rendering.test.ts @@ -5,11 +5,6 @@ import path from 'path' import cheerio from 'cheerio' describe('app dir rendering', () => { - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } - if ((global as any).isNextDeploy) { it('should skip next deploy for now', () => {}) return diff --git a/test/e2e/app-dir/root-layout.test.ts b/test/e2e/app-dir/root-layout.test.ts index 1340a1674aec4..8acc240af87f9 100644 --- a/test/e2e/app-dir/root-layout.test.ts +++ b/test/e2e/app-dir/root-layout.test.ts @@ -12,10 +12,6 @@ describe('app-dir root layout', () => { return } - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } let next: NextInstance beforeAll(async () => { diff --git a/test/e2e/app-dir/trailingslash.test.ts b/test/e2e/app-dir/trailingslash.test.ts index 0b8ca82f75532..960c731e642f4 100644 --- a/test/e2e/app-dir/trailingslash.test.ts +++ b/test/e2e/app-dir/trailingslash.test.ts @@ -11,10 +11,6 @@ describe('app-dir trailingSlash handling', () => { return } - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } let next: NextInstance beforeAll(async () => { diff --git a/test/e2e/app-dir/vercel-analytics.test.ts b/test/e2e/app-dir/vercel-analytics.test.ts index 0e6e71d99b963..f6862ac4aa33d 100644 --- a/test/e2e/app-dir/vercel-analytics.test.ts +++ b/test/e2e/app-dir/vercel-analytics.test.ts @@ -12,10 +12,6 @@ describe('vercel analytics', () => { return } - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } let next: NextInstance function runTests({ assetPrefix }: { assetPrefix?: boolean }) { diff --git a/test/e2e/app-dir/with-babel.test.ts b/test/e2e/app-dir/with-babel.test.ts index 6244e00c7ed62..77e95497a1e2f 100644 --- a/test/e2e/app-dir/with-babel.test.ts +++ b/test/e2e/app-dir/with-babel.test.ts @@ -10,10 +10,6 @@ describe('with babel', () => { return } - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } let next: NextInstance beforeAll(async () => { diff --git a/test/e2e/edge-render-getserversideprops/index.test.ts b/test/e2e/edge-render-getserversideprops/index.test.ts index 74a4c4b4c92c3..c3ea23aca7eda 100644 --- a/test/e2e/edge-render-getserversideprops/index.test.ts +++ b/test/e2e/edge-render-getserversideprops/index.test.ts @@ -8,11 +8,6 @@ import escapeStringRegexp from 'escape-string-regexp' describe('edge-render-getserversideprops', () => { let next: NextInstance - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - it('should skip for react v17', () => {}) - return - } - beforeAll(async () => { next = await createNext({ files: new FileRef(join(__dirname, 'app')), diff --git a/test/e2e/next-script/index.test.ts b/test/e2e/next-script/index.test.ts index e7493a26460cc..5596fc44257da 100644 --- a/test/e2e/next-script/index.test.ts +++ b/test/e2e/next-script/index.test.ts @@ -13,7 +13,7 @@ describe('beforeInteractive in document Head', () => { 'pages/_document.js': ` import { Html, Head, Main, NextScript } from 'next/document' import Script from 'next/script' - + export default function Document() { return ( @@ -31,7 +31,7 @@ describe('beforeInteractive in document Head', () => { ) } `, - 'pages/index.js': ` + 'pages/index.js': ` export default function Home() { return ( <> @@ -42,8 +42,8 @@ describe('beforeInteractive in document Head', () => { `, }, dependencies: { - react: '17.0.2', - 'react-dom': '17.0.2', + react: 'latest', + 'react-dom': 'latest', }, }) }) @@ -74,7 +74,7 @@ describe('beforeInteractive in document body', () => { 'pages/_document.js': ` import { Html, Head, Main, NextScript } from 'next/document' import Script from 'next/script' - + export default function Document() { return ( @@ -91,7 +91,7 @@ describe('beforeInteractive in document body', () => { ) } `, - 'pages/index.js': ` + 'pages/index.js': ` export default function Home() { return ( <> @@ -102,8 +102,8 @@ describe('beforeInteractive in document body', () => { `, }, dependencies: { - react: '17.0.2', - 'react-dom': '17.0.2', + react: 'latest', + 'react-dom': 'latest', }, }) }) @@ -149,8 +149,8 @@ describe('experimental.nextScriptWorkers: false with no Partytown dependency', ( }, // TODO: @housseindjirdeh: verify React 18 functionality dependencies: { - react: '17.0.2', - 'react-dom': '17.0.2', + react: 'latest', + 'react-dom': 'latest', }, }) }) @@ -182,10 +182,6 @@ describe('experimental.nextScriptWorkers: true with required Partytown dependenc experimental: { nextScriptWorkers: true, }, - dependencies: { - react: '17', - 'react-dom': '17', - }, }, files: { 'pages/index.js': ` @@ -204,6 +200,8 @@ describe('experimental.nextScriptWorkers: true with required Partytown dependenc `, }, dependencies: { + react: 'latest', + 'react-dom': 'latest', '@builder.io/partytown': '0.4.2', }, }) @@ -261,15 +259,11 @@ describe('experimental.nextScriptWorkers: true with required Partytown dependenc experimental: { nextScriptWorkers: true, }, - dependencies: { - react: '17', - 'react-dom': '17', - }, }, files: { 'pages/index.js': ` import Script from 'next/script' - + export default function Page() { return ( <> @@ -281,6 +275,8 @@ describe('experimental.nextScriptWorkers: true with required Partytown dependenc `, }, dependencies: { + react: 'latest', + 'react-dom': 'latest', '@builder.io/partytown': '0.4.2', }, }) @@ -399,8 +395,8 @@ describe('experimental.nextScriptWorkers: true with config override', () => { }, dependencies: { '@builder.io/partytown': '0.4.2', - react: '17', - 'react-dom': '17', + react: 'latest', + 'react-dom': 'latest', }, }) }) diff --git a/test/integration/custom-error-page-exception/test/index.test.js b/test/integration/custom-error-page-exception/test/index.test.js index a8b540ab11eb1..da579761ef751 100644 --- a/test/integration/custom-error-page-exception/test/index.test.js +++ b/test/integration/custom-error-page-exception/test/index.test.js @@ -9,16 +9,15 @@ const nodeArgs = ['-r', join(appDir, '../../lib/react-channel-require-hook.js')] let appPort let app -describe('Custom error page exception', () => { +// TODO: re-enable with React 18 +describe.skip('Custom error page exception', () => { beforeAll(async () => { await nextBuild(appDir, undefined, { nodeArgs, - env: { __NEXT_REACT_CHANNEL: '17' }, }) appPort = await findPort() app = await nextStart(appDir, appPort, { nodeArgs, - env: { __NEXT_REACT_CHANNEL: '17' }, }) }) afterAll(() => killApp(app)) diff --git a/test/integration/react-18-invalid-config/.gitignore b/test/integration/react-18-invalid-config/.gitignore deleted file mode 100644 index b512c09d47662..0000000000000 --- a/test/integration/react-18-invalid-config/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules \ No newline at end of file diff --git a/test/integration/react-18-invalid-config/components/foo.js b/test/integration/react-18-invalid-config/components/foo.js deleted file mode 100644 index f759e165008e3..0000000000000 --- a/test/integration/react-18-invalid-config/components/foo.js +++ /dev/null @@ -1 +0,0 @@ -export default () => 'foo' diff --git a/test/integration/react-18-invalid-config/index.test.js b/test/integration/react-18-invalid-config/index.test.js deleted file mode 100644 index 107e07bcdef2f..0000000000000 --- a/test/integration/react-18-invalid-config/index.test.js +++ /dev/null @@ -1,64 +0,0 @@ -/* eslint-env jest */ - -import fs from 'fs-extra' -import { join } from 'path' -import { File, nextBuild } from 'next-test-utils' - -const appDir = __dirname -const nodeArgs = ['-r', join(appDir, '../../lib/react-channel-require-hook.js')] -const reactDomPackagePah = join(appDir, 'node_modules/react-dom') -const nextConfig = new File(join(appDir, 'next.config.js')) - -function writeNextConfig(config) { - const content = ` - module.exports = { experimental: ${JSON.stringify(config)} } - ` - nextConfig.write(content) -} - -describe('Invalid react 18 webpack config', () => { - it('should install react 18 when `experimental.runtime` is enabled', async () => { - writeNextConfig({ - runtime: 'experimental-edge', - }) - const { stderr } = await nextBuild(appDir, [], { - stderr: true, - nodeArgs, - env: { __NEXT_REACT_CHANNEL: '17' }, - }) - nextConfig.restore() - - expect(stderr).toContain( - '`experimental.runtime` requires React 18 to be installed.' - ) - }) -}) - -describe('React 17 with React 18 config', () => { - beforeAll(async () => { - await fs.mkdirp(reactDomPackagePah) - await fs.writeFile( - join(reactDomPackagePah, 'package.json'), - JSON.stringify({ name: 'react-dom', version: '17.0.0' }) - ) - writeNextConfig({}) - }) - afterAll(async () => { - await fs.remove(reactDomPackagePah) - nextConfig.restore() - }) - - it('suspense is not allowed in blocking rendering mode', async () => { - const { stderr, code } = await nextBuild(appDir, [], { - stderr: true, - nodeArgs, - env: { - __NEXT_REACT_CHANNEL: '17', - }, - }) - expect(stderr).toContain( - 'Invalid suspense option usage in next/dynamic. Read more: https://nextjs.org/docs/messages/invalid-dynamic-suspense' - ) - expect(code).toBe(1) - }) -}) diff --git a/test/integration/react-18-invalid-config/next.config.js b/test/integration/react-18-invalid-config/next.config.js deleted file mode 100644 index 4ba52ba2c8df6..0000000000000 --- a/test/integration/react-18-invalid-config/next.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {} diff --git a/test/integration/react-18-invalid-config/pages/dynamic.js b/test/integration/react-18-invalid-config/pages/dynamic.js deleted file mode 100644 index f54d136c7b5d5..0000000000000 --- a/test/integration/react-18-invalid-config/pages/dynamic.js +++ /dev/null @@ -1,12 +0,0 @@ -import React, { Suspense } from 'react' -import dynamic from 'next/dynamic' - -const Foo = dynamic(() => import('../components/foo'), { suspense: true }) - -export default function Dynamic() { - return ( - - - - ) -} diff --git a/test/integration/react-18-invalid-config/pages/index.js b/test/integration/react-18-invalid-config/pages/index.js deleted file mode 100644 index f7dff6a079465..0000000000000 --- a/test/integration/react-18-invalid-config/pages/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function Index() { - return 'index' -} diff --git a/test/lib/react-channel-require-hook.js b/test/lib/react-channel-require-hook.js index b4a1ab01e225b..4372215b3c6c9 100644 --- a/test/lib/react-channel-require-hook.js +++ b/test/lib/react-channel-require-hook.js @@ -1,7 +1,7 @@ const mod = require('module') // The value will be '17' or 'exp' to alias the actual react channel -const reactVersion = process.env.__NEXT_REACT_CHANNEL +const reactVersion = undefined const reactDir = `react-${reactVersion}` const reactDomDir = `react-dom-${reactVersion}` diff --git a/test/production/jest/index.test.ts b/test/production/jest/index.test.ts index e7a8e09d748d7..5b831c9026097 100644 --- a/test/production/jest/index.test.ts +++ b/test/production/jest/index.test.ts @@ -5,12 +5,6 @@ import { renderViaHTTP } from 'next-test-utils' describe('next/jest', () => { let next: NextInstance - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - // react testing library is specific to react version - it('should bail on react v17', () => {}) - return - } - beforeAll(async () => { next = await createNext({ files: { diff --git a/test/production/jest/new-link-behavior.test.ts b/test/production/jest/new-link-behavior.test.ts index 75da74a2c588e..9e05f15725703 100644 --- a/test/production/jest/new-link-behavior.test.ts +++ b/test/production/jest/new-link-behavior.test.ts @@ -4,12 +4,6 @@ import { NextInstance } from 'test/lib/next-modes/base' describe('next/jest newLinkBehavior', () => { let next: NextInstance - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - // react testing library is specific to react version - it('should bail on react v17', () => {}) - return - } - beforeAll(async () => { next = await createNext({ files: { diff --git a/test/production/jest/relay/relay-jest.test.ts b/test/production/jest/relay/relay-jest.test.ts index d4de223629108..d829a323fe421 100644 --- a/test/production/jest/relay/relay-jest.test.ts +++ b/test/production/jest/relay/relay-jest.test.ts @@ -7,12 +7,6 @@ const appDir = path.join(__dirname, 'app') describe('next/jest', () => { let next: NextInstance - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - // react testing library is specific to react version - it('should bail on react v17', () => {}) - return - } - beforeAll(async () => { next = await createNext({ files: { diff --git a/test/production/jest/remove-react-properties/remove-react-properties-jest.test.ts b/test/production/jest/remove-react-properties/remove-react-properties-jest.test.ts index 576fb43af3921..039365f1c69f3 100644 --- a/test/production/jest/remove-react-properties/remove-react-properties-jest.test.ts +++ b/test/production/jest/remove-react-properties/remove-react-properties-jest.test.ts @@ -8,12 +8,6 @@ const appDir = path.join(__dirname, 'app') describe('next/jest', () => { let next: NextInstance - if (process.env.NEXT_TEST_REACT_VERSION === '^17') { - // react testing library is specific to react version - it('should bail on react v17', () => {}) - return - } - beforeAll(async () => { next = await createNext({ files: { From 6fb9a192a71be4c28fd9ff596b212a2c880d6986 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 21 Oct 2022 18:26:42 -0400 Subject: [PATCH 3/3] Update `next-image-experimental` codemod to handle loaders (#41633) As a follow up to #41585, this PR updates the `next-image-experimental` codemod to change `loader` to `loaderFile`. --- docs/advanced-features/codemods.md | 2 +- .../akamai/input/next.config.js | 6 + .../akamai/output/akamai-loader.js | 4 + .../akamai/output/next.config.js | 6 + .../cloudinary/input/next.config.js | 6 + .../cloudinary/output/cloudinary-loader.js | 6 + .../cloudinary/output/next.config.js | 6 + .../imgix/input/next.config.js | 6 + .../imgix/output/imgix-loader.js | 10 ++ .../imgix/output/next.config.js | 6 + .../next-image-experimental-loader.test.js | 34 ++++++ .../transforms/next-image-experimental.ts | 106 ++++++++++++++++++ 12 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/akamai/input/next.config.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/akamai/output/akamai-loader.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/akamai/output/next.config.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/cloudinary/input/next.config.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/cloudinary/output/cloudinary-loader.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/cloudinary/output/next.config.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/imgix/input/next.config.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/imgix/output/imgix-loader.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/imgix/output/next.config.js create mode 100644 packages/next-codemod/transforms/__tests__/next-image-experimental-loader.test.js diff --git a/docs/advanced-features/codemods.md b/docs/advanced-features/codemods.md index 512872ed3ea77..a19777c251cba 100644 --- a/docs/advanced-features/codemods.md +++ b/docs/advanced-features/codemods.md @@ -88,7 +88,7 @@ Dangerously migrates from `next/legacy/image` to the new `next/image` by adding - Removes `objectPosition` prop and adds `style` - Removes `lazyBoundary` prop - Removes `lazyRoot` prop -- TODO: does not migrate the `loader` config. If you need it, you must manually add a `loader` prop. +- Changes next.config.js `loader` to "custom", removes `path`, and sets `loaderFile` to a new file. #### Before: intrinsic diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/akamai/input/next.config.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/akamai/input/next.config.js new file mode 100644 index 0000000000000..3f91075b744be --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/akamai/input/next.config.js @@ -0,0 +1,6 @@ +module.exports = { + images: { + loader: "akamai", + path: "https://example.com/", + }, +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/akamai/output/akamai-loader.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/akamai/output/akamai-loader.js new file mode 100644 index 0000000000000..555068c92b5ee --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/akamai/output/akamai-loader.js @@ -0,0 +1,4 @@ +const normalizeSrc = (src) => src[0] === '/' ? src.slice(1) : src +export default function akamaiLoader({ src, width, quality }) { +return 'https://example.com/' + normalizeSrc(src) + '?imwidth=' + width +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/akamai/output/next.config.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/akamai/output/next.config.js new file mode 100644 index 0000000000000..a6f03e360cfac --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/akamai/output/next.config.js @@ -0,0 +1,6 @@ +module.exports = { + images: { + loader: "custom", + loaderFile: "./akamai-loader.js", + }, +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/cloudinary/input/next.config.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/cloudinary/input/next.config.js new file mode 100644 index 0000000000000..089eb8205f339 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/cloudinary/input/next.config.js @@ -0,0 +1,6 @@ +module.exports = { + images: { + loader: "cloudinary", + path: "https://example.com/", + }, +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/cloudinary/output/cloudinary-loader.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/cloudinary/output/cloudinary-loader.js new file mode 100644 index 0000000000000..f0ac0c3209d6b --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/cloudinary/output/cloudinary-loader.js @@ -0,0 +1,6 @@ +const normalizeSrc = (src) => src[0] === '/' ? src.slice(1) : src +export default function cloudinaryLoader({ src, width, quality }) { +const params = ['f_auto', 'c_limit', 'w_' + width, 'q_' + (quality || 'auto')] +const paramsString = params.join(',') + '/' +return 'https://example.com/' + paramsString + normalizeSrc(src) +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/cloudinary/output/next.config.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/cloudinary/output/next.config.js new file mode 100644 index 0000000000000..27ea821d47e81 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/cloudinary/output/next.config.js @@ -0,0 +1,6 @@ +module.exports = { + images: { + loader: "custom", + loaderFile: "./cloudinary-loader.js", + }, +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/imgix/input/next.config.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/imgix/input/next.config.js new file mode 100644 index 0000000000000..8c8e249cb946b --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/imgix/input/next.config.js @@ -0,0 +1,6 @@ +module.exports = { + images: { + loader: "imgix", + path: "https://example.com/", + }, +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/imgix/output/imgix-loader.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/imgix/output/imgix-loader.js new file mode 100644 index 0000000000000..22468f5a87f5d --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/imgix/output/imgix-loader.js @@ -0,0 +1,10 @@ +const normalizeSrc = (src) => src[0] === '/' ? src.slice(1) : src +export default function imgixLoader({ src, width, quality }) { +const url = new URL('https://example.com/' + normalizeSrc(src)) +const params = url.searchParams +params.set('auto', params.getAll('auto').join(',') || 'format') +params.set('fit', params.get('fit') || 'max') +params.set('w', params.get('w') || width.toString()) +if (quality) { params.set('q', quality.toString()) } +return url.href +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/imgix/output/next.config.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/imgix/output/next.config.js new file mode 100644 index 0000000000000..1304e43f493a1 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/imgix/output/next.config.js @@ -0,0 +1,6 @@ +module.exports = { + images: { + loader: "custom", + loaderFile: "./imgix-loader.js", + }, +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__tests__/next-image-experimental-loader.test.js b/packages/next-codemod/transforms/__tests__/next-image-experimental-loader.test.js new file mode 100644 index 0000000000000..e783c2fc5589a --- /dev/null +++ b/packages/next-codemod/transforms/__tests__/next-image-experimental-loader.test.js @@ -0,0 +1,34 @@ +/* global jest */ +jest.autoMockOff() +const Runner = require('jscodeshift/dist/Runner'); +const { cp, mkdir, rm, readdir, readFile } = require('fs/promises') +const { join } = require('path') + +const fixtureDir = join(__dirname, '..', '__testfixtures__', 'next-image-experimental-loader') +const transform = join(__dirname, '..', 'next-image-experimental.js') +const opts = { recursive: true } + +async function toObj(dir) { + const obj = {} + const files = await readdir(dir) + for (const file of files) { + obj[file] = await readFile(join(dir, file), 'utf8') + } + return obj +} +it.each(['imgix', 'cloudinary', 'akamai'])('should transform loader %s', async (loader) => { + try { + await mkdir(join(fixtureDir, 'tmp'), opts) + await cp(join(fixtureDir, loader, 'input'), join(fixtureDir, 'tmp'), opts) + process.chdir(join(fixtureDir, 'tmp')) + const result = await Runner.run(transform, [`.`], {}) + expect(result.error).toBe(0) + expect( + await toObj(join(fixtureDir, 'tmp')) + ).toStrictEqual( + await toObj(join(fixtureDir, loader, 'output')) + ) + } finally { + await rm(join(fixtureDir, 'tmp'), opts) + } +}) \ No newline at end of file diff --git a/packages/next-codemod/transforms/next-image-experimental.ts b/packages/next-codemod/transforms/next-image-experimental.ts index 1514c8243424a..e787164962614 100644 --- a/packages/next-codemod/transforms/next-image-experimental.ts +++ b/packages/next-codemod/transforms/next-image-experimental.ts @@ -1,3 +1,4 @@ +import { writeFileSync } from 'fs' import type { API, Collection, @@ -141,6 +142,100 @@ function findAndReplaceProps( }) } +function nextConfigTransformer(j: JSCodeshift, root: Collection) { + let pathPrefix = '' + let loaderType = '' + root.find(j.ObjectExpression).forEach((o) => { + const [images] = o.value.properties || [] + if ( + images.type === 'Property' && + images.key.type === 'Identifier' && + images.key.name === 'images' && + images.value.type === 'ObjectExpression' && + images.value.properties + ) { + const properties = images.value.properties.filter((p) => { + if ( + p.type === 'Property' && + p.key.type === 'Identifier' && + p.key.name === 'loader' && + 'value' in p.value + ) { + if ( + p.value.value === 'imgix' || + p.value.value === 'cloudinary' || + p.value.value === 'akamai' + ) { + loaderType = p.value.value + p.value.value = 'custom' + } + } + if ( + p.type === 'Property' && + p.key.type === 'Identifier' && + p.key.name === 'path' && + 'value' in p.value + ) { + pathPrefix = String(p.value.value) + return false + } + return true + }) + if (loaderType && pathPrefix) { + let filename = `./${loaderType}-loader.js` + properties.push( + j.property('init', j.identifier('loaderFile'), j.literal(filename)) + ) + images.value.properties = properties + const normalizeSrc = `const normalizeSrc = (src) => src[0] === '/' ? src.slice(1) : src` + if (loaderType === 'imgix') { + writeFileSync( + filename, + `${normalizeSrc} + export default function imgixLoader({ src, width, quality }) { + const url = new URL('${pathPrefix}' + normalizeSrc(src)) + const params = url.searchParams + params.set('auto', params.getAll('auto').join(',') || 'format') + params.set('fit', params.get('fit') || 'max') + params.set('w', params.get('w') || width.toString()) + if (quality) { params.set('q', quality.toString()) } + return url.href + }` + .split('\n') + .map((l) => l.trim()) + .join('\n') + ) + } else if (loaderType === 'cloudinary') { + writeFileSync( + filename, + `${normalizeSrc} + export default function cloudinaryLoader({ src, width, quality }) { + const params = ['f_auto', 'c_limit', 'w_' + width, 'q_' + (quality || 'auto')] + const paramsString = params.join(',') + '/' + return '${pathPrefix}' + paramsString + normalizeSrc(src) + }` + .split('\n') + .map((l) => l.trim()) + .join('\n') + ) + } else if (loaderType === 'akamai') { + writeFileSync( + filename, + `${normalizeSrc} + export default function akamaiLoader({ src, width, quality }) { + return '${pathPrefix}' + normalizeSrc(src) + '?imwidth=' + width + }` + .split('\n') + .map((l) => l.trim()) + .join('\n') + ) + } + } + } + }) + return root +} + export default function transformer( file: FileInfo, api: API, @@ -149,6 +244,17 @@ export default function transformer( const j = api.jscodeshift const root = j(file.source) + const isConfig = + file.path === 'next.config.js' || + file.path === 'next.config.ts' || + file.path === 'next.config.mjs' || + file.path === 'next.config.cjs' + + if (isConfig) { + const result = nextConfigTransformer(j, root) + return result.toSource() + } + // Before: import Image from "next/legacy/image" // After: import Image from "next/image" root