diff --git a/docs/advanced-features/middleware.md b/docs/advanced-features/middleware.md
index b0f04aa0a5b17..5d38627289a9d 100644
--- a/docs/advanced-features/middleware.md
+++ b/docs/advanced-features/middleware.md
@@ -9,14 +9,14 @@ description: Learn how to use Middleware to run code before a request is complet
| Version | Changes |
| --------- | ------------------------------------------------------------------------------------------ |
-| `v13.0.0` | Support overriding request headers. |
+| `v13.0.0` | Middleware can modify request headers, response headers, and send responses |
| `v12.2.0` | Middleware is stable |
| `v12.0.9` | Enforce absolute URLs in Edge Runtime ([PR](https://github.com/vercel/next.js/pull/33410)) |
| `v12.0.0` | Middleware (Beta) added |
-Middleware allows you to run code before a request is completed, then based on the incoming request, you can modify the response by rewriting, redirecting, adding headers, or setting cookies.
+Middleware allows you to run code before a request is completed, then based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly.
Middleware runs _before_ cached content, so you can personalize static files and pages. Common examples of Middleware would be authentication, A/B testing, localized pages, bot protection, and more. Regarding localized pages, you can start with [i18n routing](/docs/advanced-features/i18n-routing) and implement Middleware for more advanced use cases.
@@ -218,6 +218,45 @@ export function middleware(request: NextRequest) {
> **Note:** Avoid setting large headers as it might cause [431 Request Header Fields Too Large](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431) error depending on your backend web server configuration.
+## Producing a Response
+
+You can respond to middleware directly by returning a `NextResponse` (responding from middleware is available since Next.js v13.0.0).
+
+To enable middleware responses, update `next.config.js`:
+
+```js
+// next.config.js
+module.exports = {
+ experimental: {
+ allowMiddlewareResponseBody: true,
+ },
+}
+```
+
+Once enabled, you can provide a response from middleware using the `Response` or `NextResponse` API:
+
+```ts
+// middleware.ts
+import { NextRequest, NextResponse } from 'next/server'
+import { isAuthenticated } from '@lib/auth'
+
+// Limit the middleware to paths starting with `/api/`
+export const config = {
+ matcher: '/api/:function*',
+}
+
+export function middleware(request: NextRequest) {
+ // Call our authentication function to check the request
+ if (!isAuthenticated(request)) {
+ // Respond with JSON indicating an error message
+ return new NextResponse(
+ JSON.stringify({ success: false, message: 'authentication failed' }),
+ { status: 401, headers: { 'content-type': 'application/json' } }
+ )
+ }
+}
+```
+
## Related
diff --git a/docs/advanced-features/module-path-aliases.md b/docs/advanced-features/module-path-aliases.md
index 8015f82667cd0..ec19e194bbfd3 100644
--- a/docs/advanced-features/module-path-aliases.md
+++ b/docs/advanced-features/module-path-aliases.md
@@ -7,7 +7,7 @@ description: Configure module path aliases that allow you to remap certain impor
Examples
diff --git a/docs/api-reference/next/link.md b/docs/api-reference/next/link.md
index c8261e651f20b..5bcc16e4ce3e0 100644
--- a/docs/api-reference/next/link.md
+++ b/docs/api-reference/next/link.md
@@ -218,3 +218,43 @@ The default behavior of `Link` is to scroll to the top of the page. When there i
Disables scrolling to the top
```
+
+## With Next.js 13 Middleware
+
+It's common to use [Middleware](/docs/advanced-features/middleware) for authentication or other purposes that involve rewriting the user to a different page. In order for the `` component to properly prefetch links with rewrites via Middleware, you need to tell Next.js both the URL to display and the URL to prefetch. This is required to avoid un-necessary fetches to middleware to know the correct route to prefetch.
+
+For example, if you have want to serve a `/dashboard` route that has authenticated and visitor views, you may add something similar to the following in your Middleware to redirect the user to the correct page:
+
+```js
+// middleware.js
+export function middleware(req) {
+ const nextUrl = req.nextUrl
+ if (nextUrl.pathname === '/dashboard') {
+ if (req.cookies.authToken) {
+ return NextResponse.rewrite('/auth/dashboard')
+ } else {
+ return NextResponse.rewrite('/public/dashboard')
+ }
+ }
+}
+```
+
+In this case, you would want to use the following code in your `` component (inside `pages/`):
+
+```js
+// pages/index.js
+import Link from 'next/link'
+import useIsAuthed from './hooks/useIsAuthed'
+
+export default function Page() {
+ const isAuthed = useIsAuthed()
+ const path = isAuthed ? '/auth/dashboard' : '/dashboard'
+ return (
+
+ Dashboard
+
+ )
+}
+```
+
+> **Note:** If you're using [Dynamic Routes](/docs/routing/dynamic-routes), you'll need to adapt your `as` and `href` props. For example, if you have a Dynamic Route like `/dashboard/[user]` that you want to present differently via middleware, you would write: `Profile`.
diff --git a/errors/manifest.json b/errors/manifest.json
index 5e48d7fa9d44e..9be351338750b 100644
--- a/errors/manifest.json
+++ b/errors/manifest.json
@@ -757,6 +757,10 @@
{
"title": "invalid-segment-export",
"path": "/errors/invalid-segment-export.md"
+ },
+ {
+ "title": "next-router-not-mounted",
+ "path": "/errors/next-router-not-mounted.md"
}
]
}
diff --git a/errors/next-router-not-mounted.md b/errors/next-router-not-mounted.md
new file mode 100644
index 0000000000000..0bdb3f7a9ce73
--- /dev/null
+++ b/errors/next-router-not-mounted.md
@@ -0,0 +1,13 @@
+# NextRouter was not mounted
+
+#### Why This Error Occurred
+
+A component used `useRouter` outside a Next.js application, or was rendered outside a Next.js application. This can happen when doing unit testing on components that use the `useRouter` hook as they are not configured with Next.js' contexts.
+
+#### Possible Ways to Fix It
+
+If used in a test, mock out the router by mocking the `next/router`'s `useRouter()` hook.
+
+### Useful Links
+
+- [next-router-mock](https://www.npmjs.com/package/next-router-mock)
diff --git a/examples/with-absolute-imports/README.md b/examples/with-absolute-imports/README.md
index 8a35deaf0acd0..1d30ec808a3d7 100644
--- a/examples/with-absolute-imports/README.md
+++ b/examples/with-absolute-imports/README.md
@@ -1,6 +1,24 @@
-# Example app with absolute imports
+# Absolute Imports and Aliases
-This example shows how to configure Babel to have absolute imports instead of relative imports without modifying the Webpack configuration.
+This example shows how to configure [Absolute imports and Module path aliases](https://nextjs.org/docs/advanced-features/module-path-aliases) in `tsconfig.json` (or `jsconfig.json` for JavaScript projects). These options will allow absolute imports from `.` (the root directory), and allow you to create custom import aliases.
+
+If you’re working on a large project, your relative import statements might suffer from `../../../` spaghetti:
+
+```tsx
+import Button from '../../../components/button'
+```
+
+In such cases, we might want to setup absolute imports using the `baseUrl` option, for clearer and shorter imports:
+
+```tsx
+import Button from 'components/button'
+```
+
+Furthermore, TypeScript also supports the `paths` option, which allows you to configure custom module aliases. You can then use your alias like so:
+
+```tsx
+import Button from '@/components/button'
+```
## Deploy your own
diff --git a/examples/with-absolute-imports/components/button.tsx b/examples/with-absolute-imports/components/button.tsx
new file mode 100644
index 0000000000000..1deaded46204c
--- /dev/null
+++ b/examples/with-absolute-imports/components/button.tsx
@@ -0,0 +1,3 @@
+export default function Button() {
+ return
+}
diff --git a/examples/with-absolute-imports/components/header.js b/examples/with-absolute-imports/components/header.tsx
similarity index 100%
rename from examples/with-absolute-imports/components/header.js
rename to examples/with-absolute-imports/components/header.tsx
diff --git a/examples/with-absolute-imports/jsconfig.json b/examples/with-absolute-imports/jsconfig.json
deleted file mode 100644
index 36aa1a4dc28f1..0000000000000
--- a/examples/with-absolute-imports/jsconfig.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "compilerOptions": {
- "baseUrl": "."
- }
-}
diff --git a/examples/with-absolute-imports/package.json b/examples/with-absolute-imports/package.json
index bf29745bfe271..1412a9c80e177 100644
--- a/examples/with-absolute-imports/package.json
+++ b/examples/with-absolute-imports/package.json
@@ -9,5 +9,11 @@
"next": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@types/node": "^18.11.9",
+ "@types/react": "^18.0.25",
+ "@types/react-dom": "^18.0.8",
+ "typescript": "^4.8.4"
}
}
diff --git a/examples/with-absolute-imports/pages/index.js b/examples/with-absolute-imports/pages/index.js
deleted file mode 100644
index 54c8bf264718a..0000000000000
--- a/examples/with-absolute-imports/pages/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import Header from 'components/header.js'
-
-export default function Home() {
- return (
-
-
-
- )
-}
diff --git a/examples/with-absolute-imports/pages/index.tsx b/examples/with-absolute-imports/pages/index.tsx
new file mode 100644
index 0000000000000..47f057a3b4813
--- /dev/null
+++ b/examples/with-absolute-imports/pages/index.tsx
@@ -0,0 +1,11 @@
+import Header from 'components/header'
+import Button from '@/components/button'
+
+export default function Home() {
+ return (
+ <>
+
+
+ >
+ )
+}
diff --git a/examples/with-absolute-imports/tsconfig.json b/examples/with-absolute-imports/tsconfig.json
new file mode 100644
index 0000000000000..e4af555219514
--- /dev/null
+++ b/examples/with-absolute-imports/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": false,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ // Using "baseUrl" allows you to use absolute paths
+ "baseUrl": ".",
+ // Using "paths" allows you to configure module path aliases
+ "paths": {
+ "@/components/*": ["components/*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
+ "exclude": ["node_modules"]
+}
diff --git a/lerna.json b/lerna.json
index 8e4407c77312d..c15b12ba117f2 100644
--- a/lerna.json
+++ b/lerna.json
@@ -16,5 +16,5 @@
"registry": "https://registry.npmjs.org/"
}
},
- "version": "13.0.3-canary.0"
+ "version": "13.0.3-canary.1"
}
diff --git a/packages/create-next-app/index.ts b/packages/create-next-app/index.ts
index cd3c8112a1439..c02ad153b29d8 100644
--- a/packages/create-next-app/index.ts
+++ b/packages/create-next-app/index.ts
@@ -157,14 +157,13 @@ async function run(): Promise {
* to use TS or JS.
*/
if (!example) {
- if (ciInfo.isCI) {
- // default to JavaScript in CI as we can't prompt to
- // prevent breaking setup flows
- program.javascript = true
- program.typescript = false
- program.eslint = true
- } else {
- if (!program.typescript && !program.javascript) {
+ if (!program.typescript && !program.javascript) {
+ if (ciInfo.isCI) {
+ // default to JavaScript in CI as we can't prompt to
+ // prevent breaking setup flows
+ program.typescript = false
+ program.javascript = true
+ } else {
const styledTypeScript = chalk.hex('#007acc')('TypeScript')
const { typescript } = await prompts(
{
@@ -186,18 +185,21 @@ async function run(): Promise {
},
}
)
-
/**
* Depending on the prompt response, set the appropriate program flags.
*/
program.typescript = Boolean(typescript)
program.javascript = !Boolean(typescript)
}
+ }
- if (
- !process.argv.includes('--eslint') &&
- !process.argv.includes('--no-eslint')
- ) {
+ if (
+ !process.argv.includes('--eslint') &&
+ !process.argv.includes('--no-eslint')
+ ) {
+ if (ciInfo.isCI) {
+ program.eslint = true
+ } else {
const styledEslint = chalk.hex('#007acc')('ESLint')
const { eslint } = await prompts({
type: 'toggle',
diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json
index 26bfd52b8a05e..1df962ccfca8e 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.0.3-canary.0",
+ "version": "13.0.3-canary.1",
"keywords": [
"react",
"next",
diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json
index 2da845e71025f..8e46894903876 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.0.3-canary.0",
+ "version": "13.0.3-canary.1",
"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.0.3-canary.0",
+ "@next/eslint-plugin-next": "13.0.3-canary.1",
"@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 d4627ed6204e2..8e73cbdbc654c 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.0.3-canary.0",
+ "version": "13.0.3-canary.1",
"description": "ESLint plugin for NextJS.",
"main": "dist/index.js",
"license": "MIT",
diff --git a/packages/font/package.json b/packages/font/package.json
index e545c99dc623f..914cf166c877b 100644
--- a/packages/font/package.json
+++ b/packages/font/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/font",
- "version": "13.0.3-canary.0",
+ "version": "13.0.3-canary.1",
"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 f4e9a026c912e..65c055a839254 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.0.3-canary.0",
+ "version": "13.0.3-canary.1",
"main": "index.js",
"types": "index.d.ts",
"license": "MIT",
diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json
index 9fc17e6f10100..fed90c8faebec 100644
--- a/packages/next-codemod/package.json
+++ b/packages/next-codemod/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/codemod",
- "version": "13.0.3-canary.0",
+ "version": "13.0.3-canary.1",
"license": "MIT",
"dependencies": {
"chalk": "4.1.0",
diff --git a/packages/next-env/package.json b/packages/next-env/package.json
index 905a2e2d89708..dd1771b64a9b8 100644
--- a/packages/next-env/package.json
+++ b/packages/next-env/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/env",
- "version": "13.0.3-canary.0",
+ "version": "13.0.3-canary.1",
"keywords": [
"react",
"next",
diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json
index fe1514e66b0c8..f4a672d697735 100644
--- a/packages/next-mdx/package.json
+++ b/packages/next-mdx/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/mdx",
- "version": "13.0.3-canary.0",
+ "version": "13.0.3-canary.1",
"main": "index.js",
"license": "MIT",
"repository": {
diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json
index 1d9116d9dba30..a6b9f8947e4ba 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.0.3-canary.0",
+ "version": "13.0.3-canary.1",
"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 8c454e532dd2d..767c418801990 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.0.3-canary.0",
+ "version": "13.0.3-canary.1",
"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 6e218124df87b..1a82be7086f24 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.0.3-canary.0",
+ "version": "13.0.3-canary.1",
"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 2aa52011a744f..756ca0c0c37c1 100644
--- a/packages/next-swc/package.json
+++ b/packages/next-swc/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/swc",
- "version": "13.0.3-canary.0",
+ "version": "13.0.3-canary.1",
"private": true,
"scripts": {
"build-native": "napi build --platform -p next-swc-napi --cargo-name next_swc_napi --features plugin --js false native",
diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts
index 46802a753c1ae..192c8074facb4 100644
--- a/packages/next/build/index.ts
+++ b/packages/next/build/index.ts
@@ -123,6 +123,7 @@ import { RemotePattern } from '../shared/lib/image-config'
import { eventSwcPlugins } from '../telemetry/events/swc-plugins'
import { normalizeAppPath } from '../shared/lib/router/utils/app-paths'
import { AppBuildManifest } from './webpack/plugins/app-build-manifest-plugin'
+import { RSC, RSC_VARY_HEADER } from '../client/components/app-router-headers'
export type SsgRoute = {
initialRevalidateSeconds: number | false
@@ -755,6 +756,10 @@ export default async function build(
defaultLocale: string
localeDetection?: false
}
+ rsc: {
+ header: typeof RSC
+ varyHeader: typeof RSC_VARY_HEADER
+ }
} = nextBuildSpan.traceChild('generate-routes-manifest').traceFn(() => {
const sortedRoutes = getSortedRoutes([
...pageKeys.pages,
@@ -781,6 +786,10 @@ export default async function build(
staticRoutes,
dataRoutes: [],
i18n: config.i18n || undefined,
+ rsc: {
+ header: RSC,
+ varyHeader: RSC_VARY_HEADER,
+ },
}
})
@@ -1800,28 +1809,30 @@ export default async function build(
showAll: config.experimental.turbotrace.logAll,
})
} else {
+ const ignores = [
+ '**/next/dist/pages/**/*',
+ '**/next/dist/compiled/webpack/(bundle4|bundle5).js',
+ '**/node_modules/webpack5/**/*',
+ '**/next/dist/server/lib/squoosh/**/*.wasm',
+ ...(ciEnvironment.hasNextSupport
+ ? [
+ // only ignore image-optimizer code when
+ // this is being handled outside of next-server
+ '**/next/dist/server/image-optimizer.js',
+ '**/node_modules/sharp/**/*',
+ ]
+ : []),
+ ...(!hasSsrAmpPages
+ ? ['**/next/dist/compiled/@ampproject/toolbox-optimizer/**/*']
+ : []),
+ ]
+ const ignoreFn = (pathname: string) => {
+ return isMatch(pathname, ignores, { contains: true, dot: true })
+ }
serverResult = await nodeFileTrace(toTrace, {
base: root,
processCwd: dir,
- ignore: [
- '**/next/dist/pages/**/*',
- '**/next/dist/compiled/webpack/(bundle4|bundle5).js',
- '**/node_modules/webpack5/**/*',
- '**/next/dist/server/lib/squoosh/**/*.wasm',
- ...(ciEnvironment.hasNextSupport
- ? [
- // only ignore image-optimizer code when
- // this is being handled outside of next-server
- '**/next/dist/server/image-optimizer.js',
- '**/node_modules/sharp/**/*',
- ]
- : []),
- ...(!hasSsrAmpPages
- ? [
- '**/next/dist/compiled/@ampproject/toolbox-optimizer/**/*',
- ]
- : []),
- ],
+ ignore: ignoreFn,
})
const tracedFiles = new Set()
diff --git a/packages/next/build/webpack/plugins/next-trace-entrypoints-plugin.ts b/packages/next/build/webpack/plugins/next-trace-entrypoints-plugin.ts
index 3efe516f80f92..bcdea824d8e52 100644
--- a/packages/next/build/webpack/plugins/next-trace-entrypoints-plugin.ts
+++ b/packages/next/build/webpack/plugins/next-trace-entrypoints-plugin.ts
@@ -16,6 +16,7 @@ import {
} from '../../webpack-config'
import { NextConfigComplete } from '../../../server/config-shared'
import { loadBindings } from '../../swc'
+import { isMatch } from 'next/dist/compiled/micromatch'
const PLUGIN_NAME = 'TraceEntryPointsPlugin'
const TRACE_IGNORES = [
@@ -173,7 +174,11 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance {
return
}
}
+ const ignores = [...TRACE_IGNORES, ...this.excludeFiles]
+ const ignoreFn = (path: string) => {
+ return isMatch(path, ignores, { contains: true, dot: true })
+ }
const result = await nodeFileTrace([...chunksToTrace], {
base: this.tracingRoot,
processCwd: this.appDir,
@@ -204,7 +209,7 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance {
},
readlink,
stat,
- ignore: [...TRACE_IGNORES, ...this.excludeFiles],
+ ignore: ignoreFn,
mixedModules: true,
})
const reasons = result.reasons
@@ -456,6 +461,15 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance {
}
let fileList: Set
let reasons: NodeFileTraceReasons
+ const ignores = [
+ ...TRACE_IGNORES,
+ ...this.excludeFiles,
+ '**/node_modules/**',
+ ]
+ const ignoreFn = (path: string) => {
+ return isMatch(path, ignores, { contains: true, dot: true })
+ }
+
await finishModulesSpan
.traceChild('node-file-trace', {
traceEntryCount: entriesToTrace.length + '',
@@ -472,11 +486,7 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance {
return doResolve(id, parent, job, !isCjs)
}
: undefined,
- ignore: [
- ...TRACE_IGNORES,
- ...this.excludeFiles,
- '**/node_modules/**',
- ],
+ ignore: ignoreFn,
mixedModules: true,
})
// @ts-ignore
diff --git a/packages/next/cli/next-dev.ts b/packages/next/cli/next-dev.ts
index fa079ccfd3553..f00f4b9d8806e 100755
--- a/packages/next/cli/next-dev.ts
+++ b/packages/next/cli/next-dev.ts
@@ -165,6 +165,8 @@ const nextDev: cliCommand = (argv) => {
const { setGlobal } = require('../trace') as typeof import('../trace')
const { Telemetry } =
require('../telemetry/storage') as typeof import('../telemetry/storage')
+ const findUp =
+ require('next/dist/compiled/find-up') as typeof import('next/dist/compiled/find-up')
// To regenerate the TURBOPACK gradient require('gradient-string')('blue', 'red')('>>> TURBOPACK')
const isTTY = process.stdout.isTTY
@@ -182,59 +184,75 @@ const nextDev: cliCommand = (argv) => {
let babelrc = await getBabelConfigFile(dir)
if (babelrc) babelrc = path.basename(babelrc)
- const rawNextConfig = interopDefault(
- await loadConfig(PHASE_DEVELOPMENT_SERVER, dir, undefined, true)
- ) as NextConfig
-
- const checkUnsupportedCustomConfig = (
- configKey = '',
- parentUserConfig: any,
- parentDefaultConfig: any
- ): boolean => {
- // these should not error
- if (
- configKey === 'serverComponentsExternalPackages' ||
- configKey === 'appDir' ||
- configKey === 'transpilePackages' ||
- configKey === 'reactStrictMode' ||
- configKey === 'swcMinify' ||
- configKey === 'configFileName'
- ) {
- return false
- }
- let userValue = parentUserConfig[configKey]
- let defaultValue = parentDefaultConfig[configKey]
+ let hasNonDefaultConfig
+ let postcssFile
+ let tailwindFile
+ let rawNextConfig: NextConfig
+
+ try {
+ rawNextConfig = interopDefault(
+ await loadConfig(PHASE_DEVELOPMENT_SERVER, dir, undefined, true)
+ ) as NextConfig
+
+ const checkUnsupportedCustomConfig = (
+ configKey = '',
+ parentUserConfig: any,
+ parentDefaultConfig: any
+ ): boolean => {
+ try {
+ // these should not error
+ if (
+ configKey === 'serverComponentsExternalPackages' ||
+ configKey === 'appDir' ||
+ configKey === 'transpilePackages' ||
+ configKey === 'reactStrictMode' ||
+ configKey === 'swcMinify' ||
+ configKey === 'configFileName'
+ ) {
+ return false
+ }
+ let userValue = parentUserConfig?.[configKey]
+ let defaultValue = parentDefaultConfig?.[configKey]
- if (typeof defaultValue !== 'object') {
- return defaultValue !== userValue
+ if (typeof defaultValue !== 'object') {
+ return defaultValue !== userValue
+ }
+ return Object.keys(userValue || {}).some((key: string) => {
+ return checkUnsupportedCustomConfig(key, userValue, defaultValue)
+ })
+ } catch (e) {
+ console.error(
+ `Unexpected error occurred while checking ${configKey}`,
+ e
+ )
+ return false
+ }
}
- return Object.keys(userValue || {}).some((key: string) => {
- return checkUnsupportedCustomConfig(key, userValue, defaultValue)
- })
- }
- const hasNonDefaultConfig = Object.keys(rawNextConfig).some((key) =>
- checkUnsupportedCustomConfig(key, rawNextConfig, defaultConfig)
- )
-
- const findUp =
- require('next/dist/compiled/find-up') as typeof import('next/dist/compiled/find-up')
- const packagePath = findUp.sync('package.json', { cwd: dir })
- let hasSideCar = false
-
- if (packagePath) {
- const pkgData = require(packagePath)
- hasSideCar = Object.values(
- (pkgData.scripts || {}) as Record
- ).some(
- (script) => script.includes('tailwind') || script.includes('postcss')
+ hasNonDefaultConfig = Object.keys(rawNextConfig).some((key) =>
+ checkUnsupportedCustomConfig(key, rawNextConfig, defaultConfig)
)
- }
- let postcssFile = !hasSideCar && (await findConfigPath(dir, 'postcss'))
- let tailwindFile = !hasSideCar && (await findConfigPath(dir, 'tailwind'))
- if (postcssFile) postcssFile = path.basename(postcssFile)
- if (tailwindFile) tailwindFile = path.basename(tailwindFile)
+ const packagePath = findUp.sync('package.json', { cwd: dir })
+ let hasSideCar = false
+
+ if (packagePath) {
+ const pkgData = require(packagePath)
+ hasSideCar = Object.values(
+ (pkgData.scripts || {}) as Record
+ ).some(
+ (script) =>
+ script.includes('tailwind') || script.includes('postcss')
+ )
+ }
+ postcssFile = !hasSideCar && (await findConfigPath(dir, 'postcss'))
+ tailwindFile = !hasSideCar && (await findConfigPath(dir, 'tailwind'))
+
+ if (postcssFile) postcssFile = path.basename(postcssFile)
+ if (tailwindFile) tailwindFile = path.basename(tailwindFile)
+ } catch (e) {
+ console.error('Unexpected error occurred while checking config', e)
+ }
const hasWarningOrError =
tailwindFile || postcssFile || babelrc || hasNonDefaultConfig
@@ -381,6 +399,8 @@ If you cannot make the changes above, but still want to try out\nNext.js v13 wit
console.error(err)
}
)
+ }).catch((err) => {
+ console.error(err)
})
} else {
startServer(devServerOptions)
diff --git a/packages/next/client/compat/router.ts b/packages/next/client/compat/router.ts
new file mode 100644
index 0000000000000..58b1b9f02ed05
--- /dev/null
+++ b/packages/next/client/compat/router.ts
@@ -0,0 +1,17 @@
+import { useContext } from 'react'
+import { RouterContext } from '../../shared/lib/router-context'
+import { NextRouter } from '../router'
+
+/**
+ * useRouter from `next/compat/router` is designed to assist developers
+ * migrating from `pages/` to `app/`. Unlike `next/router`, this hook does not
+ * throw when the `NextRouter` is not mounted, and instead returns `null`. The
+ * more concrete return type here lets developers use this hook within
+ * components that could be shared between both `app/` and `pages/` and handle
+ * to the case where the router is not mounted.
+ *
+ * @returns The `NextRouter` instance if it's available, otherwise `null`.
+ */
+export function useRouter(): NextRouter | null {
+ return useContext(RouterContext)
+}
diff --git a/packages/next/client/components/app-router-headers.ts b/packages/next/client/components/app-router-headers.ts
new file mode 100644
index 0000000000000..1eb2747dd1cd2
--- /dev/null
+++ b/packages/next/client/components/app-router-headers.ts
@@ -0,0 +1,5 @@
+export const RSC = 'RSC' as const
+export const NEXT_ROUTER_STATE_TREE = 'Next-Router-State-Tree' as const
+export const NEXT_ROUTER_PREFETCH = 'Next-Router-Prefetch' as const
+export const RSC_VARY_HEADER =
+ `${RSC}, ${NEXT_ROUTER_STATE_TREE}, ${NEXT_ROUTER_PREFETCH}` as const
diff --git a/packages/next/client/components/app-router.tsx b/packages/next/client/components/app-router.tsx
index a1ed740d1094f..d29f680d5051c 100644
--- a/packages/next/client/components/app-router.tsx
+++ b/packages/next/client/components/app-router.tsx
@@ -29,6 +29,11 @@ import {
} from '../../shared/lib/hooks-client-context'
import { useReducerWithReduxDevtools } from './use-reducer-with-devtools'
import { ErrorBoundary, GlobalErrorComponent } from './error-boundary'
+import {
+ NEXT_ROUTER_PREFETCH,
+ NEXT_ROUTER_STATE_TREE,
+ RSC,
+} from './app-router-headers'
function urlToUrlWithoutFlightMarker(url: string): URL {
const urlWithoutFlightParameters = new URL(url, location.origin)
@@ -53,18 +58,18 @@ export async function fetchServerResponse(
prefetch?: true
): Promise<[FlightData: FlightData, canonicalUrlOverride: URL | undefined]> {
const headers: {
- __rsc__: '1'
- __next_router_state_tree__: string
- __next_router_prefetch__?: '1'
+ [RSC]: '1'
+ [NEXT_ROUTER_STATE_TREE]: string
+ [NEXT_ROUTER_PREFETCH]?: '1'
} = {
// Enable flight response
- __rsc__: '1',
+ [RSC]: '1',
// Provide the current router state
- __next_router_state_tree__: JSON.stringify(flightRouterState),
+ [NEXT_ROUTER_STATE_TREE]: JSON.stringify(flightRouterState),
}
if (prefetch) {
// Enable prefetch response
- headers.__next_router_prefetch__ = '1'
+ headers[NEXT_ROUTER_PREFETCH] = '1'
}
const res = await fetch(url.toString(), {
diff --git a/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx b/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx
index 750fb7da79ac4..f5a4f91f9fa80 100644
--- a/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx
+++ b/packages/next/client/components/react-dev-overlay/hot-reloader-client.tsx
@@ -14,13 +14,17 @@ import { errorOverlayReducer } from './internal/error-overlay-reducer'
import {
ACTION_BUILD_OK,
ACTION_BUILD_ERROR,
+ ACTION_BEFORE_REFRESH,
ACTION_REFRESH,
ACTION_UNHANDLED_ERROR,
ACTION_UNHANDLED_REJECTION,
} from './internal/error-overlay-reducer'
import { parseStack } from './internal/helpers/parseStack'
import ReactDevOverlay from './internal/ReactDevOverlay'
-import { useErrorHandler } from './internal/helpers/use-error-handler'
+import {
+ RuntimeErrorHandler,
+ useErrorHandler,
+} from './internal/helpers/use-error-handler'
import {
useSendMessage,
useWebsocket,
@@ -30,6 +34,7 @@ import {
interface Dispatcher {
onBuildOk(): void
onBuildError(message: string): void
+ onBeforeRefresh(): void
onRefresh(): void
}
@@ -38,10 +43,15 @@ type PongEvent = any
let mostRecentCompilationHash: any = null
let __nextDevClientId = Math.round(Math.random() * 100 + Date.now())
-let hadRuntimeError = false
// let startLatency = undefined
+function onBeforeFastRefresh(dispatcher: Dispatcher, hasUpdates: boolean) {
+ if (hasUpdates) {
+ dispatcher.onBeforeRefresh()
+ }
+}
+
function onFastRefresh(dispatcher: Dispatcher, hasUpdates: boolean) {
dispatcher.onBuildOk()
if (hasUpdates) {
@@ -104,6 +114,7 @@ function performFullReload(err: any, sendMessage: any) {
// Attempt to update code on the fly, fall back to a hard reload.
function tryApplyUpdates(
+ onBeforeUpdate: (hasUpdates: boolean) => void,
onHotUpdateSuccess: (hasUpdates: boolean) => void,
sendMessage: any,
dispatcher: Dispatcher
@@ -114,7 +125,7 @@ function tryApplyUpdates(
}
function handleApplyUpdates(err: any, updatedModules: any) {
- if (err || hadRuntimeError || !updatedModules) {
+ if (err || RuntimeErrorHandler.hadRuntimeError || !updatedModules) {
if (err) {
console.warn(
'[Fast Refresh] performing full reload\n\n' +
@@ -124,7 +135,7 @@ function tryApplyUpdates(
'It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.\n' +
'Fast Refresh requires at least one parent function component in your React tree.'
)
- } else if (hadRuntimeError) {
+ } else if (RuntimeErrorHandler.hadRuntimeError) {
console.warn(
'[Fast Refresh] performing full reload because your application had an unrecoverable error'
)
@@ -142,6 +153,7 @@ function tryApplyUpdates(
if (isUpdateAvailable()) {
// While we were updating, there was a new update! Do it again.
tryApplyUpdates(
+ hasUpdates ? () => {} : onBeforeUpdate,
hasUpdates ? () => dispatcher.onBuildOk() : onHotUpdateSuccess,
sendMessage,
dispatcher
@@ -161,14 +173,25 @@ function tryApplyUpdates(
// https://webpack.js.org/api/hot-module-replacement/#check
// @ts-expect-error module.hot exists
- module.hot.check(/* autoApply */ true).then(
- (updatedModules: any) => {
- handleApplyUpdates(null, updatedModules)
- },
- (err: any) => {
- handleApplyUpdates(err, null)
- }
- )
+ module.hot
+ .check(/* autoApply */ false)
+ .then((updatedModules: any) => {
+ const hasUpdates = Boolean(updatedModules.length)
+ if (typeof onBeforeUpdate === 'function') {
+ onBeforeUpdate(hasUpdates)
+ }
+ // https://webpack.js.org/api/hot-module-replacement/#apply
+ // @ts-expect-error module.hot exists
+ return module.hot.apply()
+ })
+ .then(
+ (updatedModules: any) => {
+ handleApplyUpdates(null, updatedModules)
+ },
+ (err: any) => {
+ handleApplyUpdates(err, null)
+ }
+ )
}
function processMessage(
@@ -260,6 +283,9 @@ function processMessage(
// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates(
+ function onBeforeHotUpdate(hasUpdates: boolean) {
+ onBeforeFastRefresh(dispatcher, hasUpdates)
+ },
function onSuccessfulHotUpdate(hasUpdates: any) {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
@@ -287,6 +313,9 @@ function processMessage(
// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates(
+ function onBeforeHotUpdate(hasUpdates: boolean) {
+ onBeforeFastRefresh(dispatcher, hasUpdates)
+ },
function onSuccessfulHotUpdate(hasUpdates: any) {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
@@ -306,7 +335,7 @@ function processMessage(
clientId: __nextDevClientId,
})
)
- if (hadRuntimeError) {
+ if (RuntimeErrorHandler.hadRuntimeError) {
return window.location.reload()
}
startTransition(() => {
@@ -361,6 +390,7 @@ export default function HotReload({
nextId: 1,
buildError: null,
errors: [],
+ refreshState: { type: 'idle' },
})
const dispatcher = useMemo((): Dispatcher => {
return {
@@ -370,72 +400,29 @@ export default function HotReload({
onBuildError(message: string): void {
dispatch({ type: ACTION_BUILD_ERROR, message })
},
+ onBeforeRefresh(): void {
+ dispatch({ type: ACTION_BEFORE_REFRESH })
+ },
onRefresh(): void {
dispatch({ type: ACTION_REFRESH })
},
}
}, [dispatch])
- const handleOnUnhandledError = useCallback(
- (ev: WindowEventMap['error']): void => {
- if (
- ev.error &&
- ev.error.digest &&
- (ev.error.digest.startsWith('NEXT_REDIRECT') ||
- ev.error.digest === 'NEXT_NOT_FOUND')
- ) {
- ev.preventDefault()
- return
- }
-
- hadRuntimeError = true
- const error = ev?.error
- if (
- !error ||
- !(error instanceof Error) ||
- typeof error.stack !== 'string'
- ) {
- // A non-error was thrown, we don't have anything to show. :-(
- return
- }
-
- if (
- error.message.match(/(hydration|content does not match|did not match)/i)
- ) {
- error.message += `\n\nSee more info here: https://nextjs.org/docs/messages/react-hydration-error`
- }
-
- const e = error
- dispatch({
- type: ACTION_UNHANDLED_ERROR,
- reason: error,
- frames: parseStack(e.stack!),
- })
- },
- []
- )
- const handleOnUnhandledRejection = useCallback(
- (ev: WindowEventMap['unhandledrejection']): void => {
- hadRuntimeError = true
- const reason = ev?.reason
- if (
- !reason ||
- !(reason instanceof Error) ||
- typeof reason.stack !== 'string'
- ) {
- // A non-error was thrown, we don't have anything to show. :-(
- return
- }
-
- const e = reason
- dispatch({
- type: ACTION_UNHANDLED_REJECTION,
- reason: reason,
- frames: parseStack(e.stack!),
- })
- },
- []
- )
+ const handleOnUnhandledError = useCallback((error: Error): void => {
+ dispatch({
+ type: ACTION_UNHANDLED_ERROR,
+ reason: error,
+ frames: parseStack(error.stack!),
+ })
+ }, [])
+ const handleOnUnhandledRejection = useCallback((reason: Error): void => {
+ dispatch({
+ type: ACTION_UNHANDLED_REJECTION,
+ reason: reason,
+ frames: parseStack(reason.stack!),
+ })
+ }, [])
useErrorHandler(handleOnUnhandledError, handleOnUnhandledRejection)
const webSocketRef = useWebsocket(assetPrefix)
diff --git a/packages/next/client/components/react-dev-overlay/internal/error-overlay-reducer.ts b/packages/next/client/components/react-dev-overlay/internal/error-overlay-reducer.ts
index 2ae8fe1a1a351..0553640734b6a 100644
--- a/packages/next/client/components/react-dev-overlay/internal/error-overlay-reducer.ts
+++ b/packages/next/client/components/react-dev-overlay/internal/error-overlay-reducer.ts
@@ -3,6 +3,7 @@ import { SupportedErrorEvent } from './container/Errors'
export const ACTION_BUILD_OK = 'build-ok'
export const ACTION_BUILD_ERROR = 'build-error'
+export const ACTION_BEFORE_REFRESH = 'before-fast-refresh'
export const ACTION_REFRESH = 'fast-refresh'
export const ACTION_UNHANDLED_ERROR = 'unhandled-error'
export const ACTION_UNHANDLED_REJECTION = 'unhandled-rejection'
@@ -14,6 +15,9 @@ interface BuildErrorAction {
type: typeof ACTION_BUILD_ERROR
message: string
}
+interface BeforeFastRefreshAction {
+ type: typeof ACTION_BEFORE_REFRESH
+}
interface FastRefreshAction {
type: typeof ACTION_REFRESH
}
@@ -28,6 +32,15 @@ export interface UnhandledRejectionAction {
frames: StackFrame[]
}
+export type FastRefreshState =
+ | {
+ type: 'idle'
+ }
+ | {
+ type: 'pending'
+ errors: SupportedErrorEvent[]
+ }
+
export interface OverlayState {
nextId: number
buildError: string | null
@@ -35,6 +48,20 @@ export interface OverlayState {
rootLayoutMissingTagsError?: {
missingTags: string[]
}
+ refreshState: FastRefreshState
+}
+
+function pushErrorFilterDuplicates(
+ errors: SupportedErrorEvent[],
+ err: SupportedErrorEvent
+): SupportedErrorEvent[] {
+ return [
+ ...errors.filter((e) => {
+ // Filter out duplicate errors
+ return e.event.reason !== err.event.reason
+ }),
+ err,
+ ]
}
export function errorOverlayReducer(
@@ -42,6 +69,7 @@ export function errorOverlayReducer(
action: Readonly<
| BuildOkAction
| BuildErrorAction
+ | BeforeFastRefreshAction
| FastRefreshAction
| UnhandledErrorAction
| UnhandledRejectionAction
@@ -54,21 +82,56 @@ export function errorOverlayReducer(
case ACTION_BUILD_ERROR: {
return { ...state, buildError: action.message }
}
+ case ACTION_BEFORE_REFRESH: {
+ return { ...state, refreshState: { type: 'pending', errors: [] } }
+ }
case ACTION_REFRESH: {
- return { ...state, buildError: null, errors: [] }
+ return {
+ ...state,
+ buildError: null,
+ errors:
+ // Errors can come in during updates. In this case, UNHANDLED_ERROR
+ // and UNHANDLED_REJECTION events might be dispatched between the
+ // BEFORE_REFRESH and the REFRESH event. We want to keep those errors
+ // around until the next refresh. Otherwise we run into a race
+ // condition where those errors would be cleared on refresh completion
+ // before they can be displayed.
+ state.refreshState.type === 'pending'
+ ? state.refreshState.errors
+ : [],
+ refreshState: { type: 'idle' },
+ }
}
case ACTION_UNHANDLED_ERROR:
case ACTION_UNHANDLED_REJECTION: {
- return {
- ...state,
- nextId: state.nextId + 1,
- errors: [
- ...state.errors.filter((err) => {
- // Filter out duplicate errors
- return err.event.reason !== action.reason
- }),
- { id: state.nextId, event: action },
- ],
+ switch (state.refreshState.type) {
+ case 'idle': {
+ return {
+ ...state,
+ nextId: state.nextId + 1,
+ errors: pushErrorFilterDuplicates(state.errors, {
+ id: state.nextId,
+ event: action,
+ }),
+ }
+ }
+ case 'pending': {
+ return {
+ ...state,
+ nextId: state.nextId + 1,
+ refreshState: {
+ ...state.refreshState,
+ errors: pushErrorFilterDuplicates(state.refreshState.errors, {
+ id: state.nextId,
+ event: action,
+ }),
+ },
+ }
+ }
+ default:
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const _: never = state.refreshState
+ return state
}
}
default: {
diff --git a/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts b/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts
index d5867de015dca..bd96c248ea0c7 100644
--- a/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts
+++ b/packages/next/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts
@@ -1,34 +1,111 @@
-import { useEffect, useRef } from 'react'
+import { useEffect } from 'react'
+
+export type ErrorHandler = (error: Error) => void
+
+export const RuntimeErrorHandler = {
+ hadRuntimeError: false,
+}
+
+function isNextRouterError(error: any): boolean {
+ return (
+ error &&
+ error.digest &&
+ (error.digest.startsWith('NEXT_REDIRECT') ||
+ error.digest === 'NEXT_NOT_FOUND')
+ )
+}
+
+function isHydrationError(error: Error): boolean {
+ return (
+ error.message.match(/(hydration|content does not match|did not match)/i) !=
+ null
+ )
+}
+
+try {
+ Error.stackTraceLimit = 50
+} catch {}
+
+const errorQueue: Array = []
+const rejectionQueue: Array = []
+const errorHandlers: Array = []
+const rejectionHandlers: Array = []
+
+if (typeof window !== 'undefined') {
+ // These event handlers must be added outside of the hook because there is no
+ // guarantee that the hook will be alive in a mounted component in time to
+ // when the errors occur.
+ window.addEventListener('error', (ev: WindowEventMap['error']): void => {
+ if (isNextRouterError(ev.error)) {
+ ev.preventDefault()
+ return
+ }
+
+ RuntimeErrorHandler.hadRuntimeError = true
+
+ const error = ev?.error
+ if (
+ !error ||
+ !(error instanceof Error) ||
+ typeof error.stack !== 'string'
+ ) {
+ // A non-error was thrown, we don't have anything to show. :-(
+ return
+ }
+
+ if (isHydrationError(error)) {
+ error.message += `\n\nSee more info here: https://nextjs.org/docs/messages/react-hydration-error`
+ }
+
+ const e = error
+ errorQueue.push(e)
+ for (const handler of errorHandlers) {
+ handler(e)
+ }
+ })
+ window.addEventListener(
+ 'unhandledrejection',
+ (ev: WindowEventMap['unhandledrejection']): void => {
+ RuntimeErrorHandler.hadRuntimeError = true
+
+ const reason = ev?.reason
+ if (
+ !reason ||
+ !(reason instanceof Error) ||
+ typeof reason.stack !== 'string'
+ ) {
+ // A non-error was thrown, we don't have anything to show. :-(
+ return
+ }
+
+ const e = reason
+ rejectionQueue.push(e)
+ for (const handler of rejectionHandlers) {
+ handler(e)
+ }
+ }
+ )
+}
export function useErrorHandler(
- handleOnUnhandledError: (event: WindowEventMap['error']) => void,
- handleOnUnhandledRejection: (
- event: WindowEventMap['unhandledrejection']
- ) => void
+ handleOnUnhandledError: ErrorHandler,
+ handleOnUnhandledRejection: ErrorHandler
) {
- const stacktraceLimitRef = useRef()
-
useEffect(() => {
- try {
- const limit = Error.stackTraceLimit
- Error.stackTraceLimit = 50
- stacktraceLimitRef.current = limit
- } catch {}
-
- window.addEventListener('error', handleOnUnhandledError)
- window.addEventListener('unhandledrejection', handleOnUnhandledRejection)
- return () => {
- if (stacktraceLimitRef.current !== undefined) {
- try {
- Error.stackTraceLimit = stacktraceLimitRef.current
- } catch {}
- stacktraceLimitRef.current = undefined
- }
+ // Handle queued errors.
+ errorQueue.forEach(handleOnUnhandledError)
+ rejectionQueue.forEach(handleOnUnhandledRejection)
+
+ // Listen to new errors.
+ errorHandlers.push(handleOnUnhandledError)
+ rejectionHandlers.push(handleOnUnhandledRejection)
- window.removeEventListener('error', handleOnUnhandledError)
- window.removeEventListener(
- 'unhandledrejection',
- handleOnUnhandledRejection
+ return () => {
+ // Remove listeners.
+ errorHandlers.splice(errorHandlers.indexOf(handleOnUnhandledError), 1)
+ rejectionHandlers.splice(
+ rejectionHandlers.indexOf(handleOnUnhandledRejection),
+ 1
)
}
}, [handleOnUnhandledError, handleOnUnhandledRejection])
diff --git a/packages/next/client/components/reducer.ts b/packages/next/client/components/reducer.ts
index e779e439a4b48..074deb8acaa71 100644
--- a/packages/next/client/components/reducer.ts
+++ b/packages/next/client/components/reducer.ts
@@ -1112,6 +1112,8 @@ function clientReducer(
tree: tree,
}
}
+ // TODO-APP: Add test for not scrolling to nearest layout when calling refresh.
+ // TODO-APP: Add test for startTransition(() => {router.push('/'); router.refresh();}), that case should scroll.
case ACTION_REFRESH: {
const { cache, mutable } = action
const href = state.canonicalUrl
@@ -1151,7 +1153,7 @@ function clientReducer(
pushRef: state.pushRef,
// Apply focus and scroll.
// TODO-APP: might need to disable this for Fast Refresh.
- focusAndScrollRef: { apply: true },
+ focusAndScrollRef: { apply: false },
cache: cache,
prefetchCache: state.prefetchCache,
tree: mutable.patchedTree,
diff --git a/packages/next/client/dev/error-overlay/hot-dev-client.js b/packages/next/client/dev/error-overlay/hot-dev-client.js
index a9c8d0e9d5fd1..8d128e38b2198 100644
--- a/packages/next/client/dev/error-overlay/hot-dev-client.js
+++ b/packages/next/client/dev/error-overlay/hot-dev-client.js
@@ -30,6 +30,7 @@ import {
register,
onBuildError,
onBuildOk,
+ onBeforeRefresh,
onRefresh,
} from 'next/dist/compiled/@next/react-dev-overlay/dist/client'
import stripAnsi from 'next/dist/compiled/strip-ansi'
@@ -98,11 +99,7 @@ function handleSuccess() {
// Attempt to apply hot updates or reload.
if (isHotUpdate) {
- tryApplyUpdates(function onSuccessfulHotUpdate(hasUpdates) {
- // Only dismiss it when we're sure it's a hot update.
- // Otherwise it would flicker right before the reload.
- onFastRefresh(hasUpdates)
- })
+ tryApplyUpdates(onBeforeFastRefresh, onFastRefresh)
}
}
@@ -139,11 +136,7 @@ function handleWarnings(warnings) {
// Attempt to apply hot updates or reload.
if (isHotUpdate) {
- tryApplyUpdates(function onSuccessfulHotUpdate(hasUpdates) {
- // Only dismiss it when we're sure it's a hot update.
- // Otherwise it would flicker right before the reload.
- onFastRefresh(hasUpdates)
- })
+ tryApplyUpdates(onBeforeFastRefresh, onFastRefresh)
}
}
@@ -182,9 +175,19 @@ function handleErrors(errors) {
let startLatency = undefined
+function onBeforeFastRefresh(hasUpdates) {
+ if (hasUpdates) {
+ // Only trigger a pending state if we have updates to apply
+ // (cf. onFastRefresh)
+ onBeforeRefresh()
+ }
+}
+
function onFastRefresh(hasUpdates) {
onBuildOk()
if (hasUpdates) {
+ // Only complete a pending state if we applied updates
+ // (cf. onBeforeFastRefresh)
onRefresh()
}
@@ -301,7 +304,7 @@ function afterApplyUpdates(fn) {
}
// Attempt to update code on the fly, fall back to a hard reload.
-function tryApplyUpdates(onHotUpdateSuccess) {
+function tryApplyUpdates(onBeforeHotUpdate, onHotUpdateSuccess) {
if (!module.hot) {
// HotModuleReplacementPlugin is not in Webpack configuration.
console.error('HotModuleReplacementPlugin is not in Webpack configuration.')
@@ -342,7 +345,11 @@ function tryApplyUpdates(onHotUpdateSuccess) {
if (isUpdateAvailable()) {
// While we were updating, there was a new update! Do it again.
- tryApplyUpdates(hasUpdates ? onBuildOk : onHotUpdateSuccess)
+ // However, this time, don't trigger a pending refresh state.
+ tryApplyUpdates(
+ hasUpdates ? undefined : onBeforeHotUpdate,
+ hasUpdates ? onBuildOk : onHotUpdateSuccess
+ )
} else {
onBuildOk()
if (process.env.__NEXT_TEST_MODE) {
@@ -357,14 +364,23 @@ function tryApplyUpdates(onHotUpdateSuccess) {
}
// https://webpack.js.org/api/hot-module-replacement/#check
- module.hot.check(/* autoApply */ true).then(
- (updatedModules) => {
- handleApplyUpdates(null, updatedModules)
- },
- (err) => {
- handleApplyUpdates(err, null)
- }
- )
+ module.hot
+ .check(/* autoApply */ false)
+ .then((updatedModules) => {
+ if (typeof onBeforeHotUpdate === 'function') {
+ const hasUpdates = Boolean(updatedModules.length)
+ onBeforeHotUpdate(hasUpdates)
+ }
+ return module.hot.apply()
+ })
+ .then(
+ (updatedModules) => {
+ handleApplyUpdates(null, updatedModules)
+ },
+ (err) => {
+ handleApplyUpdates(err, null)
+ }
+ )
}
function performFullReload(err) {
diff --git a/packages/next/client/route-announcer.tsx b/packages/next/client/route-announcer.tsx
index cbd26f09ea7bd..3b59fd84d96bc 100644
--- a/packages/next/client/route-announcer.tsx
+++ b/packages/next/client/route-announcer.tsx
@@ -17,7 +17,7 @@ const nextjsRouteAnnouncerStyles: React.CSSProperties = {
}
export const RouteAnnouncer = () => {
- const { asPath } = useRouter(true)
+ const { asPath } = useRouter()
const [routeAnnouncement, setRouteAnnouncement] = React.useState('')
// Only announce the path change, but not for the first load because screen
diff --git a/packages/next/client/router.ts b/packages/next/client/router.ts
index dbcd11a9b2d62..7f4acbd51a60c 100644
--- a/packages/next/client/router.ts
+++ b/packages/next/client/router.ts
@@ -129,12 +129,12 @@ export default singletonRouter as SingletonRouter
// Reexport the withRoute HOC
export { default as withRouter } from './with-router'
-export function useRouter(throwOnMissing: true): NextRouter
-export function useRouter(): NextRouter
-export function useRouter(throwOnMissing?: boolean) {
+export function useRouter(): NextRouter {
const router = React.useContext(RouterContext)
- if (!router && throwOnMissing) {
- throw new Error('invariant expected pages router to be mounted')
+ if (!router) {
+ throw new Error(
+ 'Error: NextRouter was not mounted. https://nextjs.org/docs/messages/next-router-not-mounted'
+ )
}
return router
diff --git a/packages/next/compat/router.d.ts b/packages/next/compat/router.d.ts
new file mode 100644
index 0000000000000..c458473721738
--- /dev/null
+++ b/packages/next/compat/router.d.ts
@@ -0,0 +1 @@
+export * from '../dist/client/compat/router'
diff --git a/packages/next/compat/router.js b/packages/next/compat/router.js
new file mode 100644
index 0000000000000..1b46d46053276
--- /dev/null
+++ b/packages/next/compat/router.js
@@ -0,0 +1 @@
+module.exports = require('../dist/client/compat/router')
diff --git a/packages/next/lib/typescript/writeVscodeConfigurations.ts b/packages/next/lib/typescript/writeVscodeConfigurations.ts
index d8272239d895c..577b39fe4bb47 100644
--- a/packages/next/lib/typescript/writeVscodeConfigurations.ts
+++ b/packages/next/lib/typescript/writeVscodeConfigurations.ts
@@ -6,7 +6,8 @@ import * as CommentJson from 'next/dist/compiled/comment-json'
// Write .vscode settings to enable Next.js typescript plugin.
export async function writeVscodeConfigurations(
- baseDir: string
+ baseDir: string,
+ tsPath: string
): Promise {
try {
const vscodeSettings = path.join(baseDir, '.vscode', 'settings.json')
@@ -24,7 +25,7 @@ export async function writeVscodeConfigurations(
}
}
- const libPath = './node_modules/typescript/lib'
+ const libPath = path.relative(baseDir, path.dirname(tsPath))
if (
settings['typescript.tsdk'] === libPath &&
settings['typescript.enablePromptUseWorkspaceTsdk']
diff --git a/packages/next/lib/verifyTypeScriptSetup.ts b/packages/next/lib/verifyTypeScriptSetup.ts
index e48e18ecbda6d..d280046e3f1e5 100644
--- a/packages/next/lib/verifyTypeScriptSetup.ts
+++ b/packages/next/lib/verifyTypeScriptSetup.ts
@@ -103,8 +103,9 @@ export async function verifyTypeScriptSetup({
}
// Load TypeScript after we're sure it exists:
+ const tsPath = deps.resolved.get('typescript')!
const ts = (await Promise.resolve(
- require(deps.resolved.get('typescript')!)
+ require(tsPath)
)) as typeof import('typescript')
if (semver.lt(ts.version, '4.3.2')) {
@@ -125,7 +126,7 @@ export async function verifyTypeScriptSetup({
await writeAppTypeDeclarations(dir, !disableStaticImages)
if (isAppDirEnabled) {
- await writeVscodeConfigurations(dir)
+ await writeVscodeConfigurations(dir, tsPath)
}
let result
diff --git a/packages/next/package.json b/packages/next/package.json
index 176c013378b47..b323ada40bf12 100644
--- a/packages/next/package.json
+++ b/packages/next/package.json
@@ -1,6 +1,6 @@
{
"name": "next",
- "version": "13.0.3-canary.0",
+ "version": "13.0.3-canary.1",
"description": "The React Framework",
"main": "./dist/server/next.js",
"license": "MIT",
@@ -74,7 +74,7 @@
]
},
"dependencies": {
- "@next/env": "13.0.3-canary.0",
+ "@next/env": "13.0.3-canary.1",
"@swc/helpers": "0.4.11",
"caniuse-lite": "^1.0.30001406",
"postcss": "8.4.14",
@@ -125,11 +125,11 @@
"@hapi/accept": "5.0.2",
"@napi-rs/cli": "2.12.0",
"@napi-rs/triples": "1.1.0",
- "@next/polyfill-module": "13.0.3-canary.0",
- "@next/polyfill-nomodule": "13.0.3-canary.0",
- "@next/react-dev-overlay": "13.0.3-canary.0",
- "@next/react-refresh-utils": "13.0.3-canary.0",
- "@next/swc": "13.0.3-canary.0",
+ "@next/polyfill-module": "13.0.3-canary.1",
+ "@next/polyfill-nomodule": "13.0.3-canary.1",
+ "@next/react-dev-overlay": "13.0.3-canary.1",
+ "@next/react-refresh-utils": "13.0.3-canary.1",
+ "@next/swc": "13.0.3-canary.1",
"@segment/ajv-human-errors": "2.1.2",
"@taskr/clear": "1.1.0",
"@taskr/esnext": "1.1.0",
diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx
index 277420c014b2f..d5fd2404538d9 100644
--- a/packages/next/server/app-render.tsx
+++ b/packages/next/server/app-render.tsx
@@ -38,6 +38,11 @@ import { NOT_FOUND_ERROR_CODE } from '../client/components/not-found'
import { HeadManagerContext } from '../shared/lib/head-manager-context'
import { Writable } from 'stream'
import stringHash from 'next/dist/compiled/string-hash'
+import {
+ NEXT_ROUTER_PREFETCH,
+ NEXT_ROUTER_STATE_TREE,
+ RSC,
+} from '../client/components/app-router-headers'
const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'
@@ -686,16 +691,16 @@ function getScriptNonceFromHeader(cspHeaderValue: string): string | undefined {
return nonce
}
-const FLIGHT_PARAMETERS = [
- '__rsc__',
- '__next_router_state_tree__',
- '__next_router_prefetch__',
+export const FLIGHT_PARAMETERS = [
+ [RSC],
+ [NEXT_ROUTER_STATE_TREE],
+ [NEXT_ROUTER_PREFETCH],
] as const
function headersWithoutFlight(headers: IncomingHttpHeaders) {
const newHeaders = { ...headers }
for (const param of FLIGHT_PARAMETERS) {
- delete newHeaders[param]
+ delete newHeaders[param.toString().toLowerCase()]
}
return newHeaders
}
@@ -728,7 +733,7 @@ export async function renderToHTMLOrFlight(
*/
const isStaticGeneration =
renderOpts.supportsDynamicHTML !== true && !renderOpts.isBot
- const isFlight = req.headers.__rsc__ !== undefined
+ const isFlight = req.headers[RSC.toLowerCase()] !== undefined
const capturedErrors: Error[] = []
const allCapturedErrors: Error[] = []
@@ -786,7 +791,8 @@ export async function renderToHTMLOrFlight(
// don't modify original query object
query = Object.assign({}, query)
- const isPrefetch = req.headers.__next_router_prefetch__ !== undefined
+ const isPrefetch =
+ req.headers[NEXT_ROUTER_PREFETCH.toLowerCase()] !== undefined
// TODO-APP: verify the tree is valid
// TODO-APP: verify query param is single value (not an array)
@@ -795,8 +801,10 @@ export async function renderToHTMLOrFlight(
* Router state provided from the client-side router. Used to handle rendering from the common layout down.
*/
let providedFlightRouterState: FlightRouterState = isFlight
- ? req.headers.__next_router_state_tree__
- ? JSON.parse(req.headers.__next_router_state_tree__ as string)
+ ? req.headers[NEXT_ROUTER_STATE_TREE.toLowerCase()]
+ ? JSON.parse(
+ req.headers[NEXT_ROUTER_STATE_TREE.toLowerCase()] as string
+ )
: undefined
: undefined
diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts
index 05b1ca0d9421e..1139b72994e82 100644
--- a/packages/next/server/base-server.ts
+++ b/packages/next/server/base-server.ts
@@ -74,6 +74,8 @@ import { getHostname } from '../shared/lib/get-hostname'
import { parseUrl as parseUrlUtil } from '../shared/lib/router/utils/parse-url'
import { getNextPathnameInfo } from '../shared/lib/router/utils/get-next-pathname-info'
import { MiddlewareMatcher } from '../build/analysis/get-page-static-info'
+import { RSC, RSC_VARY_HEADER } from '../client/components/app-router-headers'
+import { FLIGHT_PARAMETERS } from './app-render'
export type FindComponentsResult = {
components: LoadComponentsReturnType
@@ -1053,12 +1055,9 @@ export default abstract class Server {
(isSSG || hasServerProps)
if (isAppPath) {
- res.setHeader(
- 'vary',
- '__rsc__, __next_router_state_tree__, __next_router_prefetch__'
- )
+ res.setHeader('vary', RSC_VARY_HEADER)
- if (isSSG && req.headers['__rsc__']) {
+ if (isSSG && req.headers[RSC.toLowerCase()]) {
if (!this.minimalMode) {
isDataReq = true
}
@@ -1067,9 +1066,9 @@ export default abstract class Server {
opts.runtime !== 'experimental-edge' ||
(this.serverOptions as any).webServerConfig
) {
- delete req.headers['__rsc__']
- delete req.headers['__next_router_state_tree__']
- delete req.headers['__next_router_prefetch__']
+ for (const param of FLIGHT_PARAMETERS) {
+ delete req.headers[param.toString().toLowerCase()]
+ }
}
}
}
@@ -1096,9 +1095,9 @@ export default abstract class Server {
)
}
- // Don't delete query.__rsc__ yet, it still needs to be used in renderToHTML later
+ // Don't delete headers[RSC] yet, it still needs to be used in renderToHTML later
const isFlightRequest = Boolean(
- this.serverComponentManifest && req.headers.__rsc__
+ this.serverComponentManifest && req.headers[RSC.toLowerCase()]
)
// we need to ensure the status code if /404 is visited directly
diff --git a/packages/next/server/internal-utils.ts b/packages/next/server/internal-utils.ts
index 76f41fc0cecbe..b51e327ac8b79 100644
--- a/packages/next/server/internal-utils.ts
+++ b/packages/next/server/internal-utils.ts
@@ -5,11 +5,6 @@ const INTERNAL_QUERY_NAMES = [
'__nextLocale',
'__nextDefaultLocale',
'__nextIsNotFound',
- // RSC
- '__rsc__',
- // Routing
- '__next_router_state_tree__',
- '__next_router_prefetch__',
] as const
const EXTENDED_INTERNAL_QUERY_NAMES = ['__nextDataReq'] as const
diff --git a/packages/next/server/next-typescript.ts b/packages/next/server/next-typescript.ts
index 3daf55ab116ca..b4946c6eb4459 100644
--- a/packages/next/server/next-typescript.ts
+++ b/packages/next/server/next-typescript.ts
@@ -29,6 +29,13 @@ const DISALLOWED_SERVER_REACT_APIS: string[] = [
const ALLOWED_EXPORTS = ['config', 'generateStaticParams']
+const NEXT_TS_ERRORS = {
+ INVALID_SERVER_API: 71001,
+ INVALID_ENTRY_EXPORT: 71002,
+ INVALID_OPTION_VALUE: 71003,
+ MISPLACED_CLIENT_ENTRY: 71004,
+}
+
const API_DOCS: Record<
string,
{
@@ -187,19 +194,34 @@ export function createTSPlugin(modules: {
)
}
- function getIsClientEntry(fileName: string) {
+ function getIsClientEntry(
+ fileName: string,
+ throwOnInvalidDirective?: boolean
+ ) {
const source = info.languageService.getProgram()?.getSourceFile(fileName)
if (source) {
let isClientEntry = false
let isDirective = true
ts.forEachChild(source!, (node) => {
- if (isClientEntry || !isDirective) return
-
- if (isDirective && ts.isExpressionStatement(node)) {
- if (ts.isStringLiteral(node.expression)) {
- if (node.expression.text === 'use client') {
+ if (
+ ts.isExpressionStatement(node) &&
+ ts.isStringLiteral(node.expression)
+ ) {
+ if (node.expression.text === 'use client') {
+ if (isDirective) {
isClientEntry = true
+ } else {
+ if (throwOnInvalidDirective) {
+ const e = {
+ messageText:
+ 'The `"use client"` directive must be put at the top of the file.',
+ start: node.expression.getStart(),
+ length:
+ node.expression.getEnd() - node.expression.getStart(),
+ }
+ throw e
+ }
}
}
} else {
@@ -473,7 +495,19 @@ export function createTSPlugin(modules: {
const source = info.languageService.getProgram()?.getSourceFile(fileName)
if (source) {
- const isClientEntry = getIsClientEntry(fileName)
+ let isClientEntry = false
+
+ try {
+ isClientEntry = getIsClientEntry(fileName, true)
+ } catch (e: any) {
+ prior.push({
+ file: source,
+ category: ts.DiagnosticCategory.Error,
+ code: NEXT_TS_ERRORS.MISPLACED_CLIENT_ENTRY,
+ ...e,
+ })
+ isClientEntry = false
+ }
ts.forEachChild(source!, (node) => {
if (ts.isImportDeclaration(node)) {
@@ -492,7 +526,7 @@ export function createTSPlugin(modules: {
prior.push({
file: source,
category: ts.DiagnosticCategory.Error,
- code: 71001,
+ code: NEXT_TS_ERRORS.INVALID_SERVER_API,
messageText: `"${name}" is not allowed in Server Components.`,
start: element.name.getStart(),
length:
@@ -519,7 +553,7 @@ export function createTSPlugin(modules: {
prior.push({
file: source,
category: ts.DiagnosticCategory.Error,
- code: 71002,
+ code: NEXT_TS_ERRORS.INVALID_ENTRY_EXPORT,
messageText: `"${name.text}" is not a valid Next.js entry export value.`,
start: name.getStart(),
length: name.getEnd() - name.getStart(),
@@ -593,7 +627,7 @@ export function createTSPlugin(modules: {
prior.push({
file: source,
category: ts.DiagnosticCategory.Error,
- code: 71003,
+ code: NEXT_TS_ERRORS.INVALID_OPTION_VALUE,
messageText:
errorMessage ||
`"${displayedValue}" is not a valid value for the "${name.text}" option.`,
diff --git a/packages/next/server/node-web-streams-helper.ts b/packages/next/server/node-web-streams-helper.ts
index b0d52455c80de..c82ff473824f2 100644
--- a/packages/next/server/node-web-streams-helper.ts
+++ b/packages/next/server/node-web-streams-helper.ts
@@ -173,7 +173,7 @@ function createHeadInsertionTransformStream(
freezing = true
} else {
const content = decodeText(chunk)
- const index = content.indexOf('')
if (index !== -1) {
const insertedHeadContent =
content.slice(0, index) + insertion + content.slice(index)
diff --git a/packages/next/server/web/adapter.ts b/packages/next/server/web/adapter.ts
index 49a8ec2b52887..062dca353ae0b 100644
--- a/packages/next/server/web/adapter.ts
+++ b/packages/next/server/web/adapter.ts
@@ -10,6 +10,11 @@ import { waitUntilSymbol } from './spec-extension/fetch-event'
import { NextURL } from './next-url'
import { stripInternalSearchParams } from '../internal-utils'
import { normalizeRscPath } from '../../shared/lib/router/utils/app-paths'
+import {
+ NEXT_ROUTER_PREFETCH,
+ NEXT_ROUTER_STATE_TREE,
+ RSC,
+} from '../../client/components/app-router-headers'
class NextRequestHint extends NextRequest {
sourcePage: string
@@ -37,9 +42,9 @@ class NextRequestHint extends NextRequest {
}
const FLIGHT_PARAMETERS = [
- '__rsc__',
- '__next_router_state_tree__',
- '__next_router_prefetch__',
+ [RSC],
+ [NEXT_ROUTER_STATE_TREE],
+ [NEXT_ROUTER_PREFETCH],
] as const
export async function adapter(params: {
@@ -71,7 +76,7 @@ export async function adapter(params: {
// Parameters should only be stripped for middleware
if (!isEdgeRendering) {
for (const param of FLIGHT_PARAMETERS) {
- requestHeaders.delete(param)
+ requestHeaders.delete(param.toString().toLowerCase())
}
}
diff --git a/packages/next/tsconfig.json b/packages/next/tsconfig.json
index abbb87416c5f2..0cbb6fd2199e9 100644
--- a/packages/next/tsconfig.json
+++ b/packages/next/tsconfig.json
@@ -14,6 +14,7 @@
"./*.d.ts",
"future/*.d.ts",
"image-types/global.d.ts",
+ "compat/*.d.ts",
"legacy/*.d.ts",
"types/compiled.d.ts"
]
diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json
index 7ef6d1c50124d..f37542dc9d05d 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.0.3-canary.0",
+ "version": "13.0.3-canary.1",
"description": "A development-only overlay for developing React applications.",
"repository": {
"url": "vercel/next.js",
diff --git a/packages/react-dev-overlay/src/client.ts b/packages/react-dev-overlay/src/client.ts
index a9eb0a496eea5..8f774ec957ed1 100644
--- a/packages/react-dev-overlay/src/client.ts
+++ b/packages/react-dev-overlay/src/client.ts
@@ -89,7 +89,18 @@ function onRefresh() {
Bus.emit({ type: Bus.TYPE_REFRESH })
}
+function onBeforeRefresh() {
+ Bus.emit({ type: Bus.TYPE_BEFORE_REFRESH })
+}
+
export { getErrorByType } from './internal/helpers/getErrorByType'
export { getServerError } from './internal/helpers/nodeStackFrames'
export { default as ReactDevOverlay } from './internal/ReactDevOverlay'
-export { onBuildOk, onBuildError, register, unregister, onRefresh }
+export {
+ onBuildOk,
+ onBuildError,
+ register,
+ unregister,
+ onBeforeRefresh,
+ onRefresh,
+}
diff --git a/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx b/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx
index 8db93560df47a..049de17386589 100644
--- a/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx
+++ b/packages/react-dev-overlay/src/internal/ReactDevOverlay.tsx
@@ -9,10 +9,36 @@ import { Base } from './styles/Base'
import { ComponentStyles } from './styles/ComponentStyles'
import { CssReset } from './styles/CssReset'
+type RefreshState =
+ | {
+ // No refresh in progress.
+ type: 'idle'
+ }
+ | {
+ // The refresh process has been triggered, but the new code has not been
+ // executed yet.
+ type: 'pending'
+ errors: SupportedErrorEvent[]
+ }
+
type OverlayState = {
nextId: number
buildError: string | null
errors: SupportedErrorEvent[]
+ refreshState: RefreshState
+}
+
+function pushErrorFilterDuplicates(
+ errors: SupportedErrorEvent[],
+ err: SupportedErrorEvent
+): SupportedErrorEvent[] {
+ return [
+ ...errors.filter((e) => {
+ // Filter out duplicate errors
+ return e.event.reason !== err.event.reason
+ }),
+ err,
+ ]
}
function reducer(state: OverlayState, ev: Bus.BusEvent): OverlayState {
@@ -23,21 +49,56 @@ function reducer(state: OverlayState, ev: Bus.BusEvent): OverlayState {
case Bus.TYPE_BUILD_ERROR: {
return { ...state, buildError: ev.message }
}
+ case Bus.TYPE_BEFORE_REFRESH: {
+ return { ...state, refreshState: { type: 'pending', errors: [] } }
+ }
case Bus.TYPE_REFRESH: {
- return { ...state, buildError: null, errors: [] }
+ return {
+ ...state,
+ buildError: null,
+ errors:
+ // Errors can come in during updates. In this case, UNHANDLED_ERROR
+ // and UNHANDLED_REJECTION events might be dispatched between the
+ // BEFORE_REFRESH and the REFRESH event. We want to keep those errors
+ // around until the next refresh. Otherwise we run into a race
+ // condition where those errors would be cleared on refresh completion
+ // before they can be displayed.
+ state.refreshState.type === 'pending'
+ ? state.refreshState.errors
+ : [],
+ refreshState: { type: 'idle' },
+ }
}
case Bus.TYPE_UNHANDLED_ERROR:
case Bus.TYPE_UNHANDLED_REJECTION: {
- return {
- ...state,
- nextId: state.nextId + 1,
- errors: [
- ...state.errors.filter((err) => {
- // Filter out duplicate errors
- return err.event.reason !== ev.reason
- }),
- { id: state.nextId, event: ev },
- ],
+ switch (state.refreshState.type) {
+ case 'idle': {
+ return {
+ ...state,
+ nextId: state.nextId + 1,
+ errors: pushErrorFilterDuplicates(state.errors, {
+ id: state.nextId,
+ event: ev,
+ }),
+ }
+ }
+ case 'pending': {
+ return {
+ ...state,
+ nextId: state.nextId + 1,
+ refreshState: {
+ ...state.refreshState,
+ errors: pushErrorFilterDuplicates(state.refreshState.errors, {
+ id: state.nextId,
+ event: ev,
+ }),
+ },
+ }
+ }
+ default:
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const _: never = state.refreshState
+ return state
}
}
default: {
@@ -74,6 +135,9 @@ const ReactDevOverlay: React.FunctionComponent =
nextId: 1,
buildError: null,
errors: [],
+ refreshState: {
+ type: 'idle',
+ },
})
React.useEffect(() => {
diff --git a/packages/react-dev-overlay/src/internal/bus.ts b/packages/react-dev-overlay/src/internal/bus.ts
index 1ffc87428b8b2..6c1a3992bc88b 100644
--- a/packages/react-dev-overlay/src/internal/bus.ts
+++ b/packages/react-dev-overlay/src/internal/bus.ts
@@ -3,6 +3,7 @@ import { StackFrame } from 'stacktrace-parser'
export const TYPE_BUILD_OK = 'build-ok'
export const TYPE_BUILD_ERROR = 'build-error'
export const TYPE_REFRESH = 'fast-refresh'
+export const TYPE_BEFORE_REFRESH = 'before-fast-refresh'
export const TYPE_UNHANDLED_ERROR = 'unhandled-error'
export const TYPE_UNHANDLED_REJECTION = 'unhandled-rejection'
@@ -11,6 +12,7 @@ export type BuildError = {
type: typeof TYPE_BUILD_ERROR
message: string
}
+export type BeforeFastRefresh = { type: typeof TYPE_BEFORE_REFRESH }
export type FastRefresh = { type: typeof TYPE_REFRESH }
export type UnhandledError = {
type: typeof TYPE_UNHANDLED_ERROR
@@ -26,6 +28,7 @@ export type BusEvent =
| BuildOk
| BuildError
| FastRefresh
+ | BeforeFastRefresh
| UnhandledError
| UnhandledRejection
diff --git a/packages/react-refresh-utils/internal/helpers.ts b/packages/react-refresh-utils/internal/helpers.ts
index 9ff15e9808c49..66282e3fd84c3 100644
--- a/packages/react-refresh-utils/internal/helpers.ts
+++ b/packages/react-refresh-utils/internal/helpers.ts
@@ -29,17 +29,23 @@
import RefreshRuntime from 'react-refresh/runtime'
+type ModuleHotStatus =
+ | 'idle'
+ | 'check'
+ | 'prepare'
+ | 'ready'
+ | 'dispose'
+ | 'apply'
+ | 'abort'
+ | 'fail'
+
+type ModuleHotStatusHandler = (status: ModuleHotStatus) => void
+
declare const module: {
hot: {
- status: () =>
- | 'idle'
- | 'check'
- | 'prepare'
- | 'ready'
- | 'dispose'
- | 'apply'
- | 'abort'
- | 'fail'
+ status: () => ModuleHotStatus
+ addStatusHandler: (handler: ModuleHotStatusHandler) => void
+ removeStatusHandler: (handler: ModuleHotStatusHandler) => void
}
}
@@ -133,34 +139,46 @@ function shouldInvalidateReactRefreshBoundary(
}
var isUpdateScheduled: boolean = false
+// This function aggregates updates from multiple modules into a single React Refresh call.
function scheduleUpdate() {
if (isUpdateScheduled) {
return
}
+ isUpdateScheduled = true
- function canApplyUpdate() {
- return module.hot.status() === 'idle'
+ function canApplyUpdate(status: ModuleHotStatus) {
+ return status === 'idle'
}
- isUpdateScheduled = true
- setTimeout(function () {
+ function applyUpdate() {
isUpdateScheduled = false
+ try {
+ RefreshRuntime.performReactRefresh()
+ } catch (err) {
+ console.warn(
+ 'Warning: Failed to re-render. We will retry on the next Fast Refresh event.\n' +
+ err
+ )
+ }
+ }
- // Only trigger refresh if the webpack HMR state is idle
- if (canApplyUpdate()) {
- try {
- RefreshRuntime.performReactRefresh()
- } catch (err) {
- console.warn(
- 'Warning: Failed to re-render. We will retry on the next Fast Refresh event.\n' +
- err
- )
- }
- return
+ if (canApplyUpdate(module.hot.status())) {
+ // Apply update on the next tick.
+ Promise.resolve().then(() => {
+ applyUpdate()
+ })
+ return
+ }
+
+ const statusHandler = (status) => {
+ if (canApplyUpdate(status)) {
+ module.hot.removeStatusHandler(statusHandler)
+ applyUpdate()
}
+ }
- return scheduleUpdate()
- }, 30)
+ // Apply update once the HMR runtime's status is idle.
+ module.hot.addStatusHandler(statusHandler)
}
// Needs to be compatible with IE11
diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json
index 4018ff331b870..21f45595ce85b 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.0.3-canary.0",
+ "version": "13.0.3-canary.1",
"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 cb0db7ed5acdc..ec9d1139898fa 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -408,7 +408,7 @@ importers:
packages/eslint-config-next:
specifiers:
- '@next/eslint-plugin-next': 13.0.3-canary.0
+ '@next/eslint-plugin-next': 13.0.3-canary.1
'@rushstack/eslint-patch': ^1.1.3
'@typescript-eslint/parser': ^5.42.0
eslint-import-resolver-node: ^0.3.6
@@ -476,12 +476,12 @@ importers:
'@hapi/accept': 5.0.2
'@napi-rs/cli': 2.12.0
'@napi-rs/triples': 1.1.0
- '@next/env': 13.0.3-canary.0
- '@next/polyfill-module': 13.0.3-canary.0
- '@next/polyfill-nomodule': 13.0.3-canary.0
- '@next/react-dev-overlay': 13.0.3-canary.0
- '@next/react-refresh-utils': 13.0.3-canary.0
- '@next/swc': 13.0.3-canary.0
+ '@next/env': 13.0.3-canary.1
+ '@next/polyfill-module': 13.0.3-canary.1
+ '@next/polyfill-nomodule': 13.0.3-canary.1
+ '@next/react-dev-overlay': 13.0.3-canary.1
+ '@next/react-refresh-utils': 13.0.3-canary.1
+ '@next/swc': 13.0.3-canary.1
'@segment/ajv-human-errors': 2.1.2
'@swc/helpers': 0.4.11
'@taskr/clear': 1.1.0
diff --git a/test/.gitignore b/test/.gitignore
index cf4bab9ddde9f..d6bf3bccf5124 100644
--- a/test/.gitignore
+++ b/test/.gitignore
@@ -1 +1,2 @@
!node_modules
+.vscode
diff --git a/test/e2e/app-dir/app-alias/next.config.js b/test/e2e/app-dir/app-alias/next.config.js
index e5b838697b38b..cfa3ac3d7aa94 100644
--- a/test/e2e/app-dir/app-alias/next.config.js
+++ b/test/e2e/app-dir/app-alias/next.config.js
@@ -1,6 +1,5 @@
module.exports = {
experimental: {
appDir: true,
- transpileModules: ['ui'],
},
}
diff --git a/test/e2e/app-dir/app-alias/src/app/layout.tsx b/test/e2e/app-dir/app-alias/src/app/layout.tsx
index 079c59e3e2581..cbdfcab5036e9 100644
--- a/test/e2e/app-dir/app-alias/src/app/layout.tsx
+++ b/test/e2e/app-dir/app-alias/src/app/layout.tsx
@@ -1,8 +1,10 @@
export default function Root({ children }: { children: React.ReactNode }) {
return (
-
- {children}
+
+ top bar
+ {children}
+
)
}
diff --git a/test/e2e/app-dir/app/middleware.js b/test/e2e/app-dir/app/middleware.js
index 7ef85d902a361..2a53ed94317ab 100644
--- a/test/e2e/app-dir/app/middleware.js
+++ b/test/e2e/app-dir/app/middleware.js
@@ -35,8 +35,8 @@ export function middleware(request) {
? 'rewrite'
: 'redirect'
- const internal = ['__rsc__', '__next_router_state_tree__']
- if (internal.some((name) => request.headers.has(name))) {
+ const internal = ['RSC', 'Next-Router-State-Tree']
+ if (internal.some((name) => request.headers.has(name.toLowerCase()))) {
return NextResponse[method](new URL('/internal/failure', request.url))
}
diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts
index a87c46317ba78..d389c1e2d34cc 100644
--- a/test/e2e/app-dir/index.test.ts
+++ b/test/e2e/app-dir/index.test.ts
@@ -84,7 +84,7 @@ describe('app dir', () => {
{},
{
headers: {
- __rsc__: '1',
+ ['RSC'.toString()]: '1',
},
}
)
@@ -98,7 +98,7 @@ describe('app dir', () => {
{},
{
headers: {
- __rsc__: '1',
+ ['RSC'.toString()]: '1',
},
}
)
diff --git a/test/e2e/app-dir/rsc-basic.test.ts b/test/e2e/app-dir/rsc-basic.test.ts
index 9463fe3afb416..00fd0ef863042 100644
--- a/test/e2e/app-dir/rsc-basic.test.ts
+++ b/test/e2e/app-dir/rsc-basic.test.ts
@@ -88,9 +88,6 @@ describe('app dir - rsc basics', () => {
'__nextLocale',
'__nextDefaultLocale',
'__nextIsNotFound',
- '__rsc__',
- '__next_router_state_tree__',
- '__next_router_prefetch__',
]
const hasNextInternalQuery = inlineFlightContents.some((content) =>
@@ -108,9 +105,9 @@ describe('app dir - rsc basics', () => {
requestsCount++
return request.allHeaders().then((headers) => {
if (
- headers.__rsc__ === '1' &&
- // Prefetches also include `__rsc__`
- headers.__next_router_prefetch__ !== '1'
+ headers['RSC'.toLowerCase()] === '1' &&
+ // Prefetches also include `RSC`
+ headers['Next-Router-Prefetch'.toLowerCase()] !== '1'
) {
hasFlightRequest = true
}
@@ -185,41 +182,18 @@ describe('app dir - rsc basics', () => {
}
})
- it('should refresh correctly with next/link', async () => {
+ 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'
- let hasFlightRequest = false
- const browser = await webdriver(next.url, '/root', {
- beforePageLoad(page) {
- page.on('request', (request) => {
- return request.allHeaders().then((headers) => {
- if (
- headers.__rsc__ === '1' &&
- headers.__next_router_prefetch__ !== '1'
- ) {
- hasFlightRequest = true
- }
- })
- })
- },
- })
+ const browser = await webdriver(next.url, '/root', {})
- // wait for hydration
- await new Promise((res) => setTimeout(res, 1000))
- if (isNextDev) {
- expect(hasFlightRequest).toBe(false)
- }
- await browser.elementByCss(selector).click()
+ await browser.eval('window.didNotReloadPage = true')
+ await browser.elementByCss(selector).click().waitForElementByCss('#query')
- // wait for re-hydration
- if (isNextDev) {
- await check(
- () => (hasFlightRequest ? 'success' : hasFlightRequest),
- 'success'
- )
- }
- const refreshText = await browser.elementByCss(selector).text()
- expect(refreshText).toBe('next link')
+ 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 () => {
@@ -381,7 +355,7 @@ describe('app dir - rsc basics', () => {
{},
{
headers: {
- __rsc__: '1',
+ ['RSC'.toString()]: '1',
},
}
).then(async (response) => {
diff --git a/test/e2e/switchable-runtime/index.test.ts b/test/e2e/switchable-runtime/index.test.ts
index 59504dc20b3b1..07b3a0f58d5f3 100644
--- a/test/e2e/switchable-runtime/index.test.ts
+++ b/test/e2e/switchable-runtime/index.test.ts
@@ -120,7 +120,7 @@ describe('Switchable runtime', () => {
beforePageLoad(page) {
page.on('request', (request) => {
return request.allHeaders().then((headers) => {
- if (headers.__rsc__ === '1') {
+ if (headers['RSC'.toLowerCase()] === '1') {
flightRequest = request.url()
}
})
@@ -680,7 +680,7 @@ describe('Switchable runtime', () => {
beforePageLoad(page) {
page.on('request', (request) => {
request.allHeaders().then((headers) => {
- if (headers.__rsc__ === '1') {
+ if (headers['RSC'.toLowerCase()] === '1') {
flightRequest = request.url()
}
})
diff --git a/test/integration/create-next-app/lib/utils.ts b/test/integration/create-next-app/lib/utils.ts
index ba05cf1052e95..cd0b631ece16c 100644
--- a/test/integration/create-next-app/lib/utils.ts
+++ b/test/integration/create-next-app/lib/utils.ts
@@ -23,7 +23,6 @@ export const createNextApp = (args: string[], options?: SpawnOptions) => {
...options,
env: {
...process.env,
- ...options.env,
// unset CI env as this skips the auto-install behavior
// being tested
CI: '',
@@ -32,6 +31,7 @@ export const createNextApp = (args: string[], options?: SpawnOptions) => {
CONTINUOUS_INTEGRATION: '',
RUN_ID: '',
BUILD_NUMBER: '',
+ ...options.env,
},
})
}
diff --git a/test/integration/create-next-app/templates.test.ts b/test/integration/create-next-app/templates.test.ts
index 553d5d84fda76..eb95af6c1fc81 100644
--- a/test/integration/create-next-app/templates.test.ts
+++ b/test/integration/create-next-app/templates.test.ts
@@ -67,6 +67,24 @@ describe('create-next-app templates', () => {
})
})
+ it('should create TS projects with --ts, --typescript with CI=1', async () => {
+ await useTempDir(async (cwd) => {
+ const projectName = 'typescript-test'
+ const childProcess = createNextApp([projectName, '--ts', '--eslint'], {
+ cwd,
+ env: {
+ ...process.env,
+ CI: '1',
+ GITHUB_ACTIONS: '1',
+ },
+ })
+ const exitCode = await spawnExitPromise(childProcess)
+
+ expect(exitCode).toBe(0)
+ shouldBeTypescriptProject({ cwd, projectName, template: 'default' })
+ })
+ })
+
it('should create JS projects with --js, --javascript', async () => {
await useTempDir(async (cwd) => {
const projectName = 'javascript-test'
diff --git a/test/integration/custom-routes/test/index.test.js b/test/integration/custom-routes/test/index.test.js
index 5384aba34aa85..3217bd5140157 100644
--- a/test/integration/custom-routes/test/index.test.js
+++ b/test/integration/custom-routes/test/index.test.js
@@ -2215,6 +2215,10 @@ const runTests = (isDev = false) => {
routeKeys: {},
},
],
+ rsc: {
+ header: 'RSC',
+ varyHeader: 'RSC, Next-Router-State-Tree, Next-Router-Prefetch',
+ },
})
})
diff --git a/test/integration/dynamic-routing/test/index.test.js b/test/integration/dynamic-routing/test/index.test.js
index 6866e9ba7699c..e2a63c3cb4095 100644
--- a/test/integration/dynamic-routing/test/index.test.js
+++ b/test/integration/dynamic-routing/test/index.test.js
@@ -1426,6 +1426,10 @@ function runTests({ dev }) {
},
},
],
+ rsc: {
+ header: 'RSC',
+ varyHeader: 'RSC, Next-Router-State-Tree, Next-Router-Prefetch',
+ },
})
})
diff --git a/test/integration/typescript/pages/hello.tsx b/test/integration/typescript/pages/hello.tsx
index 1e5c82d05ec5c..acfd061ee4109 100644
--- a/test/integration/typescript/pages/hello.tsx
+++ b/test/integration/typescript/pages/hello.tsx
@@ -31,7 +31,7 @@ class Test2 extends Test {
new Test2().show()
export default function HelloPage(): JSX.Element {
- const router = useRouter(true)
+ const router = useRouter()
console.log(process.browser)
console.log(router.pathname)
console.log(router.isReady)
diff --git a/test/production/pnpm-support/index.test.ts b/test/production/pnpm-support/index.test.ts
index e0328f5001bac..fc0468d4f7234 100644
--- a/test/production/pnpm-support/index.test.ts
+++ b/test/production/pnpm-support/index.test.ts
@@ -43,6 +43,22 @@ describe('pnpm support', () => {
const html = await renderViaHTTP(next.url, '/')
expect(html).toContain('Hello World')
+
+ const manifest = JSON.parse(
+ await next.readFile('.next/next-server.js.nft.json')
+ )
+ for (const ignore of ['next/dist/pages', '.wasm', 'compiled/@ampproject']) {
+ let matchingFile
+ try {
+ matchingFile = manifest.files.some((file) => file.includes(ignore))
+ expect(!!matchingFile).toBe(false)
+ } catch (err) {
+ require('console').error(
+ `Found unexpected file in manifest ${matchingFile} matched ${ignore}`
+ )
+ throw err
+ }
+ }
})
it('should execute client-side JS on each page in output: "standalone"', async () => {
diff --git a/test/production/standalone-mode/required-server-files/required-server-files.test.ts b/test/production/standalone-mode/required-server-files/required-server-files.test.ts
index 0912f96dc3e39..5bef65ea94c87 100644
--- a/test/production/standalone-mode/required-server-files/required-server-files.test.ts
+++ b/test/production/standalone-mode/required-server-files/required-server-files.test.ts
@@ -22,9 +22,15 @@ describe('should set-up next', () => {
let stderr = ''
let requiredFilesManifest
- beforeAll(async () => {
+ const setupNext = async ({
+ nextEnv,
+ minimalMode,
+ }: {
+ nextEnv?: boolean
+ minimalMode?: boolean
+ }) => {
// test build against environment with next support
- process.env.NOW_BUILDER = '1'
+ process.env.NOW_BUILDER = nextEnv ? '1' : ''
next = await createNext({
files: {
@@ -103,7 +109,7 @@ describe('should set-up next', () => {
testServer,
(await fs.readFile(testServer, 'utf8'))
.replace('console.error(err)', `console.error('top-level', err)`)
- .replace('conf:', 'minimalMode: true,conf:')
+ .replace('conf:', `minimalMode: ${minimalMode},conf:`)
)
appPort = await findPort()
server = await initNextServerScript(
@@ -124,6 +130,10 @@ describe('should set-up next', () => {
},
}
)
+ }
+
+ beforeAll(async () => {
+ await setupNext({ nextEnv: true, minimalMode: true })
})
afterAll(async () => {
await next.destroy()
@@ -1243,7 +1253,10 @@ describe('should set-up next', () => {
})
it('should run middleware correctly without minimalMode', async () => {
+ await next.destroy()
await killApp(server)
+ await setupNext({ nextEnv: false, minimalMode: false })
+
const testServer = join(next.testDir, 'standalone/server.js')
await fs.writeFile(
testServer,