diff --git a/Cargo.lock b/Cargo.lock index 6bd7704782998..33c8f8b79d0b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -412,7 +412,7 @@ dependencies = [ [[package]] name = "auto-hash-map" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "serde", ] @@ -3633,7 +3633,7 @@ dependencies = [ [[package]] name = "node-file-trace" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "serde", @@ -7306,7 +7306,7 @@ dependencies = [ [[package]] name = "turbo-tasks" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "async-trait", @@ -7338,7 +7338,7 @@ dependencies = [ [[package]] name = "turbo-tasks-build" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "cargo-lock", @@ -7350,7 +7350,7 @@ dependencies = [ [[package]] name = "turbo-tasks-bytes" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "bytes", @@ -7365,7 +7365,7 @@ dependencies = [ [[package]] name = "turbo-tasks-env" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "dotenvs", @@ -7379,7 +7379,7 @@ dependencies = [ [[package]] name = "turbo-tasks-fetch" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "indexmap 1.9.3", @@ -7396,7 +7396,7 @@ dependencies = [ [[package]] name = "turbo-tasks-fs" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "auto-hash-map", @@ -7426,7 +7426,7 @@ dependencies = [ [[package]] name = "turbo-tasks-hash" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "base16", "hex", @@ -7438,7 +7438,7 @@ dependencies = [ [[package]] name = "turbo-tasks-macros" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -7452,7 +7452,7 @@ dependencies = [ [[package]] name = "turbo-tasks-macros-shared" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "proc-macro2", "quote", @@ -7462,7 +7462,7 @@ dependencies = [ [[package]] name = "turbo-tasks-malloc" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "mimalloc", ] @@ -7470,7 +7470,7 @@ dependencies = [ [[package]] name = "turbo-tasks-memory" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "auto-hash-map", @@ -7493,7 +7493,7 @@ dependencies = [ [[package]] name = "turbo-tasks-testing" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "auto-hash-map", @@ -7506,7 +7506,7 @@ dependencies = [ [[package]] name = "turbopack" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "async-recursion", @@ -7537,7 +7537,7 @@ dependencies = [ [[package]] name = "turbopack-bench" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "chromiumoxide", @@ -7567,7 +7567,7 @@ dependencies = [ [[package]] name = "turbopack-binding" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "auto-hash-map", "mdxjs", @@ -7610,7 +7610,7 @@ dependencies = [ [[package]] name = "turbopack-build" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "indexmap 1.9.3", @@ -7632,7 +7632,7 @@ dependencies = [ [[package]] name = "turbopack-cli-utils" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "clap 4.1.11", @@ -7656,7 +7656,7 @@ dependencies = [ [[package]] name = "turbopack-core" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "async-recursion", @@ -7685,7 +7685,7 @@ dependencies = [ [[package]] name = "turbopack-create-test-app" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "clap 4.1.11", @@ -7698,7 +7698,7 @@ dependencies = [ [[package]] name = "turbopack-css" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "async-trait", @@ -7720,7 +7720,7 @@ dependencies = [ [[package]] name = "turbopack-dev" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "indexmap 1.9.3", @@ -7744,7 +7744,7 @@ dependencies = [ [[package]] name = "turbopack-dev-server" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "async-compression", @@ -7781,7 +7781,7 @@ dependencies = [ [[package]] name = "turbopack-ecmascript" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "async-trait", @@ -7815,7 +7815,7 @@ dependencies = [ [[package]] name = "turbopack-ecmascript-hmr-protocol" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "serde", "serde_json", @@ -7826,7 +7826,7 @@ dependencies = [ [[package]] name = "turbopack-ecmascript-plugins" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "async-trait", @@ -7849,7 +7849,7 @@ dependencies = [ [[package]] name = "turbopack-ecmascript-runtime" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "indoc", @@ -7866,7 +7866,7 @@ dependencies = [ [[package]] name = "turbopack-env" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "indexmap 1.9.3", @@ -7882,7 +7882,7 @@ dependencies = [ [[package]] name = "turbopack-image" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "base64 0.21.0", @@ -7902,7 +7902,7 @@ dependencies = [ [[package]] name = "turbopack-json" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "serde", @@ -7917,7 +7917,7 @@ dependencies = [ [[package]] name = "turbopack-mdx" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "mdxjs", @@ -7932,7 +7932,7 @@ dependencies = [ [[package]] name = "turbopack-node" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "async-stream", @@ -7967,7 +7967,7 @@ dependencies = [ [[package]] name = "turbopack-static" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "serde", @@ -7983,7 +7983,7 @@ dependencies = [ [[package]] name = "turbopack-swc-utils" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "swc_core", "turbo-tasks", @@ -7994,7 +7994,7 @@ dependencies = [ [[package]] name = "turbopack-test-utils" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "once_cell", @@ -8012,7 +8012,7 @@ dependencies = [ [[package]] name = "turbopack-wasm" version = "0.1.0" -source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230825.2#faf918f24fee368fbf32cb99464a202c90068ee1" +source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230829.2#8307b9329d169b3f5174790ca95fb4a4b48573ca" dependencies = [ "anyhow", "indexmap 1.9.3", diff --git a/Cargo.toml b/Cargo.toml index 6f6f951200ef6..30355c7b5fc9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,11 +46,11 @@ swc_core = { version = "0.79.70" } testing = { version = "0.33.24" } # Turbo crates -turbopack-binding = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230825.2" } +turbopack-binding = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230829.2" } # [TODO]: need to refactor embed_directory! macro usages, as well as resolving turbo_tasks::function, macros.. -turbo-tasks = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230825.2" } +turbo-tasks = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230829.2" } # [TODO]: need to refactor embed_directory! macro usage in next-core -turbo-tasks-fs = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230825.2" } +turbo-tasks-fs = { git = "https://github.com/vercel/turbo.git", tag = "turbopack-230829.2" } # General Deps diff --git a/docs/02-app/01-building-your-application/02-data-fetching/03-forms-and-mutations.mdx b/docs/02-app/01-building-your-application/02-data-fetching/03-forms-and-mutations.mdx index 7122a6637b22e..e723edca123fc 100644 --- a/docs/02-app/01-building-your-application/02-data-fetching/03-forms-and-mutations.mdx +++ b/docs/02-app/01-building-your-application/02-data-fetching/03-forms-and-mutations.mdx @@ -170,6 +170,8 @@ export default function Page() { } ``` +> **Good to know**: `
` takes the [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData) data type. In the example above, the FormData submitted via the HTML [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) is accessible in the server action `create`. + ### Revalidating Data Server Actions allow you to invalidate the [Next.js Cache](/docs/app/building-your-application/caching) on demand. You can invalidate an entire route segment with [`revalidatePath`](/docs/app/api-reference/functions/revalidatePath): @@ -375,6 +377,20 @@ function SubmitButton() { } ``` +```jsx filename="app/page.jsx" switcher +'use client' + +import { experimental_useFormStatus as useFormStatus } from 'react-dom' + +function SubmitButton() { + const { pending } = useFormStatus() + + return ( + + ) +} +``` + > **Good to know:** > > - Displaying loading or error states currently requires using Client Components. We are exploring options for server-side functions to retrieve these values as we move forward in stability for Server Actions. @@ -829,7 +845,7 @@ You can read cookies inside a Server Action using the [`cookies`](/docs/app/api- import { cookies } from 'next/headers' -export async function create() { +export async function read() { const auth = cookies().get('authorization')?.value // ... } @@ -840,7 +856,7 @@ export async function create() { import { cookies } from 'next/headers' -export async function create() { +export async function read() { const auth = cookies().get('authorization')?.value // ... } @@ -884,7 +900,7 @@ You can delete cookies inside a Server Action using the [`cookies`](/docs/app/ap import { cookies } from 'next/headers' -export async function create() { +export async function delete() { cookies().delete('name') // ... } @@ -895,7 +911,7 @@ export async function create() { import { cookies } from 'next/headers' -export async function create() { +export async function delete() { cookies().delete('name') // ... } diff --git a/docs/02-app/01-building-your-application/03-rendering/index.mdx b/docs/02-app/01-building-your-application/03-rendering/index.mdx index 6e2fd0ea65eb0..8049b5876748b 100644 --- a/docs/02-app/01-building-your-application/03-rendering/index.mdx +++ b/docs/02-app/01-building-your-application/03-rendering/index.mdx @@ -71,7 +71,7 @@ When working in these environments, it's helpful to think of the flow of the cod {/* Diagram: Response flow */} -If you need to access the server from the client, you send a **new** request to the server rather than re-use the same request. This makes it easier understand where to render your components and where you to place the Network Boundary. +If you need to access the server from the client, you send a **new** request to the server rather than re-use the same request. This makes it easier to understand where to render your components and where to place the Network Boundary. In practice, this model encourages developers to think about what they want to execute on the server first, before sending the result to the client and making the application interactive. diff --git a/packages/next-swc/crates/next-api/src/middleware.rs b/packages/next-swc/crates/next-api/src/middleware.rs index 5c353cdb1ae5e..99e819f060f6d 100644 --- a/packages/next-swc/crates/next-api/src/middleware.rs +++ b/packages/next-swc/crates/next-api/src/middleware.rs @@ -214,6 +214,6 @@ impl Endpoint for MiddlewareEndpoint { #[turbo_tasks::function] fn client_changed(self: Vc) -> Vc { - Completion::new() + Completion::immutable() } } diff --git a/packages/next-swc/crates/next-api/src/project.rs b/packages/next-swc/crates/next-api/src/project.rs index c0ce87cf01317..8608e3546a218 100644 --- a/packages/next-swc/crates/next-api/src/project.rs +++ b/packages/next-swc/crates/next-api/src/project.rs @@ -586,7 +586,7 @@ impl Project { Ok(self .await? .versioned_content_map - .get(self.client_root().join(identifier))) + .get(self.client_relative_path().join(identifier))) } #[turbo_tasks::function] @@ -635,7 +635,7 @@ impl Project { Ok(self .await? .versioned_content_map - .keys_in_path(self.client_root())) + .keys_in_path(self.client_relative_path())) } } diff --git a/packages/next-swc/crates/next-core/js/package.json b/packages/next-swc/crates/next-core/js/package.json index b3a8d1ff67bae..878df0d993d6a 100644 --- a/packages/next-swc/crates/next-core/js/package.json +++ b/packages/next-swc/crates/next-core/js/package.json @@ -10,8 +10,8 @@ "check": "tsc --noEmit" }, "dependencies": { - "@vercel/turbopack-ecmascript-runtime": "https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230825.2", - "@vercel/turbopack-node": "https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-node/js?turbopack-230825.2", + "@vercel/turbopack-ecmascript-runtime": "https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230829.2", + "@vercel/turbopack-node": "https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-node/js?turbopack-230829.2", "anser": "^2.1.1", "css.escape": "^1.5.1", "next": "*", diff --git a/packages/next-swc/crates/next-core/js/src/dev/client.ts b/packages/next-swc/crates/next-core/js/src/dev/client.ts index 1c56f6a9ac70f..db9d09bef7d30 100644 --- a/packages/next-swc/crates/next-core/js/src/dev/client.ts +++ b/packages/next-swc/crates/next-core/js/src/dev/client.ts @@ -1,10 +1,15 @@ import { connect } from '@vercel/turbopack-ecmascript-runtime/dev/client/hmr-client' -import { connectHMR } from '@vercel/turbopack-ecmascript-runtime/dev/client/websocket' +import { + connectHMR, + addMessageListener, + sendMessage, +} from '@vercel/turbopack-ecmascript-runtime/dev/client/websocket' import { register, ReactDevOverlay } from '../overlay/client' export function initializeHMR(options: { assetPrefix: string }) { connect({ - assetPrefix: options.assetPrefix, + addMessageListener, + sendMessage, }) connectHMR({ assetPrefix: options.assetPrefix, diff --git a/packages/next-swc/crates/next-core/js/src/dev/hot-reloader.tsx b/packages/next-swc/crates/next-core/js/src/dev/hot-reloader.tsx index 0b1734bd4c140..77d5a4c74a57c 100644 --- a/packages/next-swc/crates/next-core/js/src/dev/hot-reloader.tsx +++ b/packages/next-swc/crates/next-core/js/src/dev/hot-reloader.tsx @@ -5,6 +5,7 @@ import { useRouter, usePathname } from 'next/dist/client/components/navigation' import { useEffect } from 'react' import { subscribeToUpdate } from '@vercel/turbopack-ecmascript-runtime/dev/client/hmr-client' import { ReactDevOverlay } from './client' +import { sendMessage } from '@vercel/turbopack-ecmascript-runtime/dev/client/websocket' type HotReloadProps = React.PropsWithChildren<{ assetPrefix?: string @@ -22,6 +23,7 @@ export default function HotReload({ children }: HotReloadProps) { rsc: '1', }, }, + sendMessage, (update) => { if (update.type !== 'issues') { router.refresh() diff --git a/packages/next-swc/crates/next-core/js/src/entry/fallback.tsx b/packages/next-swc/crates/next-core/js/src/entry/fallback.tsx index 90309991edfd0..dc5c9e2578757 100644 --- a/packages/next-swc/crates/next-core/js/src/entry/fallback.tsx +++ b/packages/next-swc/crates/next-core/js/src/entry/fallback.tsx @@ -4,6 +4,7 @@ import { createRoot } from 'react-dom/client' import { initializeHMR, ReactDevOverlay } from '../dev/client' import { subscribeToUpdate } from '@vercel/turbopack-ecmascript-runtime/dev/client/hmr-client' +import { sendMessage } from '@vercel/turbopack-ecmascript-runtime/dev/client/websocket' const pageChunkPath = location.pathname.slice(1) @@ -14,6 +15,7 @@ subscribeToUpdate( accept: 'text/html', }, }, + sendMessage, (update) => { if (update.type === 'restart' || update.type === 'notFound') { location.reload() diff --git a/packages/next-swc/crates/next-core/js/src/entry/next-hydrate.tsx b/packages/next-swc/crates/next-core/js/src/entry/next-hydrate.tsx index 94e32fff86d2a..a6cc01a017f89 100644 --- a/packages/next-swc/crates/next-core/js/src/entry/next-hydrate.tsx +++ b/packages/next-swc/crates/next-core/js/src/entry/next-hydrate.tsx @@ -9,6 +9,7 @@ import { import { formatWithValidation } from 'next/dist/shared/lib/router/utils/format-url' import { initializeHMR } from '../dev/client' import { subscribeToUpdate } from '@vercel/turbopack-ecmascript-runtime/dev/client/hmr-client' +import { sendMessage } from '@vercel/turbopack-ecmascript-runtime/dev/client/websocket' ;(self as any).__next_set_public_path__ = () => {} async function loadPageChunk(assetPrefix: string, chunkData: ChunkData) { @@ -83,6 +84,7 @@ function subscribeToPageManifest({ assetPrefix }: { assetPrefix: string }) { { path: '_next/static/development/_devPagesManifest.json', }, + sendMessage, (update) => { if (['restart', 'notFound', 'partial'].includes(update.type)) { return @@ -169,6 +171,7 @@ function subscribeToPageData({ 'x-nextjs-data': '1', }, }, + sendMessage, (update) => { if (update.type !== 'restart') { return diff --git a/packages/next-swc/crates/next-core/src/next_client/context.rs b/packages/next-swc/crates/next-core/src/next_client/context.rs index 488526faf3de0..c09aea8f00793 100644 --- a/packages/next-swc/crates/next-core/src/next_client/context.rs +++ b/packages/next-swc/crates/next-core/src/next_client/context.rs @@ -136,7 +136,7 @@ pub async fn get_client_resolve_options_context( let next_client_import_map = get_next_client_import_map(project_path, ty, next_config, execution_context); let next_client_fallback_import_map = get_next_client_fallback_import_map(ty); - let next_client_resolved_map = get_next_client_resolved_map(project_path, project_path); + let next_client_resolved_map = get_next_client_resolved_map(project_path, project_path, mode); let module_options_context = ResolveOptionsContext { enable_node_modules: Some(project_path.root().resolve().await?), custom_conditions: vec![mode.node_env().to_string()], diff --git a/packages/next-swc/crates/next-core/src/next_import_map.rs b/packages/next-swc/crates/next-core/src/next_import_map.rs index 485e45c23126d..b012b4fc739d4 100644 --- a/packages/next-swc/crates/next-core/src/next_import_map.rs +++ b/packages/next-swc/crates/next-core/src/next_import_map.rs @@ -309,22 +309,28 @@ pub async fn get_next_edge_import_map( pub fn get_next_client_resolved_map( context: Vc, root: Vc, + mode: NextMode, ) -> Vc { - let glob_mappings = vec![ - // Temporary hack to replace the hot reloader until this is passable by props in next.js - ( - context.root(), - Glob::new( - "**/next/dist/client/components/react-dev-overlay/hot-reloader-client.js" - .to_string(), + let glob_mappings = if mode == NextMode::Development { + vec![] + } else { + vec![ + // Temporary hack to replace the hot reloader until this is passable by props in + // next.js + ( + context.root(), + Glob::new( + "**/next/dist/client/components/react-dev-overlay/hot-reloader-client.js" + .to_string(), + ), + ImportMapping::PrimaryAlternative( + "@vercel/turbopack-next/dev/hot-reloader.tsx".to_string(), + Some(root), + ) + .into(), ), - ImportMapping::PrimaryAlternative( - "@vercel/turbopack-next/dev/hot-reloader.tsx".to_string(), - Some(root), - ) - .into(), - ), - ]; + ] + }; ResolvedMap { by_glob: glob_mappings, } diff --git a/packages/next/package.json b/packages/next/package.json index 91f473063319e..e67f088b8448b 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -192,6 +192,7 @@ "@types/ws": "8.2.0", "@vercel/ncc": "0.34.0", "@vercel/nft": "0.22.6", + "@vercel/turbopack-ecmascript-runtime": "https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230829.2", "acorn": "8.5.0", "ajv": "8.11.0", "amphtml-validator": "1.0.35", diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index 28d0cfb5ee826..ce6e8fa154e7d 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -53,7 +53,10 @@ import { isAppRouteRoute } from '../lib/is-app-route-route' import { normalizeMetadataRoute } from '../lib/metadata/get-metadata-route' import { fileExists } from '../lib/file-exists' import { getRouteLoaderEntry } from './webpack/loaders/next-route-loader' -import { isInternalComponent } from '../lib/is-internal-component' +import { + isInternalComponent, + isNonRoutePagesPage, +} from '../lib/is-internal-component' import { isStaticMetadataRouteFile } from '../lib/metadata/is-metadata-route' import { RouteKind } from '../server/future/route-kind' import { encodeToBase64 } from './webpack/loaders/utils' @@ -645,7 +648,8 @@ export async function createEntrypoints( ] } else if ( !isMiddlewareFile(page) && - !isInternalComponent(absolutePagePath) + !isInternalComponent(absolutePagePath) && + !isNonRoutePagesPage(page) ) { server[serverBundlePath] = [ getRouteLoaderEntry({ diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index a9c0545e27558..949bab7c1032d 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -475,7 +475,7 @@ export type TurbopackResult = T & { diagnostics: Diagnostics[] } -interface Middleware { +export interface Middleware { endpoint: Endpoint } @@ -500,7 +500,7 @@ export interface UpdateInfo { tasks: number } -enum ServerClientChangeType { +export enum ServerClientChangeType { Server = 'Server', Client = 'Client', Both = 'Both', @@ -550,7 +550,7 @@ export interface Endpoint { * After changed() has been awaited it will listen to changes. * The async iterator will yield for each change. */ - changed(): Promise> + changed(): Promise>> } interface EndpointConfig { @@ -593,6 +593,8 @@ function bindingToApi(binding: any, _wasm: boolean) { callback: (err: Error, value: T) => void ) => Promise<{ __napiType: 'RootTask' }> + const cancel = new (class Cancel extends Error {})() + /** * Utility function to ensure all variants of an enum are handled. */ @@ -635,6 +637,7 @@ function bindingToApi(binding: any, _wasm: boolean) { reject: (error: Error) => void } | undefined + let canceled = false // The native function will call this every time it emits a new result. We // either need to notify a waiting consumer, or buffer the new result until @@ -652,10 +655,10 @@ function bindingToApi(binding: any, _wasm: boolean) { } } - return (async function* () { + const iterator = (async function* () { const task = await withErrorCause(() => nativeFunction(emitResult)) try { - while (true) { + while (!canceled) { if (buffer.length > 0) { const item = buffer.shift()! if (item.err) throw item.err @@ -667,10 +670,19 @@ function bindingToApi(binding: any, _wasm: boolean) { }) } } + } catch (e) { + if (e === cancel) return + throw e } finally { binding.rootTaskDispose(task) } })() + iterator.return = async () => { + canceled = true + if (waiting) waiting.reject(cancel) + return { value: undefined, done: true } as IteratorReturnResult + } + return iterator } /** @@ -908,6 +920,10 @@ function bindingToApi(binding: any, _wasm: boolean) { ) ) + // The subscriptions will emit always emit once, which is the initial + // computation. This is not a change, so swallow it. + await Promise.all([serverSubscription.next(), clientSubscription.next()]) + return (async function* () { try { while (true) { diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index c50bbfcce00eb..21cf63283dc2d 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -169,33 +169,35 @@ function isResourceInPackages( } export function getDefineEnv({ - dev, + allowedRevalidateHeaderKeys, + clientRouterFilters, config, + dev, distDir, - isClient, + fetchCacheKeyPrefix, hasRewrites, - isNodeServer, + isClient, isEdgeServer, + isNodeOrEdgeCompilation, + isNodeServer, middlewareMatchers, - clientRouterFilters, previewModeId, - fetchCacheKeyPrefix, - allowedRevalidateHeaderKeys, }: { - dev?: boolean - distDir: string - isClient?: boolean - hasRewrites?: boolean - isNodeServer?: boolean - isEdgeServer?: boolean - middlewareMatchers?: MiddlewareMatcher[] - config: NextConfigComplete + allowedRevalidateHeaderKeys: string[] | undefined clientRouterFilters: Parameters< typeof getBaseWebpackConfig >[1]['clientRouterFilters'] - previewModeId?: string - fetchCacheKeyPrefix?: string - allowedRevalidateHeaderKeys?: string[] + config: NextConfigComplete + dev: boolean + distDir: string + fetchCacheKeyPrefix: string | undefined + hasRewrites: boolean + isClient: boolean + isEdgeServer: boolean + isNodeOrEdgeCompilation: boolean + isNodeServer: boolean + middlewareMatchers: MiddlewareMatcher[] | undefined + previewModeId: string | undefined }) { return { // internal field to identify the plugin config @@ -352,7 +354,7 @@ export function getDefineEnv({ config.experimental.webVitalsAttribution ), 'process.env.__NEXT_ASSET_PREFIX': JSON.stringify(config.assetPrefix), - ...(isNodeServer || isEdgeServer + ...(isNodeOrEdgeCompilation ? { // Fix bad-actors in the npm ecosystem (e.g. `node-formidable`) // This is typically found in unmaintained modules from the @@ -781,6 +783,9 @@ export default async function getBaseWebpackConfig( const isEdgeServer = compilerType === COMPILER_NAMES.edgeServer const isNodeServer = compilerType === COMPILER_NAMES.server + // If the current compilation is aimed at server-side code instead of client-side code. + const isNodeOrEdgeCompilation = isNodeServer || isEdgeServer + const hasRewrites = rewrites.beforeFiles.length > 0 || rewrites.afterFiles.length > 0 || @@ -845,7 +850,7 @@ export default async function getBaseWebpackConfig( loader: require.resolve('./babel/loader/index'), options: { configFile: babelConfigFile, - isServer: isNodeServer || isEdgeServer, + isServer: isNodeOrEdgeCompilation, distDir, pagesDir, cwd: dir, @@ -876,7 +881,7 @@ export default async function getBaseWebpackConfig( return { loader: 'next-swc-loader', options: { - isServer: isNodeServer || isEdgeServer, + isServer: isNodeOrEdgeCompilation, rootDir: dir, pagesDir, appDir, @@ -937,10 +942,9 @@ export default async function getBaseWebpackConfig( const pageExtensions = config.pageExtensions - const outputPath = - isNodeServer || isEdgeServer - ? path.join(distDir, SERVER_DIRECTORY) - : distDir + const outputPath = isNodeOrEdgeCompilation + ? path.join(distDir, SERVER_DIRECTORY) + : distDir const reactServerCondition = [ 'react-server', @@ -1886,26 +1890,24 @@ export default async function getBaseWebpackConfig( }/_next/`, path: !dev && isNodeServer ? path.join(outputPath, 'chunks') : outputPath, // On the server we don't use hashes - filename: - isNodeServer || isEdgeServer - ? dev || isEdgeServer - ? `[name].js` - : `../[name].js` - : `static/chunks/${isDevFallback ? 'fallback/' : ''}[name]${ - dev ? '' : appDir ? '-[chunkhash]' : '-[contenthash]' - }.js`, + filename: isNodeOrEdgeCompilation + ? dev || isEdgeServer + ? `[name].js` + : `../[name].js` + : `static/chunks/${isDevFallback ? 'fallback/' : ''}[name]${ + dev ? '' : appDir ? '-[chunkhash]' : '-[contenthash]' + }.js`, library: isClient || isEdgeServer ? '_N_E' : undefined, libraryTarget: isClient || isEdgeServer ? 'assign' : 'commonjs2', hotUpdateChunkFilename: 'static/webpack/[id].[fullhash].hot-update.js', hotUpdateMainFilename: 'static/webpack/[fullhash].[runtime].hot-update.json', // This saves chunks with the name given via `import()` - chunkFilename: - isNodeServer || isEdgeServer - ? '[name].js' - : `static/chunks/${isDevFallback ? 'fallback/' : ''}${ - dev ? '[name]' : '[name].[contenthash]' - }.js`, + chunkFilename: isNodeOrEdgeCompilation + ? '[name].js' + : `static/chunks/${isDevFallback ? 'fallback/' : ''}${ + dev ? '[name]' : '[name].[contenthash]' + }.js`, strictModuleExceptionHandling: true, crossOriginLoading: crossOrigin, webassemblyModuleFilename: 'static/wasm/[modulehash].wasm', @@ -2411,18 +2413,19 @@ export default async function getBaseWebpackConfig( }), new webpack.DefinePlugin( getDefineEnv({ - dev, + allowedRevalidateHeaderKeys, + clientRouterFilters, config, + dev, distDir, - isClient, + fetchCacheKeyPrefix, hasRewrites, - isNodeServer, + isClient, isEdgeServer, + isNodeOrEdgeCompilation, + isNodeServer, middlewareMatchers, - clientRouterFilters, previewModeId, - fetchCacheKeyPrefix, - allowedRevalidateHeaderKeys, }) ), isClient && @@ -2483,7 +2486,7 @@ export default async function getBaseWebpackConfig( resourceRegExp: /react-is/, contextRegExp: /next[\\/]dist[\\/]/, }), - (isNodeServer || isEdgeServer) && + isNodeOrEdgeCompilation && new PagesManifestPlugin({ dev, isEdgeRuntime: isEdgeServer, @@ -2778,11 +2781,9 @@ export default async function getBaseWebpackConfig( process.env.NEXT_WEBPACK_LOGGING.includes('summary-server') const profile = - (profileClient && isClient) || - (profileServer && (isNodeServer || isEdgeServer)) + (profileClient && isClient) || (profileServer && isNodeOrEdgeCompilation) const summary = - (summaryClient && isClient) || - (summaryServer && (isNodeServer || isEdgeServer)) + (summaryClient && isClient) || (summaryServer && isNodeOrEdgeCompilation) const logDefault = !infra && !profile && !summary @@ -2838,7 +2839,7 @@ export default async function getBaseWebpackConfig( : undefined, hasAppDir, isDevelopment: dev, - isServer: isNodeServer || isEdgeServer, + isServer: isNodeOrEdgeCompilation, isEdgeRuntime: isEdgeServer, targetWeb: isClient || isEdgeServer, assetPrefix: config.assetPrefix || '', @@ -2872,13 +2873,13 @@ export default async function getBaseWebpackConfig( webpackConfig = config.webpack(webpackConfig, { dir, dev, - isServer: isNodeServer || isEdgeServer, + isServer: isNodeOrEdgeCompilation, buildId, config, defaultLoaders, totalPages: Object.keys(entrypoints).length, webpack, - ...(isNodeServer || isEdgeServer + ...(isNodeOrEdgeCompilation ? { nextRuntime: isEdgeServer ? 'edge' : 'nodejs', } @@ -3045,7 +3046,7 @@ export default async function getBaseWebpackConfig( if (hasUserCssConfig) { // only show warning for one build - if (isNodeServer || isEdgeServer) { + if (isNodeOrEdgeCompilation) { console.warn( chalk.yellow.bold('Warning: ') + chalk.bold( @@ -3088,7 +3089,7 @@ export default async function getBaseWebpackConfig( // check if using @zeit/next-typescript and show warning if ( - (isNodeServer || isEdgeServer) && + isNodeOrEdgeCompilation && webpackConfig.module && Array.isArray(webpackConfig.module.rules) ) { diff --git a/packages/next/src/client/dev/amp-dev.ts b/packages/next/src/client/dev/amp-dev.ts index 3e39e3e025476..1d77dbe5bc7bd 100644 --- a/packages/next/src/client/dev/amp-dev.ts +++ b/packages/next/src/client/dev/amp-dev.ts @@ -77,14 +77,8 @@ async function tryApplyUpdates() { } } -addMessageListener((event) => { - if (event.data === '\uD83D\uDC93') { - return - } - +addMessageListener((message) => { try { - const message = JSON.parse(event.data) - // actions which are not related to amp-dev if ( message.action === 'serverError' || @@ -102,7 +96,7 @@ addMessageListener((event) => { } } catch (err: any) { console.warn( - '[HMR] Invalid message: ' + event.data + '\n' + (err?.stack ?? '') + '[HMR] Invalid message: ' + message + '\n' + (err?.stack ?? '') ) } }) diff --git a/packages/next/src/client/dev/dev-build-watcher.ts b/packages/next/src/client/dev/dev-build-watcher.ts index b158b536cbda8..f107d65bbf460 100644 --- a/packages/next/src/client/dev/dev-build-watcher.ts +++ b/packages/next/src/client/dev/dev-build-watcher.ts @@ -5,7 +5,7 @@ type VerticalPosition = 'top' | 'bottom' type HorizonalPosition = 'left' | 'right' export default function initializeBuildWatcher( - toggleCallback: (cb: (event: string | { data: string }) => void) => void, + toggleCallback: (cb: (obj: Record) => void) => void, position = 'bottom-right' ) { const shadowHost = document.createElement('div') @@ -53,21 +53,13 @@ export default function initializeBuildWatcher( // Handle events - addMessageListener((event) => { - // This is the heartbeat event - if (event.data === '\uD83D\uDC93') { - return - } - + addMessageListener((obj) => { try { - handleMessage(event) + handleMessage(obj) } catch {} }) - function handleMessage(event: string | { data: string }) { - const obj = - typeof event === 'string' ? { action: event } : JSON.parse(event.data) - + function handleMessage(obj: Record) { // eslint-disable-next-line default-case switch (obj.action) { case 'building': diff --git a/packages/next/src/client/dev/error-overlay/hot-dev-client.ts b/packages/next/src/client/dev/error-overlay/hot-dev-client.ts index f3d545ec924ad..06ff52f8b2674 100644 --- a/packages/next/src/client/dev/error-overlay/hot-dev-client.ts +++ b/packages/next/src/client/dev/error-overlay/hot-dev-client.ts @@ -63,15 +63,14 @@ let customHmrEventHandler: any export default function connect() { register() - addMessageListener((event) => { + addMessageListener((payload) => { try { - const payload = JSON.parse(event.data) - if (!('action' in payload)) return - - processMessage(payload) + if ('action' in payload) { + processMessage(payload) + } } catch (err: any) { console.warn( - '[HMR] Invalid message: ' + event.data + '\n' + (err?.stack ?? '') + '[HMR] Invalid message: ' + payload + '\n' + (err?.stack ?? '') ) } }) diff --git a/packages/next/src/client/dev/error-overlay/websocket.ts b/packages/next/src/client/dev/error-overlay/websocket.ts index c29adf7299241..efad62a99b6b1 100644 --- a/packages/next/src/client/dev/error-overlay/websocket.ts +++ b/packages/next/src/client/dev/error-overlay/websocket.ts @@ -1,5 +1,7 @@ +type WebSocketMessage = Record + let source: WebSocket -const eventCallbacks: ((event: any) => void)[] = [] +const eventCallbacks: ((msg: WebSocketMessage) => void)[] = [] let lastActivity = Date.now() function getSocketProtocol(assetPrefix: string): string { @@ -13,7 +15,7 @@ function getSocketProtocol(assetPrefix: string): string { return protocol === 'http:' ? 'ws' : 'wss' } -export function addMessageListener(cb: (event: any) => void) { +export function addMessageListener(cb: (msg: WebSocketMessage) => void) { eventCallbacks.push(cb) } @@ -39,11 +41,17 @@ export function connectHMR(options: { lastActivity = Date.now() } - function handleMessage(event: any) { + function handleMessage(event: MessageEvent) { lastActivity = Date.now() + // webpack's heartbeat event. + if (event.data === '\uD83D\uDC93') { + return + } + + const msg = JSON.parse(event.data) eventCallbacks.forEach((cb) => { - cb(event) + cb(msg) }) } diff --git a/packages/next/src/client/next-dev-turbopack.ts b/packages/next/src/client/next-dev-turbopack.ts index de54f55331d77..efa5ce70dda21 100644 --- a/packages/next/src/client/next-dev-turbopack.ts +++ b/packages/next/src/client/next-dev-turbopack.ts @@ -1,8 +1,12 @@ // TODO: Remove use of `any` type. -import { initialize, hydrate, version, router, emitter } from './' -import { displayContent } from './dev/fouc' +import { initialize, version, router, emitter } from './' +import initWebpackHMR from './dev/webpack-hot-middleware-client' import './setup-hydration-warning' +import { pageBootrap } from './page-bootstrap' +import { addMessageListener, sendMessage } from './dev/error-overlay/websocket' +//@ts-expect-error requires "moduleResolution": "node16" in tsconfig.json and not .ts extension +import { connect } from '@vercel/turbopack-ecmascript-runtime/dev/client/hmr-client.ts' window.next = { version: `${version}-turbo`, @@ -17,11 +21,10 @@ window.next = { // for the page loader declare let __turbopack_load__: any +const webpackHMR = initWebpackHMR() initialize({ // TODO the prop name is confusing as related to webpack - webpackHMR: { - onUnrecoverableError() {}, - }, + webpackHMR, }) .then(({ assetPrefix }) => { // for the page loader @@ -51,7 +54,19 @@ initialize({ ) } - return hydrate({ beforeRender: displayContent }).then(() => {}) + connect({ + addMessageListener(cb: (msg: Record) => void) { + addMessageListener((msg) => { + // Only call Turbopack's message listener for turbopack messages + if (msg.type?.startsWith('turbopack-')) { + cb(msg) + } + }) + }, + sendMessage, + }) + + return pageBootrap(assetPrefix) }) .catch((err) => { console.error('Error was not caught', err) diff --git a/packages/next/src/client/next-dev.ts b/packages/next/src/client/next-dev.ts index 23335e3452166..275ed7bbf6e9b 100644 --- a/packages/next/src/client/next-dev.ts +++ b/packages/next/src/client/next-dev.ts @@ -1,15 +1,8 @@ // TODO: Remove use of `any` type. import './webpack' -import { initialize, hydrate, version, router, emitter } from './' -import initOnDemandEntries from './dev/on-demand-entries-client' +import { initialize, version, router, emitter } from './' import initWebpackHMR from './dev/webpack-hot-middleware-client' -import initializeBuildWatcher from './dev/dev-build-watcher' -import { displayContent } from './dev/fouc' -import { connectHMR, addMessageListener } from './dev/error-overlay/websocket' -import { - assign, - urlQueryToSearchParams, -} from '../shared/lib/router/utils/querystring' +import { pageBootrap } from './page-bootstrap' import './setup-hydration-warning' @@ -25,84 +18,7 @@ window.next = { const webpackHMR = initWebpackHMR() initialize({ webpackHMR }) .then(({ assetPrefix }) => { - connectHMR({ assetPrefix, path: '/_next/webpack-hmr' }) - - return hydrate({ beforeRender: displayContent }).then(() => { - initOnDemandEntries() - - let buildIndicatorHandler: any = () => {} - - function devPagesHmrListener(event: any) { - let payload - try { - payload = JSON.parse(event.data) - } catch {} - if (payload.event === 'server-error' && payload.errorJSON) { - const { stack, message } = JSON.parse(payload.errorJSON) - const error = new Error(message) - error.stack = stack - throw error - } else if (payload.action === 'reloadPage') { - window.location.reload() - } else if (payload.action === 'devPagesManifestUpdate') { - fetch( - `${assetPrefix}/_next/static/development/_devPagesManifest.json` - ) - .then((res) => res.json()) - .then((manifest) => { - window.__DEV_PAGES_MANIFEST = manifest - }) - .catch((err) => { - console.log(`Failed to fetch devPagesManifest`, err) - }) - } else if (payload.event === 'middlewareChanges') { - return window.location.reload() - } else if (payload.event === 'serverOnlyChanges') { - const { pages } = payload - - // Make sure to reload when the dev-overlay is showing for an - // API route - if (pages.includes(router.query.__NEXT_PAGE)) { - return window.location.reload() - } - - if (!router.clc && pages.includes(router.pathname)) { - console.log('Refreshing page data due to server-side change') - - buildIndicatorHandler('building') - - const clearIndicator = () => buildIndicatorHandler('built') - - router - .replace( - router.pathname + - '?' + - String( - assign( - urlQueryToSearchParams(router.query), - new URLSearchParams(location.search) - ) - ), - router.asPath, - { scroll: false } - ) - .catch(() => { - // trigger hard reload when failing to refresh data - // to show error overlay properly - location.reload() - }) - .finally(clearIndicator) - } - } - } - addMessageListener(devPagesHmrListener) - - if (process.env.__NEXT_BUILD_INDICATOR) { - initializeBuildWatcher((handler: any) => { - buildIndicatorHandler = handler - }, process.env.__NEXT_BUILD_INDICATOR_POSITION) - } - }) + return pageBootrap(assetPrefix) }) .catch((err) => { console.error('Error was not caught', err) diff --git a/packages/next/src/client/page-bootstrap.ts b/packages/next/src/client/page-bootstrap.ts new file mode 100644 index 0000000000000..4163ffa8f4096 --- /dev/null +++ b/packages/next/src/client/page-bootstrap.ts @@ -0,0 +1,85 @@ +import { hydrate, router } from './' +import initOnDemandEntries from './dev/on-demand-entries-client' +import initializeBuildWatcher from './dev/dev-build-watcher' +import { displayContent } from './dev/fouc' +import { connectHMR, addMessageListener } from './dev/error-overlay/websocket' +import { + assign, + urlQueryToSearchParams, +} from '../shared/lib/router/utils/querystring' + +export function pageBootrap(assetPrefix: string) { + connectHMR({ assetPrefix, path: '/_next/webpack-hmr' }) + + return hydrate({ beforeRender: displayContent }).then(() => { + initOnDemandEntries() + + let buildIndicatorHandler: (obj: Record) => void = () => {} + + function devPagesHmrListener(payload: any) { + if (payload.event === 'server-error' && payload.errorJSON) { + const { stack, message } = JSON.parse(payload.errorJSON) + const error = new Error(message) + error.stack = stack + throw error + } else if (payload.action === 'reloadPage') { + window.location.reload() + } else if (payload.action === 'devPagesManifestUpdate') { + fetch(`${assetPrefix}/_next/static/development/_devPagesManifest.json`) + .then((res) => res.json()) + .then((manifest) => { + window.__DEV_PAGES_MANIFEST = manifest + }) + .catch((err) => { + console.log(`Failed to fetch devPagesManifest`, err) + }) + } else if (payload.event === 'middlewareChanges') { + return window.location.reload() + } else if (payload.event === 'serverOnlyChanges') { + const { pages } = payload + + // Make sure to reload when the dev-overlay is showing for an + // API route + if (pages.includes(router.query.__NEXT_PAGE)) { + return window.location.reload() + } + + if (!router.clc && pages.includes(router.pathname)) { + console.log('Refreshing page data due to server-side change') + + buildIndicatorHandler({ action: 'building' }) + + const clearIndicator = () => + buildIndicatorHandler({ action: 'built' }) + + router + .replace( + router.pathname + + '?' + + String( + assign( + urlQueryToSearchParams(router.query), + new URLSearchParams(location.search) + ) + ), + router.asPath, + { scroll: false } + ) + .catch(() => { + // trigger hard reload when failing to refresh data + // to show error overlay properly + location.reload() + }) + .finally(clearIndicator) + } + } + } + addMessageListener(devPagesHmrListener) + + if (process.env.__NEXT_BUILD_INDICATOR) { + initializeBuildWatcher((handler: any) => { + buildIndicatorHandler = handler + }, process.env.__NEXT_BUILD_INDICATOR_POSITION) + } + }) +} diff --git a/packages/next/src/lib/is-internal-component.ts b/packages/next/src/lib/is-internal-component.ts index 23d3e8421f2af..19f6b325097d5 100644 --- a/packages/next/src/lib/is-internal-component.ts +++ b/packages/next/src/lib/is-internal-component.ts @@ -7,3 +7,7 @@ export function isInternalComponent(pathname: string): boolean { return false } } + +export function isNonRoutePagesPage(pathname: string): boolean { + return pathname === '/_app' || pathname === '/_document' +} diff --git a/packages/next/src/server/dev/hot-reloader-webpack.ts b/packages/next/src/server/dev/hot-reloader-webpack.ts index bfec2c3e43ee9..f480c0480d27f 100644 --- a/packages/next/src/server/dev/hot-reloader-webpack.ts +++ b/packages/next/src/server/dev/hot-reloader-webpack.ts @@ -65,7 +65,10 @@ import { RouteMatch } from '../future/route-matches/route-match' import { parseVersionInfo, VersionInfo } from './parse-version-info' import { isAPIRoute } from '../../lib/is-api-route' import { getRouteLoaderEntry } from '../../build/webpack/loaders/next-route-loader' -import { isInternalComponent } from '../../lib/is-internal-component' +import { + isInternalComponent, + isNonRoutePagesPage, +} from '../../lib/is-internal-component' import { RouteKind } from '../future/route-kind' import { NextJsHotReloaderInterface } from './hot-reloader-types' @@ -945,7 +948,8 @@ export default class HotReloader implements NextJsHotReloaderInterface { }) } else if ( !isMiddlewareFile(page) && - !isInternalComponent(relativeRequest) + !isInternalComponent(relativeRequest) && + !isNonRoutePagesPage(page) ) { value = getRouteLoaderEntry({ kind: RouteKind.PAGES, @@ -1222,7 +1226,7 @@ export default class HotReloader implements NextJsHotReloaderInterface { return } - // If _document.js didn't change we don't trigger a reload + // If _document.js didn't change we don't trigger a reload. if (documentChunk.hash === this.serverPrevDocumentHash) { return } @@ -1247,9 +1251,10 @@ export default class HotReloader implements NextJsHotReloaderInterface { this.serverChunkNames = chunkNames } + this.serverPrevDocumentHash = documentChunk.hash || null + // Notify reload to reload the page, as _document.js was changed (different hash) this.send('reloadPage') - this.serverPrevDocumentHash = documentChunk.hash || null } ) diff --git a/packages/next/src/server/lib/router-utils/setup-dev.ts b/packages/next/src/server/lib/router-utils/setup-dev.ts index d6fa63cd6c7c6..396508617d2d5 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev.ts @@ -1,10 +1,13 @@ import type { NextConfigComplete } from '../../config-shared' -import type { +import { Endpoint, Route, TurbopackResult, WrittenEndpoint, + ServerClientChangeType, } from '../../../build/swc' +import type { Socket } from 'net' +import ws from 'next/dist/compiled/ws' import fs from 'fs' import url from 'url' @@ -91,6 +94,8 @@ import type { RenderWorkers } from '../router-server' import { pathToRegexp } from 'next/dist/compiled/path-to-regexp' import { NextJsHotReloaderInterface } from '../../dev/hot-reloader-types' +const wsServer = new ws.Server({ noServer: true }) + type SetupOpts = { renderWorkers: RenderWorkers dir: string @@ -181,6 +186,8 @@ async function startWatcher(opts: SetupOpts) { }) const iter = project.entrypointsSubscribe() const curEntries: Map = new Map() + let changeSubscriptions: Map> = new Map() + let prevMiddleware: boolean | undefined = undefined const globalEntries: { app: Endpoint | undefined document: Endpoint | undefined @@ -283,6 +290,11 @@ async function startWatcher(opts: SetupOpts) { const pagesManifests = new Map() const appPathsManifests = new Map() const middlewareManifests = new Map() + const clientToHmrSubscription = new Map< + ws, + Map> + >() + const clients = new Set() async function loadMiddlewareManifest( pageName: string, @@ -328,6 +340,25 @@ async function startWatcher(opts: SetupOpts) { ) } + async function changeSubscription( + page: string, + endpoint: Endpoint | undefined, + makePayload: ( + page: string, + change: ServerClientChangeType + ) => object | void + ) { + if (!endpoint || changeSubscriptions.has(page)) return + + const changed = await endpoint.changed() + changeSubscriptions.set(page, changed) + + for await (const change of changed) { + const payload = makePayload(page, change.change) + if (payload) hotReloader.send(payload) + } + } + try { async function handleEntries() { for await (const entrypoints of iter) { @@ -358,10 +389,35 @@ async function startWatcher(opts: SetupOpts) { } } - if (entrypoints.middleware) { + for (const [pathname, subscription] of changeSubscriptions) { + if (pathname === '') { + // middleware is handled below + continue + } + + if (!curEntries.has(pathname)) { + subscription.return?.() + changeSubscriptions.delete(pathname) + } + } + + const { middleware } = entrypoints + // We check for explicit true/false, since it's initialized to + // undefined during the first loop (middlewareChanges event is + // unnecessary during the first serve) + if (prevMiddleware === true && !middleware) { + // Went from middleware to no middleware + hotReloader.send({ event: 'middlewareChanges' }) + changeSubscriptions.get('')?.return?.() + changeSubscriptions.delete('') + } else if (prevMiddleware === false && middleware) { + // Went from no middleware to middleware + hotReloader.send({ event: 'middlewareChanges' }) + } + if (middleware) { await processResult( 'middleware', - await entrypoints.middleware.endpoint.writeToDisk() + await middleware.endpoint.writeToDisk() ) await loadMiddlewareManifest('middleware', 'middleware') serverFields.actualMiddlewareFile = 'middleware' @@ -371,10 +427,16 @@ async function startWatcher(opts: SetupOpts) { matchers: middlewareManifests.get('middleware')?.middleware['/'].matchers, } + + changeSubscription('', middleware.endpoint, () => { + return { event: 'middlewareChanges' } + }) + prevMiddleware = true } else { middlewareManifests.delete('middleware') serverFields.actualMiddlewareFile = undefined serverFields.middleware = undefined + prevMiddleware = false } await propagateToWorkers( 'actualMiddlewareFile', @@ -588,6 +650,27 @@ async function startWatcher(opts: SetupOpts) { ) } + async function subscribeToHmrEvents(id: string, client: ws) { + let mapping = clientToHmrSubscription.get(client) + if (mapping === undefined) { + mapping = new Map() + clientToHmrSubscription.set(client, mapping) + } + if (mapping.has(id)) return + + const subscription = project.hmrEvents(id) + mapping.set(id, subscription) + for await (const data of subscription) { + hotReloader.send({ type: 'turbopack-message', data }) + } + } + + function unsubscribeToHmrEvents(id: string, client: ws) { + const mapping = clientToHmrSubscription.get(client) + const subscription = mapping?.get(id) + subscription?.return!() + } + // Write empty manifests await mkdir(path.join(distDir, 'server'), { recursive: true }) await mkdir(path.join(distDir, 'static/development'), { recursive: true }) @@ -635,6 +718,80 @@ async function startWatcher(opts: SetupOpts) { return { finished: undefined } }, + onHMR(req: IncomingMessage, socket: Socket, head: Buffer) { + wsServer.handleUpgrade(req, socket, head, (client) => { + clients.add(client) + client.on('close', () => clients.delete(client)) + + // server sends: + // - Middleware HMR: + // - { action: 'building' } + // - { action: 'sync', hash, errors, warnings, versionInfo } + // - { action: 'built', hash } + + client.addEventListener('message', ({ data }) => { + const parsedData = JSON.parse( + typeof data !== 'string' ? data.toString() : data + ) + + // Next.js messages + switch (parsedData.event) { + case 'ping': { + // const result = parsedData.appDirRoute + // ? handleAppDirPing(parsedData.tree) + // : handlePing(parsedData.page) + const result = { success: true } + hotReloader.send({ + ...result, + [parsedData.appDirRoute ? 'action' : 'event']: 'pong', + }) + break + } + + case 'client-error': // { errorCount, clientId } + case 'client-warning': // { warningCount, clientId } + case 'client-success': // { clientId } + case 'server-component-reload-page': // { clientId } + case 'client-reload-page': // { clientId } + case 'client-full-reload': // { stackTrace, hadRuntimeError } + // TODO + break + + default: + // Might be a Turbopack message... + if (!parsedData.type) { + throw new Error(`unrecognized HMR message "${data}"`) + } + } + + // Turbopack messages + switch (parsedData.type) { + case 'turbopack-subscribe': + subscribeToHmrEvents(parsedData.path, client) + break + + case 'turbopack-unsubscribe': + unsubscribeToHmrEvents(parsedData.path, client) + break + + default: + throw new Error(`unrecognized Turbopack HMR message "${data}"`) + } + }) + + client.send(JSON.stringify({ type: 'turbopack-connected' })) + }) + }, + + send(action: string | object, ...data: any[]) { + const payload = JSON.stringify( + typeof action === 'string' ? { action, data } : action + ) + for (const client of clients) { + client.send(payload) + } + }, + setHmrServerError(_error) { // Not implemented yet. }, @@ -647,15 +804,9 @@ async function startWatcher(opts: SetupOpts) { async stop() { // Not implemented yet. }, - send(_action, ..._args) { - // Not implemented yet. - }, async getCompilationErrors(_page) { return [] }, - onHMR(_req, _socket, _head) { - // Not implemented yet. - }, invalidate(/* Unused parameter: { reloadAfterInvalidation } */) { // Not implemented yet. }, @@ -681,6 +832,9 @@ async function startWatcher(opts: SetupOpts) { '_document', await globalEntries.document?.writeToDisk() ) + changeSubscription('_document', globalEntries?.document, () => { + return { action: 'reloadPage' } + }) await loadPagesManifest('_document') await processResult(page, await globalEntries.error?.writeToDisk()) @@ -723,12 +877,23 @@ async function startWatcher(opts: SetupOpts) { '_document', await globalEntries.document?.writeToDisk() ) + changeSubscription('_document', globalEntries?.document, () => { + return { action: 'reloadPage' } + }) await loadPagesManifest('_document') const writtenEndpoint = await processResult( page, await route.htmlEndpoint.writeToDisk() ) + changeSubscription(page, route.htmlEndpoint, (pageName, change) => { + switch (change) { + case ServerClientChangeType.Server: + case ServerClientChangeType.Both: + return { event: 'serverOnlyChanges', pages: [pageName] } + default: + } + }) const type = writtenEndpoint?.type @@ -774,6 +939,14 @@ async function startWatcher(opts: SetupOpts) { } case 'app-page': { await processResult(page, await route.htmlEndpoint.writeToDisk()) + changeSubscription(page, route.htmlEndpoint, (_page, change) => { + switch (change) { + case ServerClientChangeType.Server: + case ServerClientChangeType.Both: + return { action: 'serverComponentChanges' } + default: + } + }) await loadAppBuildManifest(page) await loadBuildManifest(page, 'app') @@ -1249,14 +1422,19 @@ async function startWatcher(opts: SetupOpts) { plugin.definitions.__NEXT_DEFINE_ENV ) { const newDefine = getDefineEnv({ - dev: true, + allowedRevalidateHeaderKeys: undefined, + clientRouterFilters, config: nextConfig, + dev: true, distDir, - isClient, + fetchCacheKeyPrefix: undefined, hasRewrites, - isNodeServer, + isClient, isEdgeServer, - clientRouterFilters, + isNodeOrEdgeCompilation: isNodeServer || isEdgeServer, + isNodeServer, + middlewareMatchers: undefined, + previewModeId: undefined, }) Object.keys(plugin.definitions).forEach((key) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 039ae336ca234..999f835590554 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1057,6 +1057,9 @@ importers: '@vercel/nft': specifier: 0.22.6 version: 0.22.6 + '@vercel/turbopack-ecmascript-runtime': + specifier: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230829.2 + version: '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230829.2(react-refresh@0.12.0)(webpack@5.86.0)' acorn: specifier: 8.5.0 version: 8.5.0 @@ -1521,11 +1524,11 @@ importers: packages/next-swc/crates/next-core/js: dependencies: '@vercel/turbopack-ecmascript-runtime': - specifier: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230825.2 - version: '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230825.2(react-refresh@0.12.0)(webpack@5.86.0)' + specifier: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230829.2 + version: '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230829.2(react-refresh@0.12.0)(webpack@5.86.0)' '@vercel/turbopack-node': - specifier: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-node/js?turbopack-230825.2 - version: '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-node/js?turbopack-230825.2' + specifier: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-node/js?turbopack-230829.2 + version: '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-node/js?turbopack-230829.2' anser: specifier: ^2.1.1 version: 2.1.1 @@ -6867,7 +6870,6 @@ packages: dependencies: react-refresh: 0.12.0 webpack: 5.86.0(@swc/core@1.3.55) - dev: false /@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3: resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==} @@ -27185,9 +27187,9 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230825.2(react-refresh@0.12.0)(webpack@5.86.0)': - resolution: {tarball: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230825.2} - id: '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230825.2' + '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230829.2(react-refresh@0.12.0)(webpack@5.86.0)': + resolution: {tarball: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230829.2} + id: '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230829.2' name: '@vercel/turbopack-ecmascript-runtime' version: 0.0.0 dependencies: @@ -27196,10 +27198,9 @@ packages: transitivePeerDependencies: - react-refresh - webpack - dev: false - '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-node/js?turbopack-230825.2': - resolution: {tarball: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-node/js?turbopack-230825.2} + '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-node/js?turbopack-230829.2': + resolution: {tarball: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-node/js?turbopack-230829.2} name: '@vercel/turbopack-node' version: 0.0.0 dependencies: diff --git a/test/development/pages-dir/custom-app-hmr/index.test.ts b/test/development/pages-dir/custom-app-hmr/index.test.ts new file mode 100644 index 0000000000000..67107828daa83 --- /dev/null +++ b/test/development/pages-dir/custom-app-hmr/index.test.ts @@ -0,0 +1,46 @@ +import { createNextDescribe } from 'e2e-utils' +import { check } from 'next-test-utils' + +createNextDescribe( + 'custom-app-hmr', + { + files: __dirname, + }, + ({ next }) => { + it('should not do full reload when simply editing _app.js', async () => { + const customAppFilePath = 'pages/_app.js' + const browser = await next.browser('/') + await browser.eval('window.hmrConstantValue = "should-not-change"') + + const customAppContent = await next.readFile(customAppFilePath) + const newCustomAppContent = customAppContent.replace( + 'hmr text origin', + 'hmr text changed' + ) + await next.patchFile(customAppFilePath, newCustomAppContent) + + await check(async () => { + const pText = await browser.elementByCss('h1').text() + expect(pText).toBe('hmr text changed') + + // Should keep the value on window, which indicates there's no full reload + const hmrConstantValue = await browser.eval('window.hmrConstantValue') + expect(hmrConstantValue).toBe('should-not-change') + + return 'success' + }, 'success') + + await next.patchFile(customAppFilePath, customAppContent) + await check(async () => { + const pText = await browser.elementByCss('h1').text() + expect(pText).toBe('hmr text origin') + + // Should keep the value on window, which indicates there's no full reload + const hmrConstantValue = await browser.eval('window.hmrConstantValue') + expect(hmrConstantValue).toBe('should-not-change') + + return 'success' + }, 'success') + }) + } +) diff --git a/test/development/pages-dir/custom-app-hmr/pages/_app.js b/test/development/pages-dir/custom-app-hmr/pages/_app.js new file mode 100644 index 0000000000000..d10eb5b009dd5 --- /dev/null +++ b/test/development/pages-dir/custom-app-hmr/pages/_app.js @@ -0,0 +1,8 @@ +export default function App({ Component, pageProps }) { + return ( + <> +

hmr text origin

+ + + ) +} diff --git a/test/development/pages-dir/custom-app-hmr/pages/index.js b/test/development/pages-dir/custom-app-hmr/pages/index.js new file mode 100644 index 0000000000000..08263e34c35fd --- /dev/null +++ b/test/development/pages-dir/custom-app-hmr/pages/index.js @@ -0,0 +1,3 @@ +export default function Page() { + return

index page

+}