From dbb412437a6e20ee904d92797ec6227ba106136d Mon Sep 17 00:00:00 2001 From: Toru Kobayashi Date: Tue, 9 May 2023 08:14:48 +0900 Subject: [PATCH 01/11] fix: better error message with an invalid assetPrefix (#49403) fixes #47641 This adds a friendly error for loading a font error caused by the invalid `assetPrefix` setting. Current: image This PR: image --------- Co-authored-by: JJ Kasper --- .../build/webpack/loaders/next-font-loader/index.ts | 9 +++++++++ .../fixtures/invalid-assertprefix/next.config.js | 3 +++ .../fixtures/invalid-assertprefix/pages/index.js | 10 ++++++++++ test/integration/font-optimization/test/index.test.js | 11 +++++++++++ 4 files changed, 33 insertions(+) create mode 100644 test/integration/font-optimization/fixtures/invalid-assertprefix/next.config.js create mode 100644 test/integration/font-optimization/fixtures/invalid-assertprefix/pages/index.js diff --git a/packages/next/src/build/webpack/loaders/next-font-loader/index.ts b/packages/next/src/build/webpack/loaders/next-font-loader/index.ts index 7bacdbe0839a2..87221932b26b1 100644 --- a/packages/next/src/build/webpack/loaders/next-font-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-font-loader/index.ts @@ -49,6 +49,15 @@ export default async function nextFontLoader(this: any) { postcss: getPostcss, } = this.getOptions() + if (assetPrefix && !/^\/|https?:\/\//.test(assetPrefix)) { + const err = new Error( + 'assetPrefix must start with a leading slash or be an absolute URL(http:// or https://)' + ) + err.name = 'NextFontError' + callback(err) + return + } + /** * Emit font files to .next/static/media as [hash].[ext]. * diff --git a/test/integration/font-optimization/fixtures/invalid-assertprefix/next.config.js b/test/integration/font-optimization/fixtures/invalid-assertprefix/next.config.js new file mode 100644 index 0000000000000..0e35ec361f683 --- /dev/null +++ b/test/integration/font-optimization/fixtures/invalid-assertprefix/next.config.js @@ -0,0 +1,3 @@ +module.exports = { + assetPrefix: '.', +} diff --git a/test/integration/font-optimization/fixtures/invalid-assertprefix/pages/index.js b/test/integration/font-optimization/fixtures/invalid-assertprefix/pages/index.js new file mode 100644 index 0000000000000..6c4bf4e7f3d8c --- /dev/null +++ b/test/integration/font-optimization/fixtures/invalid-assertprefix/pages/index.js @@ -0,0 +1,10 @@ +import React from 'react' +import { Inter } from 'next/font/google' + +const inter = Inter({ subsets: ['latin'] }) + +const Page = () => { + return
Hello
+} + +export default Page diff --git a/test/integration/font-optimization/test/index.test.js b/test/integration/font-optimization/test/index.test.js index d0c58172db94f..b0c7d804e8ff8 100644 --- a/test/integration/font-optimization/test/index.test.js +++ b/test/integration/font-optimization/test/index.test.js @@ -313,4 +313,15 @@ describe('Font Optimization', () => { ) }) }) + describe('invalid configuration', () => { + it('should show a proper error if assetPrefix starts with .', async () => { + const appDir = join(fixturesDir, 'invalid-assertprefix') + const { stderr } = await nextBuild(appDir, undefined, { + stderr: true, + }) + expect(stderr).toContain( + 'assetPrefix must start with a leading slash or be an absolute URL(http:// or https://)' + ) + }) + }) }) From 7a1bc235f506d60dd4803da660db71e6cc7b8189 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Mon, 8 May 2023 17:26:33 -0700 Subject: [PATCH 02/11] Update start release workflow inputs (#49492) Uses the choice input type instead of requiring manually typing the values. --- .github/workflows/trigger_release.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index 3e644c4203e04..db2d63a9ac242 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -2,13 +2,20 @@ on: workflow_dispatch: inputs: releaseType: - description: stable or canary (case sensitive)? + description: stable or canary? required: true - type: string + type: choice + options: + - canary + - stable semverType: - description: patch, minor, or major (case sensitive)? - type: string + description: semver type? + type: choice + options: + - patch + - minor + - major secrets: RELEASE_BOT_TOKEN: From 8d228780e72706ef4bd5b6327ede2c0340181353 Mon Sep 17 00:00:00 2001 From: vercel-release-bot Date: Tue, 9 May 2023 01:27:04 +0000 Subject: [PATCH 03/11] v13.4.2-canary.3 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- packages/eslint-config-next/package.json | 4 ++-- packages/eslint-plugin-next/package.json | 2 +- packages/font/package.json | 2 +- packages/next-bundle-analyzer/package.json | 2 +- packages/next-codemod/package.json | 2 +- packages/next-env/package.json | 2 +- packages/next-mdx/package.json | 2 +- packages/next-plugin-storybook/package.json | 2 +- packages/next-polyfill-module/package.json | 2 +- packages/next-polyfill-nomodule/package.json | 2 +- packages/next-swc/package.json | 2 +- packages/next/package.json | 14 +++++++------- packages/react-dev-overlay/package.json | 2 +- packages/react-refresh-utils/package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- 17 files changed, 30 insertions(+), 30 deletions(-) diff --git a/lerna.json b/lerna.json index 99bc54f482fbe..b1c8d1d5c9225 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "13.4.2-canary.2" + "version": "13.4.2-canary.3" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 08e958d992135..6dd425049a3d2 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "13.4.2-canary.2", + "version": "13.4.2-canary.3", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index b3df50322978d..a5ec589165be9 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "13.4.2-canary.2", + "version": "13.4.2-canary.3", "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": "13.4.2-canary.2", + "@next/eslint-plugin-next": "13.4.2-canary.3", "@rushstack/eslint-patch": "^1.1.3", "@typescript-eslint/parser": "^5.42.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 3c63dbea26625..380b7679e1aa4 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": "13.4.2-canary.2", + "version": "13.4.2-canary.3", "description": "ESLint plugin for NextJS.", "main": "dist/index.js", "license": "MIT", diff --git a/packages/font/package.json b/packages/font/package.json index e3735a2023806..6d16529ecd502 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "13.4.2-canary.2", + "version": "13.4.2-canary.3", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 50e254b0ffa9b..e12ba6bcf7f5c 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "13.4.2-canary.2", + "version": "13.4.2-canary.3", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 1b39fab5b05ba..ba189b45f3180 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "13.4.2-canary.2", + "version": "13.4.2-canary.3", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 42485e68f1d74..c432c934399d4 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "13.4.2-canary.2", + "version": "13.4.2-canary.3", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 55d2cc453b70b..48ad191a0630d 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "13.4.2-canary.2", + "version": "13.4.2-canary.3", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 2193248ad258d..175b537b9fb07 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "13.4.2-canary.2", + "version": "13.4.2-canary.3", "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 af074badb13f8..875c6968691f0 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "13.4.2-canary.2", + "version": "13.4.2-canary.3", "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 a0c023effdf74..f5afd45abd684 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "13.4.2-canary.2", + "version": "13.4.2-canary.3", "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 3703253fbec34..17e44e52e7fd5 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "13.4.2-canary.2", + "version": "13.4.2-canary.3", "private": true, "scripts": { "clean": "rm -rf ./native/*", diff --git a/packages/next/package.json b/packages/next/package.json index f079d1d4388de..772f70ff8d872 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "13.4.2-canary.2", + "version": "13.4.2-canary.3", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -83,7 +83,7 @@ ] }, "dependencies": { - "@next/env": "13.4.2-canary.2", + "@next/env": "13.4.2-canary.3", "@swc/helpers": "0.5.1", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", @@ -143,11 +143,11 @@ "@jest/types": "29.5.0", "@napi-rs/cli": "2.14.7", "@napi-rs/triples": "1.1.0", - "@next/polyfill-module": "13.4.2-canary.2", - "@next/polyfill-nomodule": "13.4.2-canary.2", - "@next/react-dev-overlay": "13.4.2-canary.2", - "@next/react-refresh-utils": "13.4.2-canary.2", - "@next/swc": "13.4.2-canary.2", + "@next/polyfill-module": "13.4.2-canary.3", + "@next/polyfill-nomodule": "13.4.2-canary.3", + "@next/react-dev-overlay": "13.4.2-canary.3", + "@next/react-refresh-utils": "13.4.2-canary.3", + "@next/swc": "13.4.2-canary.3", "@opentelemetry/api": "1.4.1", "@segment/ajv-human-errors": "2.1.2", "@taskr/clear": "1.1.0", diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index ea6e135738805..14b9fc4c75562 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": "13.4.2-canary.2", + "version": "13.4.2-canary.3", "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 6b1d3dc637ed4..c657875b69e0b 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": "13.4.2-canary.2", + "version": "13.4.2-canary.3", "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 0b414247a57e9..ca62165554348 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -464,7 +464,7 @@ importers: packages/eslint-config-next: specifiers: - '@next/eslint-plugin-next': 13.4.2-canary.2 + '@next/eslint-plugin-next': 13.4.2-canary.3 '@rushstack/eslint-patch': ^1.1.3 '@typescript-eslint/parser': ^5.42.0 eslint: ^7.23.0 || ^8.0.0 @@ -540,12 +540,12 @@ importers: '@jest/types': 29.5.0 '@napi-rs/cli': 2.14.7 '@napi-rs/triples': 1.1.0 - '@next/env': 13.4.2-canary.2 - '@next/polyfill-module': 13.4.2-canary.2 - '@next/polyfill-nomodule': 13.4.2-canary.2 - '@next/react-dev-overlay': 13.4.2-canary.2 - '@next/react-refresh-utils': 13.4.2-canary.2 - '@next/swc': 13.4.2-canary.2 + '@next/env': 13.4.2-canary.3 + '@next/polyfill-module': 13.4.2-canary.3 + '@next/polyfill-nomodule': 13.4.2-canary.3 + '@next/react-dev-overlay': 13.4.2-canary.3 + '@next/react-refresh-utils': 13.4.2-canary.3 + '@next/swc': 13.4.2-canary.3 '@opentelemetry/api': 1.4.1 '@segment/ajv-human-errors': 2.1.2 '@swc/helpers': 0.5.1 From 04799216ec4e8bb40c9742b250fb0f42d91bf2fa Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Mon, 8 May 2023 21:09:42 -0700 Subject: [PATCH 04/11] Fix failing actions e2e deploy test (#49497) x-ref: https://github.com/vercel/next.js/actions/runs/4921305860/jobs/8791173036 --- test/e2e/app-dir/actions/app-action.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts index b94b72f54ea8c..2cf0b7c6117d2 100644 --- a/test/e2e/app-dir/actions/app-action.test.ts +++ b/test/e2e/app-dir/actions/app-action.test.ts @@ -12,7 +12,7 @@ createNextDescribe( { files: __dirname, }, - ({ next, isNextDev }) => { + ({ next, isNextDev, isNextStart }) => { it('should handle basic actions correctly', async () => { const browser = await next.browser('/server') @@ -172,7 +172,7 @@ createNextDescribe( await check(() => browser.elementByCss('h1').text(), '3') }) - if (!isNextDev) { + if (isNextStart) { it('should not expose action content in sourcemaps', async () => { const sourcemap = ( await fs.readdir( From e570ad4e56b494e0b2c2824d3ef71f30741412d5 Mon Sep 17 00:00:00 2001 From: Wojciech Grzebieniowski Date: Tue, 9 May 2023 09:48:12 +0200 Subject: [PATCH 05/11] Fix Node Crypto polyfill (#49288) ### What? Allow overwriting the `global.crypto` property when polyfilling it. ### Why? #48941 introduced `global.crypto` polyfill. The problem is that if some library (e.g. [xksuid](https://github.com/ValeriaVG/xksuid/blob/main/src/index.node.mjs)) tries to do the same thing, it breaks as `global.crypto` is defined as non-writable[^1]. Arguably libraries should check for `global.crypto` presence before overwriting it BUT I think polyfill should match the actual implementation[^2]. ### How? Make `global.crypto` `enumerable` and `configurable`, as well as define `set` implementation[^3]. [^1]: ![image](https://user-images.githubusercontent.com/7079786/236440322-7bcf1b18-8fcc-4bb9-b9b4-0f2eb032f5ba.png) [^2]: ![image](https://user-images.githubusercontent.com/7079786/236437260-d3abdb0c-134f-4c9d-aab8-de7bf4d7c831.png) [^3]: ![image](https://user-images.githubusercontent.com/7079786/236440393-1c469035-a9f1-4fbe-9ce7-c0308e980510.png) --------- Co-authored-by: Tim Neutkens --- .../src/server/node-polyfill-crypto.test.ts | 11 +++++++++++ .../next/src/server/node-polyfill-crypto.ts | 17 ++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 packages/next/src/server/node-polyfill-crypto.test.ts diff --git a/packages/next/src/server/node-polyfill-crypto.test.ts b/packages/next/src/server/node-polyfill-crypto.test.ts new file mode 100644 index 0000000000000..8272642cc2528 --- /dev/null +++ b/packages/next/src/server/node-polyfill-crypto.test.ts @@ -0,0 +1,11 @@ +/* eslint-env jest */ +import './node-polyfill-crypto' + +describe('node-polyfill-crypto', () => { + test('overwrite crypto', async () => { + expect(global.crypto).not.toBeUndefined() + const a = {} as Crypto + global.crypto = a + expect(global.crypto).toBe(a) + }) +}) diff --git a/packages/next/src/server/node-polyfill-crypto.ts b/packages/next/src/server/node-polyfill-crypto.ts index 204f165872fee..cec97c6179556 100644 --- a/packages/next/src/server/node-polyfill-crypto.ts +++ b/packages/next/src/server/node-polyfill-crypto.ts @@ -1,12 +1,19 @@ // Polyfill crypto() in the Node.js environment -if (!(global as any).crypto) { - function getCryptoImpl() { - return require('node:crypto').webcrypto - } +if (!global.crypto) { + let webcrypto: Crypto | undefined + Object.defineProperty(global, 'crypto', { + enumerable: false, + configurable: true, get() { - return getCryptoImpl() + if (!webcrypto) { + webcrypto = require('node:crypto').webcrypto + } + return webcrypto + }, + set(value: Crypto) { + webcrypto = value }, }) } From d6c83a4fefc1dbadaf9249ac6ffb025f53b1ef6f Mon Sep 17 00:00:00 2001 From: Cesar Kohl Date: Tue, 9 May 2023 05:25:39 -0300 Subject: [PATCH 06/11] Replace var with const (#49379) Hey there :) I found this small issue. Replace `var` with `const` for block-scope variable declaration following the standard introduced by ES2015. --- bench/rendering/pages/stateless-big.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bench/rendering/pages/stateless-big.js b/bench/rendering/pages/stateless-big.js index 87f340d66d7e9..fdf9f92eefb3d 100644 --- a/bench/rendering/pages/stateless-big.js +++ b/bench/rendering/pages/stateless-big.js @@ -5,7 +5,7 @@ export default () => { } const items = () => { - var out = new Array(10000) + const out = new Array(10000) for (let i = 0; i < out.length; i++) { out[i] =
  • This is row {i + 1}
  • } From 4171cf38662c4df14f4bd96fa68e9619294542af Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 9 May 2023 11:16:56 +0200 Subject: [PATCH 07/11] test: pages react version with react hook in deployment (#48907) testing react version in pages for deployment env --- test/e2e/app-dir/app/pages/index.js | 5 +- test/e2e/app-dir/rsc-basic/rsc-basic.test.ts | 890 +++++++++---------- 2 files changed, 441 insertions(+), 454 deletions(-) diff --git a/test/e2e/app-dir/app/pages/index.js b/test/e2e/app-dir/app/pages/index.js index 5ee253ffe17b6..b1037a470b719 100644 --- a/test/e2e/app-dir/app/pages/index.js +++ b/test/e2e/app-dir/app/pages/index.js @@ -5,8 +5,11 @@ import styles from '../styles/shared.module.css' export default function Page() { return ( <> -

    hello from pages/index

    +

    + hello from pages/index +

    Dashboard +

    {React.version}

    ) } diff --git a/test/e2e/app-dir/rsc-basic/rsc-basic.test.ts b/test/e2e/app-dir/rsc-basic/rsc-basic.test.ts index fddb59590aab2..ff3e2014a6c25 100644 --- a/test/e2e/app-dir/rsc-basic/rsc-basic.test.ts +++ b/test/e2e/app-dir/rsc-basic/rsc-basic.test.ts @@ -1,8 +1,6 @@ import path from 'path' -import fs from 'fs-extra' import { check } from 'next-test-utils' -import { createNext } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' +import { createNextDescribe } from 'e2e-utils' import cheerio from 'cheerio' async function resolveStreamResponse(response: any, onData?: any) { @@ -16,481 +14,467 @@ async function resolveStreamResponse(response: any, onData?: any) { return result } -describe('app dir - rsc basics', () => { - let next: NextInstance - let distDir: string +createNextDescribe( + 'app dir - rsc basics', + { + files: __dirname, + dependencies: { + 'styled-components': '6.0.0-beta.5', + 'server-only': 'latest', + }, + packageJson: { + scripts: { + build: 'next build', + dev: 'next dev', + start: 'next start', + }, + }, + installCommand: 'yarn', + startCommand: (global as any).isNextDev ? 'yarn dev' : 'yarn start', + buildCommand: 'yarn build', + }, + ({ next, isNextDev, isNextStart }) => { + it('should correctly render page returning null', async () => { + const homeHTML = await next.render('/return-null/page') + const $ = cheerio.load(homeHTML) + expect($('#return-null-layout').html()).toBeEmpty() + }) - if ((global as any).isNextDeploy) { - it('should skip for deploy mode for now', () => {}) - return - } + it('should correctly render component returning null', async () => { + const homeHTML = await next.render('/return-null/component') + const $ = cheerio.load(homeHTML) + expect($('#return-null-layout').html()).toBeEmpty() + }) - beforeAll(async () => { - next = await createNext({ - files: __dirname, - dependencies: { - 'styled-components': '6.0.0-beta.5', - react: 'latest', - 'react-dom': 'latest', - 'server-only': 'latest', - }, - packageJson: { - scripts: { - build: 'next build', - dev: 'next dev', - start: 'next start', - }, - }, - installCommand: 'yarn', - startCommand: (global as any).isNextDev ? 'yarn dev' : 'yarn start', - buildCommand: 'yarn build', + it('should correctly render layout returning null', async () => { + const homeHTML = await next.render('/return-null/layout') + const $ = cheerio.load(homeHTML) + expect($('#return-null-layout').html()).toBeEmpty() }) - distDir = path.join(next.testDir, '.next') - }) - afterAll(() => next.destroy()) - - const { isNextDeploy, isNextDev } = global as any - if (isNextDeploy) { - it('should skip tests for next-deploy and react 17', () => {}) - return - } - it('should correctly render page returning null', async () => { - const homeHTML = await next.render('/return-null/page') - const $ = cheerio.load(homeHTML) - expect($('#return-null-layout').html()).toBeEmpty() - }) - - it('should correctly render component returning null', async () => { - const homeHTML = await next.render('/return-null/component') - const $ = cheerio.load(homeHTML) - expect($('#return-null-layout').html()).toBeEmpty() - }) - - it('should correctly render layout returning null', async () => { - const homeHTML = await next.render('/return-null/layout') - const $ = cheerio.load(homeHTML) - expect($('#return-null-layout').html()).toBeEmpty() - }) - - it('should correctly render page returning undefined', async () => { - const homeHTML = await next.render('/return-undefined/page') - const $ = cheerio.load(homeHTML) - expect($('#return-undefined-layout').html()).toBeEmpty() - }) - - it('should correctly render component returning undefined', async () => { - const homeHTML = await next.render('/return-undefined/component') - const $ = cheerio.load(homeHTML) - expect($('#return-undefined-layout').html()).toBeEmpty() - }) - - it('should correctly render layout returning undefined', async () => { - const homeHTML = await next.render('/return-undefined/layout') - const $ = cheerio.load(homeHTML) - expect($('#return-undefined-layout').html()).toBeEmpty() - }) - - it('should render server components correctly', async () => { - const homeHTML = await next.render('/', null, { - headers: { - 'x-next-test-client': 'test-util', - }, + it('should correctly render page returning undefined', async () => { + const homeHTML = await next.render('/return-undefined/page') + const $ = cheerio.load(homeHTML) + expect($('#return-undefined-layout').html()).toBeEmpty() }) - // should have only 1 DOCTYPE - expect(homeHTML).toMatch(/^') - expect(homeHTML).toContain( - '' - ) - - expect(homeHTML).toContain('component:index.server') - expect(homeHTML).toContain('header:test-util') - - const inlineFlightContents = [] - const $ = cheerio.load(homeHTML) - $('script').each((_index, tag) => { - const content = $(tag).text() - if (content) inlineFlightContents.push(content) + it('should correctly render component returning undefined', async () => { + const homeHTML = await next.render('/return-undefined/component') + const $ = cheerio.load(homeHTML) + expect($('#return-undefined-layout').html()).toBeEmpty() }) - const internalQueries = [ - '__nextFallback', - '__nextLocale', - '__nextDefaultLocale', - '__nextIsNotFound', - ] - - const hasNextInternalQuery = inlineFlightContents.some((content) => - internalQueries.some((query) => content.includes(query)) - ) - expect(hasNextInternalQuery).toBe(false) - }) - - it('should reuse the inline flight response without sending extra requests', async () => { - let hasFlightRequest = false - let requestsCount = 0 - await next.browser('/root', { - beforePageLoad(page) { - page.on('request', (request) => { - requestsCount++ - return request.allHeaders().then((headers) => { - if ( - headers['RSC'.toLowerCase()] === '1' && - // Prefetches also include `RSC` - headers['Next-Router-Prefetch'.toLowerCase()] !== '1' - ) { - hasFlightRequest = true - } - }) - }) - }, + it('should correctly render layout returning undefined', async () => { + const homeHTML = await next.render('/return-undefined/layout') + const $ = cheerio.load(homeHTML) + expect($('#return-undefined-layout').html()).toBeEmpty() }) - expect(requestsCount).toBeGreaterThan(0) - expect(hasFlightRequest).toBe(false) - }) - - it('should support multi-level server component imports', async () => { - const html = await next.render('/multi') - expect(html).toContain('bar.server.js:') - expect(html).toContain('foo.client') - }) - - it('should be able to navigate between rsc routes', async () => { - const browser = await next.browser('/root') - - await browser.waitForElementByCss('#goto-next-link').click() - await new Promise((res) => setTimeout(res, 1000)) - await check(() => browser.url(), `${next.url}/next-api/link`) - await browser.waitForElementByCss('#goto-home').click() - await new Promise((res) => setTimeout(res, 1000)) - await check(() => browser.url(), `${next.url}/root`) - const content = await browser.elementByCss('body').text() - expect(content).toContain('component:root.server') - - await browser.waitForElementByCss('#goto-streaming-rsc').click() - - // Wait for navigation and streaming to finish. - await check( - () => browser.elementByCss('#content').text(), - 'next_streaming_data' - ) - expect(await browser.url()).toBe(`${next.url}/streaming-rsc`) - }) - - it('should handle streaming server components correctly', async () => { - const browser = await next.browser('/streaming-rsc') - const content = await browser.eval( - `document.querySelector('#content').innerText` - ) - expect(content).toMatchInlineSnapshot('"next_streaming_data"') - }) - - it('should support next/link in server components', async () => { - const $ = await next.render$('/next-api/link') - const linkText = $('body a[href="/root"]').text() - - expect(linkText).toContain('home') - - const browser = await next.browser('/next-api/link') - - // We need to make sure the app is fully hydrated before clicking, otherwise - // it will be a full redirection instead of being taken over by the next - // router. This timeout prevents it being flaky caused by fast refresh's - // rebuilding event. - await new Promise((res) => setTimeout(res, 1000)) - await browser.eval('window.beforeNav = 1') - - await browser.waitForElementByCss('#next_id').click() - await check(() => browser.elementByCss('#query').text(), 'query:1') - - await browser.waitForElementByCss('#next_id').click() - await check(() => browser.elementByCss('#query').text(), 'query:2') - - if (isNextDev) { - expect(await browser.eval('window.beforeNav')).toBe(1) - } - }) - - it('should link correctly with next/link without mpa navigation to the page', async () => { - // Select the button which is not hidden but rendered - const selector = '#goto-next-link' - const browser = await next.browser('/root', {}) - - await browser.eval('window.didNotReloadPage = true') - await browser.elementByCss(selector).click().waitForElementByCss('#query') - - expect(await browser.eval('window.didNotReloadPage')).toBe(true) - - const text = await browser.elementByCss('#query').text() - expect(text).toBe('query:0') - }) - - it('should escape streaming data correctly', async () => { - const browser = await next.browser('/escaping-rsc') - const manipulated = await browser.eval(`window.__manipulated_by_injection`) - expect(manipulated).toBe(undefined) - }) - - it('should render built-in 404 page for missing route if pagesDir is not presented', async () => { - const res = await next.fetch('/does-not-exist') - - expect(res.status).toBe(404) - const html = await res.text() - expect(html).toContain('This page could not be found') - }) - - it('should suspense next/legacy/image in server components', async () => { - const $ = await next.render$('/next-api/image-legacy') - const imageTag = $('#myimg') - - expect(imageTag.attr('src')).toContain('data:image') - }) - - it('should suspense next/image in server components', async () => { - const $ = await next.render$('/next-api/image-new') - const imageTag = $('#myimg') - - expect(imageTag.attr('src')).toMatch(/test.+jpg/) - }) - - it('should handle various kinds of exports correctly', async () => { - const $ = await next.render$('/various-exports') - const content = $('body').text() - - expect(content).toContain('abcde') - expect(content).toContain('default-export-arrow.client') - expect(content).toContain('named.client') - - const browser = await next.browser('/various-exports') - const hydratedContent = await browser.waitForElementByCss('body').text() - - expect(hydratedContent).toContain('abcde') - expect(hydratedContent).toContain('default-export-arrow.client') - expect(hydratedContent).toContain('named.client') - expect(hydratedContent).toContain('cjs-shared') - expect(hydratedContent).toContain('cjs-client') - expect(hydratedContent).toContain('Export All: one, two, two') - }) - - it('should support native modules in server component', async () => { - const $ = await next.render$('/native-module') - const content = $('body').text() - - expect(content).toContain('fs: function') - expect(content).toContain('foo.client') - }) - - it('should resolve different kinds of components correctly', async () => { - const $ = await next.render$('/shared') - const main = $('#main').html() - const content = $('#bar').text() - - // Should have 5 occurrences of "client_component". - expect(Array.from(main.matchAll(/client_component/g)).length).toBe(5) - - // Should have 2 occurrences of "shared:server", and 2 occurrences of - // "shared:client". - const sharedServerModule = Array.from(main.matchAll(/shared:server:(\d+)/g)) - const sharedClientModule = Array.from(main.matchAll(/shared:client:(\d+)/g)) - expect(sharedServerModule.length).toBe(2) - expect(sharedClientModule.length).toBe(2) - - // Should have 2 modules created for the shared component. - expect(sharedServerModule[0][1]).toBe(sharedServerModule[1][1]) - expect(sharedClientModule[0][1]).toBe(sharedClientModule[1][1]) - expect(sharedServerModule[0][1]).not.toBe(sharedClientModule[0][1]) - expect(content).toContain('bar.server.js:') - }) - - it('should render initial styles of css-in-js in nodejs SSR correctly', async () => { - const $ = await next.render$('/css-in-js') - const head = $('head').html() - - // from styled-jsx - expect(head).toMatch(/{color:(\s*)purple;?}/) // styled-jsx/style - expect(head).toMatch(/{color:(\s*)hotpink;?}/) // styled-jsx/css - - // from styled-components - expect(head).toMatch(/{color:(\s*)blue;?}/) - }) - - it('should render initial styles of css-in-js in edge SSR correctly', async () => { - const $ = await next.render$('/css-in-js/edge') - const head = $('head').html() - - // from styled-jsx - expect(head).toMatch(/{color:(\s*)purple;?}/) // styled-jsx/style - expect(head).toMatch(/{color:(\s*)hotpink;?}/) // styled-jsx/css - - // from styled-components - expect(head).toMatch(/{color:(\s*)blue;?}/) - }) - - it('should render css-in-js suspense boundary correctly', async () => { - await next.fetch('/css-in-js/suspense').then(async (response) => { - const results = [] - - await resolveStreamResponse(response, (chunk: string) => { - const isSuspenseyDataResolved = - /]*>(\s)*.+{padding:2px;(\s)*color:orange;}/.test(chunk) - if (isSuspenseyDataResolved) results.push('data') - - // check if rsc refresh script for suspense show up, the test content could change with react version - const hasRCScript = /\$RC=function/.test(chunk) - if (hasRCScript) results.push('refresh-script') - - const isFallbackResolved = chunk.includes('fallback') - if (isFallbackResolved) results.push('fallback') + it('should render server components correctly', async () => { + const homeHTML = await next.render('/', null, { + headers: { + 'x-next-test-client': 'test-util', + }, + }) + + // should have only 1 DOCTYPE + expect(homeHTML).toMatch(/^') + expect(homeHTML).toContain( + '' + ) + + expect(homeHTML).toContain('component:index.server') + expect(homeHTML).toContain('header:test-util') + + const inlineFlightContents = [] + const $ = cheerio.load(homeHTML) + $('script').each((_index, tag) => { + const content = $(tag).text() + if (content) inlineFlightContents.push(content) }) - expect(results).toEqual(['fallback', 'data', 'refresh-script']) + const internalQueries = [ + '__nextFallback', + '__nextLocale', + '__nextDefaultLocale', + '__nextIsNotFound', + ] + + const hasNextInternalQuery = inlineFlightContents.some((content) => + internalQueries.some((query) => content.includes(query)) + ) + expect(hasNextInternalQuery).toBe(false) }) - // // TODO-APP: fix streaming/suspense within browser for test suite - // const browser = await next.browser( '/css-in-js', { waitHydration: false }) - // const footer = await browser.elementByCss('#footer') - // expect(await footer.text()).toBe('wait for fallback') - // expect( - // await browser.eval( - // `window.getComputedStyle(document.querySelector('#footer')).borderColor` - // ) - // ).toBe('rgb(255, 165, 0)') - // // Suspense is not rendered yet - // expect( - // await browser.eval( - // `document.querySelector('#footer-inner')` - // ) - // ).toBe('null') - - // // Wait for suspense boundary - // await check( - // () => browser.elementByCss('#footer').text(), - // 'wait for footer' - // ) - // expect( - // await browser.eval( - // `window.getComputedStyle(document.querySelector('#footer-inner')).color` - // ) - // ).toBe('rgb(255, 165, 0)') - }) - - it('should stick to the url without trailing /page suffix', async () => { - const browser = await next.browser('/edge/dynamic') - const indexUrl = await browser.url() - - await browser.loadPage(`${next.url}/edge/dynamic/123`, { - disableCache: false, - beforePageLoad: null, + + it('should reuse the inline flight response without sending extra requests', async () => { + let hasFlightRequest = false + let requestsCount = 0 + await next.browser('/root', { + beforePageLoad(page) { + page.on('request', (request) => { + requestsCount++ + return request.allHeaders().then((headers) => { + if ( + headers['RSC'.toLowerCase()] === '1' && + // Prefetches also include `RSC` + headers['Next-Router-Prefetch'.toLowerCase()] !== '1' + ) { + hasFlightRequest = true + } + }) + }) + }, + }) + + expect(requestsCount).toBeGreaterThan(0) + expect(hasFlightRequest).toBe(false) }) - const dynamicRouteUrl = await browser.url() - expect(indexUrl).toBe(`${next.url}/edge/dynamic`) - expect(dynamicRouteUrl).toBe(`${next.url}/edge/dynamic/123`) - }) + it('should support multi-level server component imports', async () => { + const html = await next.render('/multi') + expect(html).toContain('bar.server.js:') + expect(html).toContain('foo.client') + }) - it('should support streaming for flight response', async () => { - await next - .fetch('/', { - headers: { - ['RSC'.toString()]: '1', - }, + it('should be able to navigate between rsc routes', async () => { + const browser = await next.browser('/root') + + await browser.waitForElementByCss('#goto-next-link').click() + await new Promise((res) => setTimeout(res, 1000)) + await check(() => browser.url(), `${next.url}/next-api/link`) + await browser.waitForElementByCss('#goto-home').click() + await new Promise((res) => setTimeout(res, 1000)) + await check(() => browser.url(), `${next.url}/root`) + const content = await browser.elementByCss('body').text() + expect(content).toContain('component:root.server') + + await browser.waitForElementByCss('#goto-streaming-rsc').click() + + // Wait for navigation and streaming to finish. + await check( + () => browser.elementByCss('#content').text(), + 'next_streaming_data' + ) + expect(await browser.url()).toBe(`${next.url}/streaming-rsc`) + }) + + it('should handle streaming server components correctly', async () => { + const browser = await next.browser('/streaming-rsc') + const content = await browser.eval( + `document.querySelector('#content').innerText` + ) + expect(content).toMatchInlineSnapshot('"next_streaming_data"') + }) + + it('should support next/link in server components', async () => { + const $ = await next.render$('/next-api/link') + const linkText = $('body a[href="/root"]').text() + + expect(linkText).toContain('home') + + const browser = await next.browser('/next-api/link') + + // We need to make sure the app is fully hydrated before clicking, otherwise + // it will be a full redirection instead of being taken over by the next + // router. This timeout prevents it being flaky caused by fast refresh's + // rebuilding event. + await new Promise((res) => setTimeout(res, 1000)) + await browser.eval('window.beforeNav = 1') + + await browser.waitForElementByCss('#next_id').click() + await check(() => browser.elementByCss('#query').text(), 'query:1') + + await browser.waitForElementByCss('#next_id').click() + await check(() => browser.elementByCss('#query').text(), 'query:2') + + if (isNextDev) { + expect(await browser.eval('window.beforeNav')).toBe(1) + } + }) + + it('should link correctly with next/link without mpa navigation to the page', async () => { + // Select the button which is not hidden but rendered + const selector = '#goto-next-link' + const browser = await next.browser('/root', {}) + + await browser.eval('window.didNotReloadPage = true') + await browser.elementByCss(selector).click().waitForElementByCss('#query') + + expect(await browser.eval('window.didNotReloadPage')).toBe(true) + + const text = await browser.elementByCss('#query').text() + expect(text).toBe('query:0') + }) + + it('should escape streaming data correctly', async () => { + const browser = await next.browser('/escaping-rsc') + const manipulated = await browser.eval( + `window.__manipulated_by_injection` + ) + expect(manipulated).toBe(undefined) + }) + + it('should render built-in 404 page for missing route if pagesDir is not presented', async () => { + const res = await next.fetch('/does-not-exist') + + expect(res.status).toBe(404) + const html = await res.text() + expect(html).toContain('This page could not be found') + }) + + it('should suspense next/legacy/image in server components', async () => { + const $ = await next.render$('/next-api/image-legacy') + const imageTag = $('#myimg') + + expect(imageTag.attr('src')).toContain('data:image') + }) + + it('should suspense next/image in server components', async () => { + const $ = await next.render$('/next-api/image-new') + const imageTag = $('#myimg') + + expect(imageTag.attr('src')).toMatch(/test.+jpg/) + }) + + it('should handle various kinds of exports correctly', async () => { + const $ = await next.render$('/various-exports') + const content = $('body').text() + + expect(content).toContain('abcde') + expect(content).toContain('default-export-arrow.client') + expect(content).toContain('named.client') + + const browser = await next.browser('/various-exports') + const hydratedContent = await browser.waitForElementByCss('body').text() + + expect(hydratedContent).toContain('abcde') + expect(hydratedContent).toContain('default-export-arrow.client') + expect(hydratedContent).toContain('named.client') + expect(hydratedContent).toContain('cjs-shared') + expect(hydratedContent).toContain('cjs-client') + expect(hydratedContent).toContain('Export All: one, two, two') + }) + + it('should support native modules in server component', async () => { + const $ = await next.render$('/native-module') + const content = $('body').text() + + expect(content).toContain('fs: function') + expect(content).toContain('foo.client') + }) + + it('should resolve different kinds of components correctly', async () => { + const $ = await next.render$('/shared') + const main = $('#main').html() + const content = $('#bar').text() + + // Should have 5 occurrences of "client_component". + expect(Array.from(main.matchAll(/client_component/g)).length).toBe(5) + + // Should have 2 occurrences of "shared:server", and 2 occurrences of + // "shared:client". + const sharedServerModule = Array.from( + main.matchAll(/shared:server:(\d+)/g) + ) + const sharedClientModule = Array.from( + main.matchAll(/shared:client:(\d+)/g) + ) + expect(sharedServerModule.length).toBe(2) + expect(sharedClientModule.length).toBe(2) + + // Should have 2 modules created for the shared component. + expect(sharedServerModule[0][1]).toBe(sharedServerModule[1][1]) + expect(sharedClientModule[0][1]).toBe(sharedClientModule[1][1]) + expect(sharedServerModule[0][1]).not.toBe(sharedClientModule[0][1]) + expect(content).toContain('bar.server.js:') + }) + + it('should render initial styles of css-in-js in nodejs SSR correctly', async () => { + const $ = await next.render$('/css-in-js') + const head = $('head').html() + + // from styled-jsx + expect(head).toMatch(/{color:(\s*)purple;?}/) // styled-jsx/style + expect(head).toMatch(/{color:(\s*)hotpink;?}/) // styled-jsx/css + + // from styled-components + expect(head).toMatch(/{color:(\s*)blue;?}/) + }) + + it('should render initial styles of css-in-js in edge SSR correctly', async () => { + const $ = await next.render$('/css-in-js/edge') + const head = $('head').html() + + // from styled-jsx + expect(head).toMatch(/{color:(\s*)purple;?}/) // styled-jsx/style + expect(head).toMatch(/{color:(\s*)hotpink;?}/) // styled-jsx/css + + // from styled-components + expect(head).toMatch(/{color:(\s*)blue;?}/) + }) + + it('should render css-in-js suspense boundary correctly', async () => { + await next.fetch('/css-in-js/suspense').then(async (response) => { + const results = [] + + await resolveStreamResponse(response, (chunk: string) => { + const isSuspenseyDataResolved = + /]*>(\s)*.+{padding:2px;(\s)*color:orange;}/.test(chunk) + if (isSuspenseyDataResolved) results.push('data') + + // check if rsc refresh script for suspense show up, the test content could change with react version + const hasRCScript = /\$RC=function/.test(chunk) + if (hasRCScript) results.push('refresh-script') + + const isFallbackResolved = chunk.includes('fallback') + if (isFallbackResolved) results.push('fallback') + }) + + expect(results).toEqual(['fallback', 'data', 'refresh-script']) }) - .then(async (response) => { - const result = await resolveStreamResponse(response) - expect(result).toContain('component:index.server') + // // TODO-APP: fix streaming/suspense within browser for test suite + // const browser = await next.browser( '/css-in-js', { waitHydration: false }) + // const footer = await browser.elementByCss('#footer') + // expect(await footer.text()).toBe('wait for fallback') + // expect( + // await browser.eval( + // `window.getComputedStyle(document.querySelector('#footer')).borderColor` + // ) + // ).toBe('rgb(255, 165, 0)') + // // Suspense is not rendered yet + // expect( + // await browser.eval( + // `document.querySelector('#footer-inner')` + // ) + // ).toBe('null') + + // // Wait for suspense boundary + // await check( + // () => browser.elementByCss('#footer').text(), + // 'wait for footer' + // ) + // expect( + // await browser.eval( + // `window.getComputedStyle(document.querySelector('#footer-inner')).color` + // ) + // ).toBe('rgb(255, 165, 0)') + }) + + it('should stick to the url without trailing /page suffix', async () => { + const browser = await next.browser('/edge/dynamic') + const indexUrl = await browser.url() + + await browser.loadPage(`${next.url}/edge/dynamic/123`, { + disableCache: false, + beforePageLoad: null, }) - }) - - it('should support partial hydration with inlined server data', async () => { - await next.fetch('/partial-hydration').then(async (response) => { - let gotFallback = false - let gotData = false - let gotInlinedData = false - - await resolveStreamResponse(response, (_, result) => { - gotInlinedData = result.includes('self.__next_f=') - gotData = result.includes('next_streaming_data') - if (!gotFallback) { - gotFallback = result.includes('next_streaming_fallback') - if (gotFallback) { - expect(gotData).toBe(false) - // TODO-APP: investigate the failing test - // expect(gotInlinedData).toBe(false) + + const dynamicRouteUrl = await browser.url() + expect(indexUrl).toBe(`${next.url}/edge/dynamic`) + expect(dynamicRouteUrl).toBe(`${next.url}/edge/dynamic/123`) + }) + + it('should support streaming for flight response', async () => { + await next + .fetch('/', { + headers: { + ['RSC'.toString()]: '1', + }, + }) + .then(async (response) => { + const result = await resolveStreamResponse(response) + expect(result).toContain('component:index.server') + }) + }) + + it('should support partial hydration with inlined server data', async () => { + await next.fetch('/partial-hydration').then(async (response) => { + let gotFallback = false + let gotData = false + let gotInlinedData = false + + await resolveStreamResponse(response, (_, result) => { + gotInlinedData = result.includes('self.__next_f=') + gotData = result.includes('next_streaming_data') + if (!gotFallback) { + gotFallback = result.includes('next_streaming_fallback') + if (gotFallback) { + expect(gotData).toBe(false) + // TODO-APP: investigate the failing test + // expect(gotInlinedData).toBe(false) + } } - } + }) + + expect(gotFallback).toBe(true) + expect(gotData).toBe(true) + expect(gotInlinedData).toBe(true) }) + }) - expect(gotFallback).toBe(true) - expect(gotData).toBe(true) - expect(gotInlinedData).toBe(true) + it('should not apply rsc syntax checks in pages/api', async () => { + const res = await next.fetch('/api/import-test') + expect(await res.text()).toBe('Hello from import-test.js') }) - }) - - it('should not apply rsc syntax checks in pages/api', async () => { - const res = await next.fetch('/api/import-test') - expect(await res.text()).toBe('Hello from import-test.js') - }) - - it('should use stable react for pages', async () => { - const resPages = await next.fetch('/pages-react') - const versionPages = (await resPages.text()).match( - /
    version=([^<]+)<\/div>/ - )?.[1] - - const resApp = await next.fetch('/app-react') - const versionApp = (await resApp.text()).match( - /
    version=([^<]+)<\/div>/ - )?.[1] - - expect(versionPages).not.toInclude('-canary-') - expect(versionApp).toInclude('-canary-') - }) - - // disable this flaky test - it.skip('should support partial hydration with inlined server data in browser', async () => { - // Should end up with "next_streaming_data". - const browser = await next.browser('/partial-hydration', { - waitHydration: false, + + it('should use stable react for pages', async () => { + const resPages = await next.fetch('/pages-react') + const versionPages = (await resPages.text()).match( + /
    version=([^<]+)<\/div>/ + )?.[1] + + const resApp = await next.fetch('/app-react') + const versionApp = (await resApp.text()).match( + /
    version=([^<]+)<\/div>/ + )?.[1] + + expect(versionPages).not.toInclude('-canary-') + expect(versionApp).toInclude('-canary-') }) - const content = await browser.eval(`window.document.body.innerText`) - expect(content).toContain('next_streaming_data') - - // Should support partial hydration: the boundary should still be pending - // while another part is hydrated already. - expect(await browser.eval(`window.partial_hydration_suspense_result`)).toBe( - 'next_streaming_fallback' - ) - expect(await browser.eval(`window.partial_hydration_counter_result`)).toBe( - 'count: 1' - ) - }) - - if (!isNextDev) { - it('should generate edge SSR manifests for Node.js', async () => { - const distServerDir = path.join(distDir, 'server') - - const requiredServerFiles = ( - await fs.readJSON(path.join(distDir, 'required-server-files.json')) - ).files - - const files = [ - 'middleware-build-manifest.js', - 'middleware-manifest.json', - 'client-reference-manifest.json', - ] - files.forEach((file) => { - const filepath = path.join(distServerDir, file) - expect(fs.existsSync(filepath)).toBe(true) + // disable this flaky test + it.skip('should support partial hydration with inlined server data in browser', async () => { + // Should end up with "next_streaming_data". + const browser = await next.browser('/partial-hydration', { + waitHydration: false, }) + const content = await browser.eval(`window.document.body.innerText`) + expect(content).toContain('next_streaming_data') + + // Should support partial hydration: the boundary should still be pending + // while another part is hydrated already. + expect( + await browser.eval(`window.partial_hydration_suspense_result`) + ).toBe('next_streaming_fallback') + expect( + await browser.eval(`window.partial_hydration_counter_result`) + ).toBe('count: 1') + }) + + if (isNextStart) { + it('should generate edge SSR manifests for Node.js', async () => { + const requiredServerFiles = JSON.parse( + await next.readFile('.next/required-server-files.json') + ).files - requiredServerFiles.forEach((file) => { - const requiredFilePath = path.join(next.testDir, file) - expect(fs.existsSync(requiredFilePath)).toBe(true) + const files = [ + 'middleware-build-manifest.js', + 'middleware-manifest.json', + 'client-reference-manifest.json', + ] + + let promises = files.map(async (file) => { + expect(await next.hasFile(path.join('.next/server', file))).toBe(true) + }) + await Promise.all(promises) + + promises = requiredServerFiles.map(async (file) => { + expect(await next.hasFile(file)).toBe(true) + }) + await Promise.all(promises) }) - }) + } } -}) +) From 761c293093a91151d154310dfa241ec6b88dbb15 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 9 May 2023 11:17:44 +0200 Subject: [PATCH 08/11] Add test case for #49235 (#49488) This PR adds a test case for `headers()` and `cookies().set()` in Client Component imported actions. --- test/e2e/app-dir/actions/app-action.test.ts | 26 +++++++++++++++++++ .../e2e/app-dir/actions/app/client/actions.js | 6 +++++ test/e2e/app-dir/actions/app/client/page.js | 7 ++++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts index 2cf0b7c6117d2..1297524713c8c 100644 --- a/test/e2e/app-dir/actions/app-action.test.ts +++ b/test/e2e/app-dir/actions/app-action.test.ts @@ -59,6 +59,32 @@ createNextDescribe( }, 'same') }) + it('should support headers in client imported actions', async () => { + const logs: string[] = [] + next.on('stdout', (log) => { + logs.push(log) + }) + next.on('stderr', (log) => { + logs.push(log) + }) + + const currentTimestamp = Date.now() + + const browser = await next.browser('/client') + await browser.elementByCss('#get-header').click() + await check(() => { + return logs.some((log) => + log.includes('accept header: text/x-component') + ) + ? 'yes' + : '' + }, 'yes') + + expect( + await browser.eval('+document.cookie.match(/test-cookie=(\\d+)/)[1]') + ).toBeGreaterThanOrEqual(currentTimestamp) + }) + it('should support setting cookies in route handlers with the correct overrides', async () => { const res = await next.fetch('/handler') const setCookieHeader = res.headers.get('set-cookie') as string[] diff --git a/test/e2e/app-dir/actions/app/client/actions.js b/test/e2e/app-dir/actions/app/client/actions.js index a1aa005a571d0..9039a73b18b0a 100644 --- a/test/e2e/app-dir/actions/app/client/actions.js +++ b/test/e2e/app-dir/actions/app/client/actions.js @@ -1,6 +1,12 @@ 'use server' import { redirect } from 'next/navigation' +import { headers, cookies } from 'next/headers' + +export async function getHeaders() { + console.log('accept header:', headers().get('accept')) + cookies().set('test-cookie', Date.now()) +} export async function inc(value) { return value + 1 diff --git a/test/e2e/app-dir/actions/app/client/page.js b/test/e2e/app-dir/actions/app/client/page.js index 68043e4c0cfca..bec2e5e4e23f1 100644 --- a/test/e2e/app-dir/actions/app/client/page.js +++ b/test/e2e/app-dir/actions/app/client/page.js @@ -2,7 +2,7 @@ import { useState } from 'react' -import double, { inc, dec, redirectAction } from './actions' +import double, { inc, dec, redirectAction, getHeaders } from './actions' export default function Counter() { const [count, setCount] = useState(0) @@ -52,6 +52,11 @@ export default function Counter() { redirect external +
    + +
    ) } From 81f5ed7aa29a210b352b9c7331b62547e9881f67 Mon Sep 17 00:00:00 2001 From: Darsh Patel Date: Tue, 9 May 2023 16:19:27 +0530 Subject: [PATCH 09/11] Fix: Router.query contains _next when using middleware with dynamic routes (#48753) Fixes: #43598 This was a tricky one to find! Not sure why more people aren't complaining about this issue, was super annoying in my use case since links had the wrong URL. ## What? This issue only occurred when basepath was defined and middleware and dynamic pages are being used. Example from the reproduction repo mentioned in the issue tagged: Screenshot 2023-04-23 at 9 32 55 PM ## Why? `nextConfig` wasn't passed to `getNextPathnameInfo` function, hence the basePath wasn't removed from a intermediate variable and that trickled down to cause this issue. Added test case based on the issue reproduction repo --------- Co-authored-by: Jimmy Lai --- packages/next/src/shared/lib/router/router.ts | 7 ++- .../app/middleware.js | 9 +++ .../app/next.config.js | 3 + .../app/pages/[path]/[[...pages]].js | 35 ++++++++++++ .../test/index.test.ts | 55 +++++++++++++++++++ 5 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 test/e2e/middleware-dynamic-basepath-matcher/app/middleware.js create mode 100644 test/e2e/middleware-dynamic-basepath-matcher/app/next.config.js create mode 100644 test/e2e/middleware-dynamic-basepath-matcher/app/pages/[path]/[[...pages]].js create mode 100644 test/e2e/middleware-dynamic-basepath-matcher/test/index.test.ts diff --git a/packages/next/src/shared/lib/router/router.ts b/packages/next/src/shared/lib/router/router.ts index 3aa2d9e3d2c5c..a17eac086963e 100644 --- a/packages/next/src/shared/lib/router/router.ts +++ b/packages/next/src/shared/lib/router/router.ts @@ -221,7 +221,12 @@ function getMiddlewareData( ) { const parsedSource = getNextPathnameInfo( parseRelativeUrl(source).pathname, - { parseData: true } + { + nextConfig: process.env.__NEXT_HAS_REWRITES + ? undefined + : nextConfig, + parseData: true, + } ) as = addBasePath(parsedSource.pathname) diff --git a/test/e2e/middleware-dynamic-basepath-matcher/app/middleware.js b/test/e2e/middleware-dynamic-basepath-matcher/app/middleware.js new file mode 100644 index 0000000000000..d4a6e6e00a89e --- /dev/null +++ b/test/e2e/middleware-dynamic-basepath-matcher/app/middleware.js @@ -0,0 +1,9 @@ +import { NextResponse } from 'next/server' + +export default function middleware(_) { + const res = NextResponse.next() + res.headers.set('X-From-Middleware', 'true') + return res +} + +export const config = { matcher: '/random' } diff --git a/test/e2e/middleware-dynamic-basepath-matcher/app/next.config.js b/test/e2e/middleware-dynamic-basepath-matcher/app/next.config.js new file mode 100644 index 0000000000000..1773c854e2959 --- /dev/null +++ b/test/e2e/middleware-dynamic-basepath-matcher/app/next.config.js @@ -0,0 +1,3 @@ +module.exports = { + basePath: '/base', +} diff --git a/test/e2e/middleware-dynamic-basepath-matcher/app/pages/[path]/[[...pages]].js b/test/e2e/middleware-dynamic-basepath-matcher/app/pages/[path]/[[...pages]].js new file mode 100644 index 0000000000000..7d7dd64f320a2 --- /dev/null +++ b/test/e2e/middleware-dynamic-basepath-matcher/app/pages/[path]/[[...pages]].js @@ -0,0 +1,35 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' + +const Index = ({ value }) => { + const router = useRouter() + + return ( +
    +

    {value}

    +

    + {router.query.path} +

    + + Link to another page + +
    + ) +} + +export async function getStaticPaths() { + return { + paths: [{ params: { path: 'another-page', pages: null } }], + fallback: true, + } +} + +export async function getStaticProps() { + return { + props: { + value: 'Hello', + }, + } +} + +export default Index diff --git a/test/e2e/middleware-dynamic-basepath-matcher/test/index.test.ts b/test/e2e/middleware-dynamic-basepath-matcher/test/index.test.ts new file mode 100644 index 0000000000000..07d4dc0051831 --- /dev/null +++ b/test/e2e/middleware-dynamic-basepath-matcher/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 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 [ + '/base/default', + `/base/_next/data/${next.buildId}/default.json`, + ]) { + const res = await fetchViaHTTP(next.url, path) + expect(res.status).toBe(200) + expect(res.headers.get('x-from-middleware')).toBeDefined() + } + }) + + it.each(['/default', '/invalid/base/default'])( + '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 query path', async () => { + const browser = await webdriver(next.url, '/base/random') + const currentPath = await browser.elementById('router-path').text() + expect(currentPath).toBe('random') + await browser.elementById('linkelement').click() + const anotherPagePath = await browser.elementById('router-path').text() + expect(anotherPagePath).toBe('another-page') + }) +}) From 5451564f364399003b05939fa6dd7d32c1dabec7 Mon Sep 17 00:00:00 2001 From: Josh Parnham Date: Tue, 9 May 2023 20:55:41 +1000 Subject: [PATCH 10/11] [docs] Add iOS hydration mismatch details to error page (#43584) We recently ran into hydration mismatch errors on iOS, which we realised was due to iOS automatically converting what it thought were phone numbers into links. We then found this GitHub issue, https://github.com/vercel/next.js/issues/38290, and thought it would be useful to add these details to the documentation page linked from the next.js error. --- Example from the linked issue: ![image](https://user-images.githubusercontent.com/712727/204925813-f986e2ca-29c2-4b63-9121-df4c7b00b1d8.png) ## Documentation / Examples - [x] Make sure the linting passes by running `pnpm build && pnpm lint` - [x] 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: Tim Neutkens --- errors/react-hydration-error.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/errors/react-hydration-error.md b/errors/react-hydration-error.md index 486112fc43eaf..916e695b2a66f 100644 --- a/errors/react-hydration-error.md +++ b/errors/react-hydration-error.md @@ -82,10 +82,18 @@ Common causes with css-in-js libraries: - When using other css-in-js libraries - Similar to Styled Components / Emotion css-in-js libraries generally need configuration specified in their examples in the [examples directory](https://github.com/vercel/next.js/tree/canary/examples) -Local Overrides +Local Overrides: It's possible you may have [Local Overrides enabled in Chrome devtools](https://developer.chrome.com/blog/new-in-devtools-65/#overrides). With this enabled, the HTML served will be different from what the SSR emitted. It also won't show up in view-source, so you may be left wondering what is going on. +Common causes on iOS: + +- iOS attempts to detect phone numbers, email addressees and other data in text content and convert them into links, which can [lead to hydration mismatches](https://github.com/vercel/next.js/issues/38290). This can be disabled with the following `meta` tag: + +``` + +``` + ### Useful Links - [React Hydration Documentation](https://react.dev/reference/react-dom/client/hydrateRoot) From 201ab71ee74b58484806c13b794023965210967b Mon Sep 17 00:00:00 2001 From: lijianan <574980606@qq.com> Date: Tue, 9 May 2023 21:16:55 +0800 Subject: [PATCH 11/11] type: update React.CSSProperties type to Record (#49186) update React.CSSProperties type to Record --------- Co-authored-by: Parbez --- packages/next/src/client/components/error.tsx | 2 +- packages/next/src/pages/_error.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next/src/client/components/error.tsx b/packages/next/src/client/components/error.tsx index 94f97386167c0..3b0c133603308 100644 --- a/packages/next/src/client/components/error.tsx +++ b/packages/next/src/client/components/error.tsx @@ -1,6 +1,6 @@ import React from 'react' -const styles: { [k: string]: React.CSSProperties } = { +const styles: Record = { error: { // https://github.com/sindresorhus/modern-normalize/blob/main/modern-normalize.css#L38-L52 fontFamily: diff --git a/packages/next/src/pages/_error.tsx b/packages/next/src/pages/_error.tsx index 376fb9536ba68..ca57c82991eb3 100644 --- a/packages/next/src/pages/_error.tsx +++ b/packages/next/src/pages/_error.tsx @@ -24,7 +24,7 @@ function _getInitialProps({ return { statusCode } } -const styles: { [k: string]: React.CSSProperties } = { +const styles: Record = { error: { // https://github.com/sindresorhus/modern-normalize/blob/main/modern-normalize.css#L38-L52 fontFamily: