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: 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}
  • } 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) 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/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/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: 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 }, }) } 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/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 diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts index b94b72f54ea8c..1297524713c8c 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') @@ -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[] @@ -172,7 +198,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( 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 +
    + +
    ) } 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) }) - }) + } } -}) +) 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') + }) +}) 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://)' + ) + }) + }) })