diff --git a/docs/02-app/02-api-reference/04-functions/redirect.mdx b/docs/02-app/02-api-reference/04-functions/redirect.mdx index eb3a1337a4247..92649be1bb035 100644 --- a/docs/02-app/02-api-reference/04-functions/redirect.mdx +++ b/docs/02-app/02-api-reference/04-functions/redirect.mdx @@ -5,7 +5,7 @@ description: API Reference for the redirect function. The `redirect` function allows you to redirect the user to another URL. `redirect` can be used in Server Components, Client Components, [Route Handlers](/docs/app/building-your-application/routing/route-handlers), and [Server Actions](/docs/app/building-your-application/data-fetching/forms-and-mutations). -When used in a streaming context, this will insert a meta tag to emit the redirect on the client side. Otherwise, it will serve a 307 HTTP redirect response to the caller. +When used in a [streaming context](/docs/app/building-your-application/routing/loading-ui-and-streaming#what-is-streaming), this will insert a meta tag to emit the redirect on the client side. Otherwise, it will serve a 307 HTTP redirect response to the caller. If a resource doesn't exist, you can use the [`notFound` function](/docs/app/api-reference/functions/not-found) instead. diff --git a/examples/api-routes-apollo-server/package.json b/examples/api-routes-apollo-server/package.json index e0dbb2f961eea..62d77ba20476b 100644 --- a/examples/api-routes-apollo-server/package.json +++ b/examples/api-routes-apollo-server/package.json @@ -9,7 +9,7 @@ "@apollo/server": "^4.1.1", "@as-integrations/next": "^1.1.0", "@graphql-tools/schema": "^9.0.9", - "graphql": "16.6.0", + "graphql": "16.8.1", "graphql-tag": "^2.12.6", "next": "latest", "react": "^18.2.0", diff --git a/examples/cms-contentful/package.json b/examples/cms-contentful/package.json index ff94b0010ea40..6f8cbac311b23 100644 --- a/examples/cms-contentful/package.json +++ b/examples/cms-contentful/package.json @@ -17,7 +17,7 @@ "contentful-import": "^9.0.4", "date-fns": "2.30.0", "next": "latest", - "postcss": "8.4.28", + "postcss": "8.4.31", "react": "^18.2.0", "react-dom": "^18.2.0", "tailwindcss": "^3.3.3", diff --git a/examples/cms-cosmic/package.json b/examples/cms-cosmic/package.json index f9f16d092100a..e4a7adee9cb9f 100644 --- a/examples/cms-cosmic/package.json +++ b/examples/cms-cosmic/package.json @@ -21,7 +21,7 @@ "@types/node": "^18.0.0", "@types/react": "^18.0.14", "autoprefixer": "10.4.7", - "postcss": "8.4.14", + "postcss": "8.4.31", "tailwindcss": "^3.1.4", "typescript": "^4.7.4" } diff --git a/examples/cms-datocms/package.json b/examples/cms-datocms/package.json index 824026ab1843e..72b156cf78f58 100644 --- a/examples/cms-datocms/package.json +++ b/examples/cms-datocms/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-drupal/package.json b/examples/cms-drupal/package.json index c93fc4ed075fa..e06b113ce89ff 100644 --- a/examples/cms-drupal/package.json +++ b/examples/cms-drupal/package.json @@ -15,7 +15,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-enterspeed/package.json b/examples/cms-enterspeed/package.json index ec83c566b3089..6bd95a5c90666 100644 --- a/examples/cms-enterspeed/package.json +++ b/examples/cms-enterspeed/package.json @@ -16,7 +16,7 @@ "@types/node": "18.0.0", "@types/react": "18.0.14", "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15", "typescript": "4.7.4" } diff --git a/examples/cms-ghost/package.json b/examples/cms-ghost/package.json index 1693030273811..ba4ee4cf1fefc 100644 --- a/examples/cms-ghost/package.json +++ b/examples/cms-ghost/package.json @@ -16,7 +16,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-graphcms/package.json b/examples/cms-graphcms/package.json index d8f489e42f184..0712af518d4d5 100644 --- a/examples/cms-graphcms/package.json +++ b/examples/cms-graphcms/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-kontent-ai/package.json b/examples/cms-kontent-ai/package.json index 1e967c6517d2f..ee16f680781a4 100644 --- a/examples/cms-kontent-ai/package.json +++ b/examples/cms-kontent-ai/package.json @@ -22,7 +22,7 @@ }, "devDependencies": { "autoprefixer": "10.4.7", - "postcss": "8.4.14", + "postcss": "8.4.31", "tailwindcss": "^3.0.15", "tslib": "2.4.0" } diff --git a/examples/cms-prepr/package.json b/examples/cms-prepr/package.json index ac66c6022e78e..8af08ce227afd 100644 --- a/examples/cms-prepr/package.json +++ b/examples/cms-prepr/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-prismic/package.json b/examples/cms-prismic/package.json index ecbe22f538405..db200ccdb7440 100644 --- a/examples/cms-prismic/package.json +++ b/examples/cms-prismic/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "autoprefixer": "10.4.10", - "postcss": "8.4.16", + "postcss": "8.4.31", "slice-machine-ui": "^0.4.2", "tailwindcss": "^3.1.8", "typescript": "^4.8.3" diff --git a/examples/cms-storyblok/package.json b/examples/cms-storyblok/package.json index 99b21b04ac7a0..42c49db957a99 100644 --- a/examples/cms-storyblok/package.json +++ b/examples/cms-storyblok/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-takeshape/package.json b/examples/cms-takeshape/package.json index 83e224c3549bc..61ea4c3c96155 100644 --- a/examples/cms-takeshape/package.json +++ b/examples/cms-takeshape/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-umbraco-heartcore/package.json b/examples/cms-umbraco-heartcore/package.json old mode 100755 new mode 100644 index d8f489e42f184..0712af518d4d5 --- a/examples/cms-umbraco-heartcore/package.json +++ b/examples/cms-umbraco-heartcore/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-wordpress/package.json b/examples/cms-wordpress/package.json index 4ca98678fb7e1..6f194082af0ff 100644 --- a/examples/cms-wordpress/package.json +++ b/examples/cms-wordpress/package.json @@ -19,7 +19,7 @@ }, "devDependencies": { "autoprefixer": "10.4.7", - "postcss": "8.4.14", + "postcss": "8.4.31", "tailwindcss": "^3.0.24" } } diff --git a/examples/radix-ui/package.json b/examples/radix-ui/package.json index 4daa626b78aa3..2fe64f2f71407 100644 --- a/examples/radix-ui/package.json +++ b/examples/radix-ui/package.json @@ -16,7 +16,7 @@ "@types/node": "18.8.0", "@types/react": "18.0.21", "autoprefixer": "10.4.12", - "postcss": "8.4.17", + "postcss": "8.4.31", "tailwindcss": "3.1.8", "typescript": "4.8.4" } diff --git a/lerna.json b/lerna.json index 8e4d9dffb488c..b4ff2eda07dcf 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "13.5.5-canary.0" + "version": "13.5.5-canary.2" } diff --git a/lint-staged.config.js b/lint-staged.config.js index b44eaea2a0a3f..8c1b2df519b67 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -27,6 +27,12 @@ module.exports = { `git add ${escapedFileNames}`, ] }, + '**/*.rs': (filenames) => { + const escapedFileNames = filenames + .map((filename) => (isWin ? filename : escape([filename]))) + .join(' ') + return [`cargo fmt -- ${escapedFileNames}`, `git add ${escapedFileNames}`] + }, } function escape(str) { diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 648867990efd4..1ca01794a8ee1 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.5.5-canary.0", + "version": "13.5.5-canary.2", "keywords": [ "react", "next", diff --git a/packages/create-next-app/templates/app-tw/js/app/layout.js b/packages/create-next-app/templates/app-tw/js/app/layout.js index c93f80617d1d1..821f712283060 100644 --- a/packages/create-next-app/templates/app-tw/js/app/layout.js +++ b/packages/create-next-app/templates/app-tw/js/app/layout.js @@ -1,5 +1,5 @@ -import './globals.css' import { Inter } from 'next/font/google' +import './globals.css' const inter = Inter({ subsets: ['latin'] }) diff --git a/packages/create-next-app/templates/app-tw/ts/app/layout.tsx b/packages/create-next-app/templates/app-tw/ts/app/layout.tsx index ae8456212360d..40e027fbefc15 100644 --- a/packages/create-next-app/templates/app-tw/ts/app/layout.tsx +++ b/packages/create-next-app/templates/app-tw/ts/app/layout.tsx @@ -1,6 +1,6 @@ -import './globals.css' import type { Metadata } from 'next' import { Inter } from 'next/font/google' +import './globals.css' const inter = Inter({ subsets: ['latin'] }) diff --git a/packages/create-next-app/templates/app/js/app/layout.js b/packages/create-next-app/templates/app/js/app/layout.js index c93f80617d1d1..821f712283060 100644 --- a/packages/create-next-app/templates/app/js/app/layout.js +++ b/packages/create-next-app/templates/app/js/app/layout.js @@ -1,5 +1,5 @@ -import './globals.css' import { Inter } from 'next/font/google' +import './globals.css' const inter = Inter({ subsets: ['latin'] }) diff --git a/packages/create-next-app/templates/app/ts/app/layout.tsx b/packages/create-next-app/templates/app/ts/app/layout.tsx index ae8456212360d..40e027fbefc15 100644 --- a/packages/create-next-app/templates/app/ts/app/layout.tsx +++ b/packages/create-next-app/templates/app/ts/app/layout.tsx @@ -1,6 +1,6 @@ -import './globals.css' import type { Metadata } from 'next' import { Inter } from 'next/font/google' +import './globals.css' const inter = Inter({ subsets: ['latin'] }) diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 8d40eb0b77bf3..ab5cafb81e28c 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.5.5-canary.0", + "version": "13.5.5-canary.2", "description": "ESLint configuration used by Next.js.", "main": "index.js", "license": "MIT", @@ -10,7 +10,7 @@ }, "homepage": "https://nextjs.org/docs/app/building-your-application/configuring/eslint#eslint-config", "dependencies": { - "@next/eslint-plugin-next": "13.5.5-canary.0", + "@next/eslint-plugin-next": "13.5.5-canary.2", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/parser": "^5.4.2 || ^6.0.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 dfce4c360f7e5..d37cf3822c4f9 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.5.5-canary.0", + "version": "13.5.5-canary.2", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "license": "MIT", diff --git a/packages/font/package.json b/packages/font/package.json index 72eb78af4d5bc..e8dac1caf89d8 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "13.5.5-canary.0", + "version": "13.5.5-canary.2", "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 b1b091ec9438f..44ac0d40b40ac 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.5.5-canary.0", + "version": "13.5.5-canary.2", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 1e1ada1dce8ea..81b150a3b928c 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "13.5.5-canary.0", + "version": "13.5.5-canary.2", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index bc3499315e7fc..e3c5a99ea5487 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "13.5.5-canary.0", + "version": "13.5.5-canary.2", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 71e9385cab0fa..102a8ed4d33ef 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "13.5.5-canary.0", + "version": "13.5.5-canary.2", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 3e4d9e2208e02..cd3dca272564e 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.5.5-canary.0", + "version": "13.5.5-canary.2", "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 967e20e5fa678..d498743e0287b 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.5.5-canary.0", + "version": "13.5.5-canary.2", "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 93451691fcedc..c715e5a210921 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.5.5-canary.0", + "version": "13.5.5-canary.2", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", 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 63cf8cf828d4a..fc2a29ec84456 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 @@ -452,7 +452,7 @@ async fn insert_next_server_special_aliases( match runtime { NextRuntime::Edge => "next/dist/compiled/react/jsx-runtime", NextRuntime::NodeJs => { - "next/dist/server/future/route-modules/app-page/vendored/shared/\ + "next/dist/server/future/route-modules/app-page/vendored/ssr/\ react-jsx-runtime" } }, @@ -465,7 +465,7 @@ async fn insert_next_server_special_aliases( match runtime { NextRuntime::Edge => "next/dist/compiled/react/jsx-dev-runtime", NextRuntime::NodeJs => { - "next/dist/server/future/route-modules/app-page/vendored/shared/\ + "next/dist/server/future/route-modules/app-page/vendored/ssr/\ react-jsx-dev-runtime" } }, @@ -579,7 +579,7 @@ async fn insert_next_server_special_aliases( match runtime { NextRuntime::Edge => "next/dist/compiled/react/jsx-runtime", NextRuntime::NodeJs => { - "next/dist/server/future/route-modules/app-page/vendored/shared/\ + "next/dist/server/future/route-modules/app-page/vendored/rsc/\ react-jsx-runtime" } }, @@ -592,7 +592,7 @@ async fn insert_next_server_special_aliases( match runtime { NextRuntime::Edge => "next/dist/compiled/react/jsx-dev-runtime", NextRuntime::NodeJs => { - "next/dist/server/future/route-modules/app-page/vendored/shared/\ + "next/dist/server/future/route-modules/app-page/vendored/rsc/\ react-jsx-dev-runtime" } }, diff --git a/packages/next-swc/crates/next-core/src/next_server/context.rs b/packages/next-swc/crates/next-core/src/next_server/context.rs index a67ecdb111d2e..ac8dd43c0997a 100644 --- a/packages/next-swc/crates/next-core/src/next_server/context.rs +++ b/packages/next-swc/crates/next-core/src/next_server/context.rs @@ -109,6 +109,7 @@ pub async fn get_server_resolve_options_context( let unsupported_modules_resolve_plugin = UnsupportedModulesResolvePlugin::new(project_path); let server_component_externals_plugin = ExternalCjsModulesResolvePlugin::new( project_path, + project_path.root(), ExternalPredicate::Only(next_config.server_component_externals()).cell(), ); let ty = ty.into_value(); @@ -125,6 +126,7 @@ pub async fn get_server_resolve_options_context( }; let external_cjs_modules_plugin = ExternalCjsModulesResolvePlugin::new( project_path, + project_path.root(), ExternalPredicate::AllExcept(next_config.transpile_packages()).cell(), ); diff --git a/packages/next-swc/crates/next-core/src/next_server/resolve.rs b/packages/next-swc/crates/next-core/src/next_server/resolve.rs index 558ba777840a6..5a720139d2c56 100644 --- a/packages/next-swc/crates/next-core/src/next_server/resolve.rs +++ b/packages/next-swc/crates/next-core/src/next_server/resolve.rs @@ -1,6 +1,4 @@ use anyhow::Result; -use once_cell::sync::Lazy; -use regex::Regex; use turbo_tasks::Vc; use turbopack_binding::{ turbo::tasks_fs::{glob::Glob, FileJsonContent, FileSystemPath}, @@ -33,6 +31,7 @@ pub enum ExternalPredicate { /// possible to resolve them at runtime. #[turbo_tasks::value] pub(crate) struct ExternalCjsModulesResolvePlugin { + project_path: Vc, root: Vc, predicate: Vc, } @@ -40,8 +39,17 @@ pub(crate) struct ExternalCjsModulesResolvePlugin { #[turbo_tasks::value_impl] impl ExternalCjsModulesResolvePlugin { #[turbo_tasks::function] - pub fn new(root: Vc, predicate: Vc) -> Vc { - ExternalCjsModulesResolvePlugin { root, predicate }.cell() + pub fn new( + project_path: Vc, + root: Vc, + predicate: Vc, + ) -> Vc { + ExternalCjsModulesResolvePlugin { + project_path, + root, + predicate, + } + .cell() } } @@ -66,11 +74,9 @@ async fn is_node_resolveable( Ok(Vc::cell(true)) } -static PNPM: Lazy = Lazy::new(|| Regex::new(r"(?:/|^)node_modules/(.pnpm/.+)").unwrap()); - #[turbo_tasks::function] fn condition(root: Vc) -> Vc { - ResolvePluginCondition::new(root.root(), Glob::new("**/node_modules/**".to_string())) + ResolvePluginCondition::new(root, Glob::new("**/node_modules/**".to_string())) } #[turbo_tasks::value_impl] @@ -101,14 +107,20 @@ impl ResolvePlugin for ExternalCjsModulesResolvePlugin { ExternalPredicate::AllExcept(exceptions) => { let exception_glob = packages_glob(*exceptions).await?; - if exception_glob.execute(&raw_fs_path.path) { - return Ok(ResolveResultOption::none()); + if let Some(exception_glob) = *exception_glob { + if exception_glob.await?.execute(&raw_fs_path.path) { + return Ok(ResolveResultOption::none()); + } } } ExternalPredicate::Only(externals) => { let external_glob = packages_glob(*externals).await?; - if !external_glob.execute(&raw_fs_path.path) { + if let Some(external_glob) = *external_glob { + if !external_glob.await?.execute(&raw_fs_path.path) { + return Ok(ResolveResultOption::none()); + } + } else { return Ok(ResolveResultOption::none()); } } @@ -141,42 +153,30 @@ impl ResolvePlugin for ExternalCjsModulesResolvePlugin { // check if we can resolve the package from the project dir with node.js resolve // options (might be hidden by pnpm) - if *is_node_resolveable(self.root.root(), request, fs_path).await? { + if *is_node_resolveable(self.project_path, request, fs_path).await? { // mark as external return Ok(ResolveResultOption::some( ResolveResult::primary(ResolveResultItem::OriginalReferenceExternal).cell(), )); } - // Special behavior for pnpm as we could reference all .pnpm modules by - // referencing the `.pnpm` folder as module, e. g. - // /node_modules/.pnpm/some-package@2.29.2/node_modules/some-package/dir/file.js - // becomes - // .pnpm/some-package@2.29.2/node_modules/some-package/dir/file.js - if let Some(captures) = PNPM.captures(&fs_path.await?.path) { - if let Some(import_path) = captures.get(1) { - // we could load it directly as external, but we want to make sure node.js would - // resolve it the same way e. g. that we didn't follow any special resolve - // options, to come here like the `module` field in package.json - if *is_node_resolveable(context, request, fs_path).await? { - // mark as external - return Ok(ResolveResultOption::some( - ResolveResult::primary(ResolveResultItem::OriginalReferenceTypeExternal( - import_path.as_str().to_string(), - )) - .cell(), - )); - } - } - } Ok(ResolveResultOption::none()) } } +// TODO move that to turbo +#[turbo_tasks::value(transparent)] +pub struct OptionGlob(Option>); + #[turbo_tasks::function] -async fn packages_glob(packages: Vc>) -> Result> { - Ok(Glob::new(format!( - "**/node_modules/{{{}}}/**", - packages.await?.join(",") +async fn packages_glob(packages: Vc>) -> Result> { + let packages = packages.await?; + if packages.is_empty() { + return Ok(Vc::cell(None)); + } + Ok(Vc::cell(Some( + Glob::new(format!("**/node_modules/{{{}}}/**", packages.join(","))) + .resolve() + .await?, ))) } diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 7ab625e716510..eb13e7a3d6d43 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "13.5.5-canary.0", + "version": "13.5.5-canary.2", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", diff --git a/packages/next/package.json b/packages/next/package.json index a3e3b9912d703..842f76584ca4f 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "13.5.5-canary.0", + "version": "13.5.5-canary.2", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -90,7 +90,7 @@ ] }, "dependencies": { - "@next/env": "13.5.5-canary.0", + "@next/env": "13.5.5-canary.2", "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", @@ -144,11 +144,11 @@ "@mswjs/interceptors": "0.23.0", "@napi-rs/cli": "2.16.2", "@napi-rs/triples": "1.1.0", - "@next/polyfill-module": "13.5.5-canary.0", - "@next/polyfill-nomodule": "13.5.5-canary.0", - "@next/react-dev-overlay": "13.5.5-canary.0", - "@next/react-refresh-utils": "13.5.5-canary.0", - "@next/swc": "13.5.5-canary.0", + "@next/polyfill-module": "13.5.5-canary.2", + "@next/polyfill-nomodule": "13.5.5-canary.2", + "@next/react-dev-overlay": "13.5.5-canary.2", + "@next/react-refresh-utils": "13.5.5-canary.2", + "@next/swc": "13.5.5-canary.2", "@opentelemetry/api": "1.4.1", "@playwright/test": "^1.35.1", "@segment/ajv-human-errors": "2.1.2", diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index 301f7843d0481..551f2a5e960b0 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -356,6 +356,7 @@ export function getEdgeServerEntry(opts: { layer: WEBPACK_LAYERS.reactServerComponents, } } + if (isMiddlewareFile(opts.page)) { const loaderParams: MiddlewareLoaderOptions = { absolutePagePath: opts.absolutePagePath, diff --git a/packages/next/src/build/load-entrypoint.ts b/packages/next/src/build/load-entrypoint.ts index 880899b693680..ec97ac1527264 100644 --- a/packages/next/src/build/load-entrypoint.ts +++ b/packages/next/src/build/load-entrypoint.ts @@ -19,12 +19,18 @@ const TEMPLATES_ESM_FOLDER = path.normalize( * handle replacement values that are related to imports. * * @param entrypoint the entrypoint to load - * @param replacements the replacements to perform + * @param replacements string replacements to perform + * @param injections code injections to perform * @returns the loaded file with the replacements */ export async function loadEntrypoint( - entrypoint: 'pages' | 'pages-api' | 'app-page' | 'app-route', + entrypoint: + | 'pages' + | 'pages-api' + | 'app-page' + | 'app-route' + | 'edge-app-route', replacements: Record<`VAR_${string}`, string>, injections?: Record ): Promise { diff --git a/packages/next/src/build/templates/edge-app-route.ts b/packages/next/src/build/templates/edge-app-route.ts new file mode 100644 index 0000000000000..69b12643ecfad --- /dev/null +++ b/packages/next/src/build/templates/edge-app-route.ts @@ -0,0 +1,9 @@ +import { EdgeRouteModuleWrapper } from '../../server/web/edge-route-module-wrapper' + +// Import the userland code. +// @ts-expect-error - replaced by webpack/turbopack loader +import * as module from 'VAR_USERLAND' + +export const ComponentMod = module + +export default EdgeRouteModuleWrapper.wrap(module.routeModule) diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index bd2976b75d88a..b12532aad7b14 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -21,6 +21,7 @@ import '../server/require-hook' import '../server/node-polyfill-fetch' import '../server/node-polyfill-crypto' import '../server/node-environment' +import '../lib/polyfill-promise-with-resolvers' import { green, yellow, red, cyan, bold, underline } from '../lib/picocolors' import getGzipSize from 'next/dist/compiled/gzip-size' diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 2c417158370ab..de5d19591b091 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -176,16 +176,16 @@ function createRSCAliases( if (!opts.isEdgeServer) { if (opts.layer === WEBPACK_LAYERS.serverSideRendering) { alias = Object.assign(alias, { - 'react/jsx-runtime$': `next/dist/server/future/route-modules/app-page/vendored/shared/react-jsx-runtime`, - 'react/jsx-dev-runtime$': `next/dist/server/future/route-modules/app-page/vendored/shared/react-jsx-dev-runtime`, + 'react/jsx-runtime$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-jsx-runtime`, + 'react/jsx-dev-runtime$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-jsx-dev-runtime`, react$: `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react`, 'react-dom$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-dom`, 'react-server-dom-webpack/client.edge$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-server-dom-webpack-client-edge`, }) } else if (opts.layer === WEBPACK_LAYERS.reactServerComponents) { alias = Object.assign(alias, { - 'react/jsx-runtime$': `next/dist/server/future/route-modules/app-page/vendored/shared/react-jsx-runtime`, - 'react/jsx-dev-runtime$': `next/dist/server/future/route-modules/app-page/vendored/shared/react-jsx-dev-runtime`, + 'react/jsx-runtime$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-jsx-runtime`, + 'react/jsx-dev-runtime$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-jsx-dev-runtime`, react$: `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react`, 'react-dom$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-dom`, 'react-server-dom-webpack/server.edge$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-server-dom-webpack-server-edge`, diff --git a/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts b/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts index 5d2ae74df14b0..e14db1b3b21b1 100644 --- a/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts @@ -4,6 +4,7 @@ import { NextConfig } from '../../../../server/config-shared' import { webpack } from 'next/dist/compiled/webpack/webpack' import { WEBPACK_RESOURCE_QUERIES } from '../../../../lib/constants' import { MiddlewareConfig } from '../../../analysis/get-page-static-info' +import { loadEntrypoint } from '../../../load-entrypoint' export type EdgeAppRouteLoaderQuery = { absolutePagePath: string @@ -15,7 +16,7 @@ export type EdgeAppRouteLoaderQuery = { } const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction = - function (this) { + async function (this) { const { page, absolutePagePath, @@ -52,13 +53,9 @@ const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction = } const nextFontManifest = maybeJSONParse(self.__NEXT_FONT_MANIFEST) - globalThis.__next_private_global_wait_until__ = [] - const render = getRender({ pagesType, dev: ${dev}, @@ -257,22 +255,12 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction = export const ComponentMod = pageMod - export default async function(opts, event) { - const res = await adapter({ + export default function nHandler (opts, event) { + return adapter({ ...opts, IncrementalCache, handler: render }) - - if (event && event.waitUntil) { - event.waitUntil( - Promise.all( - [res?.waitUntil, ...globalThis.__next_private_global_wait_until__] - ) - ) - } - - return res }` return transformed diff --git a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts index eacd767be52e9..d4c4aa11138b0 100644 --- a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts +++ b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts @@ -5,6 +5,7 @@ import type { BuildManifest } from '../../../../server/get-page-files' import type { ReactLoadableManifest } from '../../../../server/load-components' import type { ClientReferenceManifest } from '../../plugins/flight-manifest-plugin' import type { NextFontManifest } from '../../plugins/next-font-manifest-plugin' +import type { NextFetchEvent } from '../../../../server/web/spec-extension/fetch-event' import WebServer from '../../../../server/web-server' import { @@ -16,6 +17,15 @@ import { PrerenderManifest } from '../../..' import { normalizeAppPath } from '../../../../shared/lib/router/utils/app-paths' import { SizeLimit } from '../../../../../types' +const NEXT_PRIVATE_GLOBAL_WAIT_UNTIL = Symbol.for( + '__next_private_global_wait_until__' +) + +// @ts-ignore +globalThis[NEXT_PRIVATE_GLOBAL_WAIT_UNTIL] = + // @ts-ignore + globalThis[NEXT_PRIVATE_GLOBAL_WAIT_UNTIL] || [] + export function getRender({ dev, page, @@ -143,13 +153,20 @@ export function getRender({ const handler = server.getRequestHandler() - return async function render(request: Request) { + return async function render(request: Request, event: NextFetchEvent) { const extendedReq = new WebNextRequest(request) const extendedRes = new WebNextResponse() handler(extendedReq, extendedRes) const result = await extendedRes.toResponse() + if (event && event.waitUntil) { + event.waitUntil( + // @ts-ignore + Promise.all([...globalThis[NEXT_PRIVATE_GLOBAL_WAIT_UNTIL]]) + ) + } + // fetchMetrics is attached to the web request that going through the server, // wait for the handler result is ready and attach it back to the original request. ;(request as any).fetchMetrics = extendedReq.fetchMetrics diff --git a/packages/next/src/build/webpack/loaders/next-middleware-loader.ts b/packages/next/src/build/webpack/loaders/next-middleware-loader.ts index 39c185774be6a..36a1d6ece07e4 100644 --- a/packages/next/src/build/webpack/loaders/next-middleware-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-middleware-loader.ts @@ -67,7 +67,7 @@ export default function middlewareLoader(this: any) { throw new Error('The Middleware "pages${page}" must export a \`middleware\` or a \`default\` function'); } - export default function (opts) { + export default function nHandler(opts) { return adapter({ ...opts, page: ${JSON.stringify(page)}, diff --git a/packages/next/src/export/helpers/create-incremental-cache.ts b/packages/next/src/export/helpers/create-incremental-cache.ts index 233a68f680317..35a3ae75b345d 100644 --- a/packages/next/src/export/helpers/create-incremental-cache.ts +++ b/packages/next/src/export/helpers/create-incremental-cache.ts @@ -35,8 +35,8 @@ export function createIncrementalCache( notFoundRoutes: [], }), fs: { - readFile: (f) => fs.promises.readFile(f), - readFileSync: (f) => fs.readFileSync(f), + readFile: fs.promises.readFile, + readFileSync: fs.readFileSync, writeFile: (f, d) => fs.promises.writeFile(f, d), mkdir: (dir) => fs.promises.mkdir(dir, { recursive: true }), stat: (f) => fs.promises.stat(f), diff --git a/packages/next/src/export/worker.ts b/packages/next/src/export/worker.ts index de3d1af82b84a..a3ee08796c1fd 100644 --- a/packages/next/src/export/worker.ts +++ b/packages/next/src/export/worker.ts @@ -11,6 +11,7 @@ import type { import '../server/node-polyfill-fetch' import '../server/node-polyfill-web-streams' import '../server/node-environment' +import '../lib/polyfill-promise-with-resolvers' process.env.NEXT_IS_EXPORT_WORKER = 'true' diff --git a/packages/next/src/lib/batcher.test.ts b/packages/next/src/lib/batcher.test.ts new file mode 100644 index 0000000000000..ef9224129f6d4 --- /dev/null +++ b/packages/next/src/lib/batcher.test.ts @@ -0,0 +1,78 @@ +import { Batcher } from './batcher' + +describe('Batcher', () => { + describe('batch', () => { + it('should execute the work function immediately', async () => { + const batcher = Batcher.create() + const workFn = jest.fn().mockResolvedValue(42) + + const result = await batcher.batch('key', workFn) + + expect(result).toBe(42) + expect(workFn).toHaveBeenCalledTimes(1) + }) + + it('should batch multiple calls to the same key', async () => { + const batcher = Batcher.create() + const workFn = jest.fn().mockResolvedValue(42) + + const result1 = batcher.batch('key', workFn) + const result2 = batcher.batch('key', workFn) + + expect(result1).toBeInstanceOf(Promise) + expect(result2).toBeInstanceOf(Promise) + expect(workFn).toHaveBeenCalledTimes(1) + + const [value1, value2] = await Promise.all([result1, result2]) + + expect(value1).toBe(42) + expect(value2).toBe(42) + expect(workFn).toHaveBeenCalledTimes(1) + }) + + it('should not batch calls to different keys', async () => { + const batcher = Batcher.create() + const workFn = jest.fn((key) => key) + + const result1 = batcher.batch('key1', workFn) + const result2 = batcher.batch('key2', workFn) + + expect(result1).toBeInstanceOf(Promise) + expect(result2).toBeInstanceOf(Promise) + expect(workFn).toHaveBeenCalledTimes(2) + + const [value1, value2] = await Promise.all([result1, result2]) + + expect(value1).toBe('key1') + expect(value2).toBe('key2') + expect(workFn).toHaveBeenCalledTimes(2) + }) + + it('should use the cacheKeyFn to generate cache keys', async () => { + const cacheKeyFn = jest.fn().mockResolvedValue('cache-key') + const batcher = Batcher.create({ cacheKeyFn }) + const workFn = jest.fn().mockResolvedValue(42) + + const result = await batcher.batch('key', workFn) + + expect(result).toBe(42) + expect(cacheKeyFn).toHaveBeenCalledWith('key') + expect(workFn).toHaveBeenCalledTimes(1) + }) + + it('should use the schedulerFn to schedule work', async () => { + const schedulerFn = jest.fn().mockImplementation((fn) => fn()) + const batcher = Batcher.create({ schedulerFn }) + const workFn = jest.fn().mockResolvedValue(42) + + const results = await Promise.all([ + batcher.batch('key', workFn), + batcher.batch('key', workFn), + batcher.batch('key', workFn), + ]) + + expect(results).toEqual([42, 42, 42]) + expect(workFn).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/packages/next/src/lib/batcher.ts b/packages/next/src/lib/batcher.ts new file mode 100644 index 0000000000000..480b70df15f00 --- /dev/null +++ b/packages/next/src/lib/batcher.ts @@ -0,0 +1,94 @@ +// This takes advantage of `Promise.withResolvers` which is polyfilled in +// this imported module. +import './polyfill-promise-with-resolvers' + +import { SchedulerFn } from '../server/lib/schedule-on-next-tick' + +type CacheKeyFn = ( + key: K +) => PromiseLike | C + +type BatcherOptions = { + cacheKeyFn?: CacheKeyFn + schedulerFn?: SchedulerFn +} + +type WorkFn = ( + key: C, + resolve: (value: V | PromiseLike) => void +) => Promise + +/** + * A wrapper for a function that will only allow one call to the function to + * execute at a time. + */ +export class Batcher { + private readonly pending = new Map>() + + protected constructor( + private readonly cacheKeyFn?: CacheKeyFn, + /** + * A function that will be called to schedule the wrapped function to be + * executed. This defaults to a function that will execute the function + * immediately. + */ + private readonly schedulerFn: SchedulerFn = (fn) => fn() + ) {} + + /** + * Creates a new instance of PendingWrapper. If the key extends a string or + * number, the key will be used as the cache key. If the key is an object, a + * cache key function must be provided. + */ + public static create( + options?: BatcherOptions + ): Batcher + public static create( + options: BatcherOptions & + Required, 'cacheKeyFn'>> + ): Batcher + public static create( + options?: BatcherOptions + ): Batcher { + return new Batcher(options?.cacheKeyFn, options?.schedulerFn) + } + + /** + * Wraps a function in a promise that will be resolved or rejected only once + * for a given key. This will allow multiple calls to the function to be + * made, but only one will be executed at a time. The result of the first + * call will be returned to all callers. + * + * @param key the key to use for the cache + * @param fn the function to wrap + * @returns a promise that resolves to the result of the function + */ + public async batch(key: K, fn: WorkFn): Promise { + const cacheKey = (this.cacheKeyFn ? await this.cacheKeyFn(key) : key) as C + if (cacheKey === null) { + return fn(cacheKey, Promise.resolve) + } + + const pending = this.pending.get(cacheKey) + if (pending) return pending + + const { promise, resolve, reject } = Promise.withResolvers() + this.pending.set(cacheKey, promise) + + this.schedulerFn(async () => { + try { + const result = await fn(cacheKey, resolve) + + // Resolving a promise multiple times is a no-op, so we can safely + // resolve all pending promises with the same result. + resolve(result) + } catch (err) { + reject(err) + } finally { + this.pending.delete(cacheKey) + } + }) + + return promise + } +} diff --git a/packages/next/src/lib/picocolors.ts b/packages/next/src/lib/picocolors.ts index 67a9ac98e9bce..d78de403455d6 100644 --- a/packages/next/src/lib/picocolors.ts +++ b/packages/next/src/lib/picocolors.ts @@ -65,6 +65,9 @@ export const green = enabled ? formatter('\x1b[32m', '\x1b[39m') : String export const yellow = enabled ? formatter('\x1b[33m', '\x1b[39m') : String export const blue = enabled ? formatter('\x1b[34m', '\x1b[39m') : String export const magenta = enabled ? formatter('\x1b[35m', '\x1b[39m') : String +export const purple = enabled + ? formatter('\x1b[38;2;173;127;168m', '\x1b[39m') + : String export const cyan = enabled ? formatter('\x1b[36m', '\x1b[39m') : String export const white = enabled ? formatter('\x1b[37m', '\x1b[39m') : String export const gray = enabled ? formatter('\x1b[90m', '\x1b[39m') : String diff --git a/packages/next/src/lib/polyfill-promise-with-resolvers.ts b/packages/next/src/lib/polyfill-promise-with-resolvers.ts new file mode 100644 index 0000000000000..80144878eef2b --- /dev/null +++ b/packages/next/src/lib/polyfill-promise-with-resolvers.ts @@ -0,0 +1,27 @@ +// This adds a `Promise.withResolvers` polyfill. This will soon be adopted into +// the spec. +// +// TODO: remove this polyfill when it is adopted into the spec. +// +// https://tc39.es/proposal-promise-with-resolvers/ +// +if ( + !('withResolvers' in Promise) || + typeof Promise.withResolvers !== 'function' +) { + Promise.withResolvers = () => { + let resolvers: { + resolve: (value: T | PromiseLike) => void + reject: (reason: any) => void + } + + // Create the promise and assign the resolvers to the object. + const promise = new Promise((resolve, reject) => { + resolvers = { resolve, reject } + }) + + // We know that resolvers is defined because the Promise constructor runs + // synchronously. + return { promise, resolve: resolvers!.resolve, reject: resolvers!.reject } + } +} diff --git a/packages/next/src/lib/worker.ts b/packages/next/src/lib/worker.ts index bd0ebd2e06381..05f1bf4ccf2cd 100644 --- a/packages/next/src/lib/worker.ts +++ b/packages/next/src/lib/worker.ts @@ -3,7 +3,7 @@ import { Worker as JestWorker } from 'next/dist/compiled/jest-worker' import { getNodeOptionsWithoutInspect } from '../server/lib/utils' // We need this as we're using `Promise.withResolvers` which is not available in the node typings -import '../server/node-environment' +import '../lib/polyfill-promise-with-resolvers' type FarmOptions = ConstructorParameters[1] diff --git a/packages/next/src/server/dev/on-demand-entry-handler.ts b/packages/next/src/server/dev/on-demand-entry-handler.ts index b6cfa1e556b06..c5ef3a5802cef 100644 --- a/packages/next/src/server/dev/on-demand-entry-handler.ts +++ b/packages/next/src/server/dev/on-demand-entry-handler.ts @@ -39,6 +39,7 @@ import HotReloader from './hot-reloader-webpack' import { isAppPageRouteDefinition } from '../future/route-definitions/app-page-route-definition' import { scheduleOnNextTick } from '../lib/schedule-on-next-tick' import { RouteDefinition } from '../future/route-definitions/route-definition' +import { Batcher } from '../../lib/batcher' const debug = origDebug('next:on-demand-entry-handler') @@ -878,8 +879,34 @@ export function onDemandEntryHandler({ } } + type EnsurePageOptions = { + page: string + clientOnly: boolean + appPaths?: ReadonlyArray | null + match?: RouteMatch + isApp?: boolean + } + // Make sure that we won't have multiple invalidations ongoing concurrently. - const curEnsurePage = new Map>() + const batcher = Batcher.create< + Omit & { + definition?: RouteDefinition + }, + void, + string + >({ + // The cache key here is composed of the elements that affect the + // compilation, namely, the page, whether it's client only, and whether + // it's an app page. This ensures that we don't have multiple compilations + // for the same page happening concurrently. + // + // We don't include the whole match because it contains match specific + // parameters (like route params) that would just bust this cache. Any + // details that would possibly bust the cache should be listed here. + cacheKeyFn: (options) => JSON.stringify(options), + // Schedule the invocation of the ensurePageImpl function on the next tick. + schedulerFn: scheduleOnNextTick, + }) return { async ensurePage({ @@ -888,13 +915,7 @@ export function onDemandEntryHandler({ appPaths = null, match, isApp, - }: { - page: string - clientOnly: boolean - appPaths?: ReadonlyArray | null - match?: RouteMatch - isApp?: boolean - }) { + }: EnsurePageOptions) { // If the route is actually an app page route, then we should have access // to the app route match, and therefore, the appPaths from it. if ( @@ -905,43 +926,15 @@ export function onDemandEntryHandler({ appPaths = match.definition.appPaths } - // The cache key here is composed of the elements that affect the - // compilation, namely, the page, whether it's client only, and whether - // it's an app page. This ensures that we don't have multiple compilations + // Wrap the invocation of the ensurePageImpl function in the pending + // wrapper, which will ensure that we don't have multiple compilations // for the same page happening concurrently. - // - // We don't include the whole match because it contains match specific - // parameters (like route params) that would just bust this cache. Any - // details that would possibly bust the cache should be listed here. - const key = JSON.stringify({ - page, - clientOnly, - appPaths, - definition: match?.definition, - isApp, - }) - - // See if we're already building this page. - const pending = curEnsurePage.get(key) - if (pending) return pending - - const { promise, resolve, reject } = Promise.withResolvers() - curEnsurePage.set(key, promise) - - // Schedule the build to occur on the next tick, but don't wait and - // instead return the promise immediately. - scheduleOnNextTick(async () => { - try { + return batcher.batch( + { page, clientOnly, appPaths, definition: match?.definition, isApp }, + async () => { await ensurePageImpl({ page, clientOnly, appPaths, match, isApp }) - resolve() - } catch (err) { - reject(err) - } finally { - curEnsurePage.delete(key) } - }) - - return promise + ) }, onHMR(client: ws, getHmrServerError: () => Error | null) { let bufferedHmrServerError: Error | null = null diff --git a/packages/next/src/server/dev/static-paths-worker.ts b/packages/next/src/server/dev/static-paths-worker.ts index cd7d5305fa306..d357a170fefbf 100644 --- a/packages/next/src/server/dev/static-paths-worker.ts +++ b/packages/next/src/server/dev/static-paths-worker.ts @@ -3,6 +3,7 @@ import type { NextConfigComplete } from '../config-shared' import '../require-hook' import '../node-polyfill-fetch' import '../node-environment' +import '../../lib/polyfill-promise-with-resolvers' import { buildAppStaticPaths, diff --git a/packages/next/src/server/future/route-modules/app-page/module.ts b/packages/next/src/server/future/route-modules/app-page/module.ts index 5cc33c1a60376..5a34be6759ad4 100644 --- a/packages/next/src/server/future/route-modules/app-page/module.ts +++ b/packages/next/src/server/future/route-modules/app-page/module.ts @@ -15,13 +15,11 @@ import * as vendoredContexts from './vendored/contexts/entrypoints' let vendoredReactRSC let vendoredReactSSR -let vendoredReactShared // the vendored Reacts are loaded from their original source in the edge runtime if (process.env.NEXT_RUNTIME !== 'edge') { vendoredReactRSC = require('./vendored/rsc/entrypoints') vendoredReactSSR = require('./vendored/ssr/entrypoints') - vendoredReactShared = require('./vendored/shared/entrypoints') } type AppPageUserlandModule = { @@ -64,7 +62,6 @@ export class AppPageRouteModule extends RouteModule< const vendored = { 'react-rsc': vendoredReactRSC, 'react-ssr': vendoredReactSSR, - 'react-shared': vendoredReactShared, contexts: vendoredContexts, } diff --git a/packages/next/src/server/future/route-modules/app-page/vendored/rsc/entrypoints.ts b/packages/next/src/server/future/route-modules/app-page/vendored/rsc/entrypoints.ts index e693d48484574..5b7d91e3601b2 100644 --- a/packages/next/src/server/future/route-modules/app-page/vendored/rsc/entrypoints.ts +++ b/packages/next/src/server/future/route-modules/app-page/vendored/rsc/entrypoints.ts @@ -1,6 +1,7 @@ import * as React from 'react' - import * as ReactDOM from 'react-dom/server-rendering-stub' +import * as ReactJsxDevRuntime from 'react/jsx-dev-runtime' +import * as ReactJsxRuntime from 'react/jsx-runtime' function getAltProxyForBindingsDEV( type: 'Turbopack' | 'Webpack', @@ -68,6 +69,8 @@ if (process.env.TURBOPACK) { export { React, + ReactJsxDevRuntime, + ReactJsxRuntime, ReactDOM, ReactServerDOMWebpackServerEdge, ReactServerDOMTurbopackServerEdge, diff --git a/packages/next/src/server/future/route-modules/app-page/vendored/shared/react-jsx-dev-runtime.ts b/packages/next/src/server/future/route-modules/app-page/vendored/rsc/react-jsx-dev-runtime.ts similarity index 82% rename from packages/next/src/server/future/route-modules/app-page/vendored/shared/react-jsx-dev-runtime.ts rename to packages/next/src/server/future/route-modules/app-page/vendored/rsc/react-jsx-dev-runtime.ts index 9623bb4a90ae0..324b0f5a2faf8 100644 --- a/packages/next/src/server/future/route-modules/app-page/vendored/shared/react-jsx-dev-runtime.ts +++ b/packages/next/src/server/future/route-modules/app-page/vendored/rsc/react-jsx-dev-runtime.ts @@ -1,3 +1,3 @@ module.exports = require('../../module.compiled').vendored[ - 'react-shared' + 'react-rsc' ].ReactJsxDevRuntime diff --git a/packages/next/src/server/future/route-modules/app-page/vendored/shared/react-jsx-runtime.ts b/packages/next/src/server/future/route-modules/app-page/vendored/rsc/react-jsx-runtime.ts similarity index 82% rename from packages/next/src/server/future/route-modules/app-page/vendored/shared/react-jsx-runtime.ts rename to packages/next/src/server/future/route-modules/app-page/vendored/rsc/react-jsx-runtime.ts index b7d24f304f96b..7335eb8d8c1e1 100644 --- a/packages/next/src/server/future/route-modules/app-page/vendored/shared/react-jsx-runtime.ts +++ b/packages/next/src/server/future/route-modules/app-page/vendored/rsc/react-jsx-runtime.ts @@ -1,3 +1,3 @@ module.exports = require('../../module.compiled').vendored[ - 'react-shared' + 'react-rsc' ].ReactJsxRuntime diff --git a/packages/next/src/server/future/route-modules/app-page/vendored/shared/entrypoints.ts b/packages/next/src/server/future/route-modules/app-page/vendored/shared/entrypoints.ts deleted file mode 100644 index b5a1bf1a3fd02..0000000000000 --- a/packages/next/src/server/future/route-modules/app-page/vendored/shared/entrypoints.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as ReactJsxDevRuntime from 'react/jsx-dev-runtime' -import * as ReactJsxRuntime from 'react/jsx-runtime' - -export { ReactJsxDevRuntime, ReactJsxRuntime } diff --git a/packages/next/src/server/future/route-modules/app-page/vendored/ssr/entrypoints.ts b/packages/next/src/server/future/route-modules/app-page/vendored/ssr/entrypoints.ts index ad3ee57d8f1f5..70f432fff4e88 100644 --- a/packages/next/src/server/future/route-modules/app-page/vendored/ssr/entrypoints.ts +++ b/packages/next/src/server/future/route-modules/app-page/vendored/ssr/entrypoints.ts @@ -1,10 +1,10 @@ import * as React from 'react' - import * as ReactDOM from 'react-dom/server-rendering-stub' +import * as ReactJsxDevRuntime from 'react/jsx-dev-runtime' +import * as ReactJsxRuntime from 'react/jsx-runtime' // eslint-disable-next-line import/no-extraneous-dependencies import * as ReactDOMServerEdge from 'react-dom/server.edge' -// eslint-disable-next-line import/no-extraneous-dependencies function getAltProxyForBindingsDEV( type: 'Turbopack' | 'Webpack', @@ -52,6 +52,8 @@ if (process.env.TURBOPACK) { export { React, + ReactJsxDevRuntime, + ReactJsxRuntime, ReactDOM, ReactDOMServerEdge, ReactServerDOMTurbopackClientEdge, diff --git a/packages/next/src/server/future/route-modules/app-page/vendored/ssr/react-jsx-dev-runtime.ts b/packages/next/src/server/future/route-modules/app-page/vendored/ssr/react-jsx-dev-runtime.ts new file mode 100644 index 0000000000000..501951272d5a7 --- /dev/null +++ b/packages/next/src/server/future/route-modules/app-page/vendored/ssr/react-jsx-dev-runtime.ts @@ -0,0 +1,3 @@ +module.exports = require('../../module.compiled').vendored[ + 'react-ssr' +].ReactJsxDevRuntime diff --git a/packages/next/src/server/future/route-modules/app-page/vendored/ssr/react-jsx-runtime.ts b/packages/next/src/server/future/route-modules/app-page/vendored/ssr/react-jsx-runtime.ts new file mode 100644 index 0000000000000..82499202f03eb --- /dev/null +++ b/packages/next/src/server/future/route-modules/app-page/vendored/ssr/react-jsx-runtime.ts @@ -0,0 +1,3 @@ +module.exports = require('../../module.compiled').vendored[ + 'react-ssr' +].ReactJsxRuntime diff --git a/packages/next/src/server/lib/incremental-cache/file-system-cache.ts b/packages/next/src/server/lib/incremental-cache/file-system-cache.ts index 79e18ad3b996b..0d63dd3e8a709 100644 --- a/packages/next/src/server/lib/incremental-cache/file-system-cache.ts +++ b/packages/next/src/server/lib/incremental-cache/file-system-cache.ts @@ -75,7 +75,7 @@ export default class FileSystemCache implements CacheHandler { if (!this.tagsManifestPath || !this.fs || tagsManifest) return try { tagsManifest = JSON.parse( - this.fs.readFileSync(this.tagsManifestPath).toString('utf8') + this.fs.readFileSync(this.tagsManifestPath, 'utf8') ) } catch (err: any) { tagsManifest = { version: 1, items: {} } @@ -131,9 +131,7 @@ export default class FileSystemCache implements CacheHandler { const { mtime } = await this.fs.stat(filePath) const meta = JSON.parse( - ( - await this.fs.readFile(filePath.replace(/\.body$/, '.meta')) - ).toString('utf8') + await this.fs.readFile(filePath.replace(/\.body$/, '.meta'), 'utf8') ) const cacheEntry: CacheHandlerValue = { @@ -155,7 +153,7 @@ export default class FileSystemCache implements CacheHandler { pathname: fetchCache ? key : `${key}.html`, fetchCache, }) - const fileData = (await this.fs.readFile(filePath)).toString('utf-8') + const fileData = await this.fs.readFile(filePath, 'utf8') const { mtime } = await this.fs.stat(filePath) if (fetchCache) { @@ -178,27 +176,25 @@ export default class FileSystemCache implements CacheHandler { } } else { const pageData = isAppPath - ? ( + ? await this.fs.readFile( + ( + await this.getFsPath({ + pathname: `${key}.rsc`, + appDir: true, + }) + ).filePath, + 'utf8' + ) + : JSON.parse( await this.fs.readFile( ( await this.getFsPath({ - pathname: `${key}.rsc`, - appDir: true, + pathname: `${key}.json`, + appDir: false, }) - ).filePath + ).filePath, + 'utf8' ) - ).toString('utf8') - : JSON.parse( - ( - await this.fs.readFile( - ( - await this.getFsPath({ - pathname: `${key}.json`, - appDir: false, - }) - ).filePath - ) - ).toString('utf8') ) let meta: { status?: number; headers?: OutgoingHttpHeaders } = {} @@ -206,9 +202,10 @@ export default class FileSystemCache implements CacheHandler { if (isAppPath) { try { meta = JSON.parse( - ( - await this.fs.readFile(filePath.replace(/\.html$/, '.meta')) - ).toString('utf-8') + await this.fs.readFile( + filePath.replace(/\.html$/, '.meta'), + 'utf8' + ) ) } catch {} } diff --git a/packages/next/src/server/lib/node-fs-methods.ts b/packages/next/src/server/lib/node-fs-methods.ts index 490c4e0336c4a..121bf9cc89f40 100644 --- a/packages/next/src/server/lib/node-fs-methods.ts +++ b/packages/next/src/server/lib/node-fs-methods.ts @@ -2,8 +2,8 @@ import _fs from 'fs' import type { CacheFs } from '../../shared/lib/utils' export const nodeFs: CacheFs = { - readFile: (f) => _fs.promises.readFile(f), - readFileSync: (f) => _fs.readFileSync(f), + readFile: _fs.promises.readFile, + readFileSync: _fs.readFileSync, writeFile: (f, d) => _fs.promises.writeFile(f, d), mkdir: (dir) => _fs.promises.mkdir(dir, { recursive: true }), stat: (f) => _fs.promises.stat(f), diff --git a/packages/next/src/server/lib/router-server.ts b/packages/next/src/server/lib/router-server.ts index c8158e295baf2..b16858a506458 100644 --- a/packages/next/src/server/lib/router-server.ts +++ b/packages/next/src/server/lib/router-server.ts @@ -7,6 +7,7 @@ import type { WorkerRequestHandler, WorkerUpgradeHandler } from './types' import '../node-polyfill-fetch' import '../node-environment' import '../require-hook' +import '../../lib/polyfill-promise-with-resolvers' import url from 'url' import path from 'path' diff --git a/packages/next/src/server/lib/schedule-on-next-tick.ts b/packages/next/src/server/lib/schedule-on-next-tick.ts index 7004d0e953ae3..8374d4bd692b0 100644 --- a/packages/next/src/server/lib/schedule-on-next-tick.ts +++ b/packages/next/src/server/lib/schedule-on-next-tick.ts @@ -1,10 +1,11 @@ -type ScheduledFn = () => T | PromiseLike +export type ScheduledFn = () => T | PromiseLike +export type SchedulerFn = (cb: ScheduledFn) => void /** * Schedules a function to be called on the next tick after the other promises * have been resolved. */ -export function scheduleOnNextTick(cb: ScheduledFn): void { +export const scheduleOnNextTick = (cb: ScheduledFn): void => { // We use Promise.resolve().then() here so that the operation is scheduled at // the end of the promise job queue, we then add it to the next process tick // to ensure it's evaluated afterwards. diff --git a/packages/next/src/server/lib/start-server.ts b/packages/next/src/server/lib/start-server.ts index 61266efcdcc74..9300fe5f69bf2 100644 --- a/packages/next/src/server/lib/start-server.ts +++ b/packages/next/src/server/lib/start-server.ts @@ -18,7 +18,7 @@ import { formatHostname } from './format-hostname' import { initialize } from './router-server' import { checkIsNodeDebugging } from './is-node-debugging' import { CONFIG_FILES } from '../../shared/lib/constants' -import { bold, magenta } from '../../lib/picocolors' +import { bold, purple } from '../../lib/picocolors' const debug = setupDebug('next:start-server') @@ -92,11 +92,7 @@ function logStartInfo({ formatDurationText: string }) { Log.bootstrap( - bold( - magenta( - `${`${Log.prefixes.ready} Next.js`} ${process.env.__NEXT_VERSION}` - ) - ) + bold(purple(`${Log.prefixes.ready} Next.js ${process.env.__NEXT_VERSION}`)) ) Log.bootstrap(`- Local: ${appUrl}`) if (hostname) { diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index ca8510c57e29e..ead7b51bc8fed 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -4,6 +4,7 @@ import './node-polyfill-fetch' import './node-polyfill-form' import './node-polyfill-web-streams' import './node-polyfill-crypto' +import '../lib/polyfill-promise-with-resolvers' import type { TLSSocket } from 'tls' import { @@ -367,8 +368,8 @@ export default class NextNodeServer extends BaseServer { const buildIdFile = join(this.distDir, BUILD_ID_FILE) try { return fs.readFileSync(buildIdFile, 'utf8').trim() - } catch (err) { - if (!fs.existsSync(buildIdFile)) { + } catch (err: any) { + if (err.code === 'ENOENT') { throw new Error( `Could not find a production build in the '${this.distDir}' directory. Try building your app with 'next build' before starting the production server. https://nextjs.org/docs/messages/production-start-no-build-id` ) @@ -722,14 +723,13 @@ export default class NextNodeServer extends BaseServer { ) } - protected async getFallback(page: string): Promise { + protected getFallback(page: string): Promise { page = normalizePagePath(page) const cacheFs = this.getCacheFilesystem() - const html = await cacheFs.readFile( - join(this.serverDistDir, 'pages', `${page}.html`) + return cacheFs.readFile( + join(this.serverDistDir, 'pages', `${page}.html`), + 'utf8' ) - - return html.toString('utf8') } protected async handleNextImageRequest( @@ -985,10 +985,11 @@ export default class NextNodeServer extends BaseServer { return this.runApi(req, res, query, match) } - protected async getPrefetchRsc(pathname: string) { - return this.getCacheFilesystem() - .readFile(join(this.serverDistDir, 'app', `${pathname}.prefetch.rsc`)) - .then((res) => res.toString()) + protected getPrefetchRsc(pathname: string): Promise { + return this.getCacheFilesystem().readFile( + join(this.serverDistDir, 'app', `${pathname}.prefetch.rsc`), + 'utf8' + ) } protected getCacheFilesystem(): CacheFs { diff --git a/packages/next/src/server/node-environment.ts b/packages/next/src/server/node-environment.ts index 88780ed6dc8b8..fdd0d585dc9ce 100644 --- a/packages/next/src/server/node-environment.ts +++ b/packages/next/src/server/node-environment.ts @@ -14,31 +14,3 @@ if (typeof (globalThis as any).WebSocket !== 'function') { }, }) } - -// This adds a `Promise.withResolvers` polyfill. This will soon be adopted into -// the spec. -// -// TODO: remove this polyfill when it is adopted into the spec. -// -// https://tc39.es/proposal-promise-with-resolvers/ -// -if ( - !('withResolvers' in Promise) || - typeof Promise.withResolvers !== 'function' -) { - Promise.withResolvers = () => { - let resolvers: { - resolve: (value: T | PromiseLike) => void - reject: (reason: any) => void - } - - // Create the promise and assign the resolvers to the object. - const promise = new Promise((resolve, reject) => { - resolvers = { resolve, reject } - }) - - // We know that resolvers is defined because the Promise constructor runs - // synchronously. - return { promise, resolve: resolvers!.resolve, reject: resolvers!.reject } - } -} diff --git a/packages/next/src/server/response-cache/index.ts b/packages/next/src/server/response-cache/index.ts index 7bd6710cc4a80..9a0451e4dbe78 100644 --- a/packages/next/src/server/response-cache/index.ts +++ b/packages/next/src/server/response-cache/index.ts @@ -6,20 +6,36 @@ import type { } from './types' import RenderResult from '../render-result' +import { Batcher } from '../../lib/batcher' +import { scheduleOnNextTick } from '../lib/schedule-on-next-tick' export * from './types' export default class ResponseCache { - pendingResponses: Map> - previousCacheItem?: { + private readonly batcher = Batcher.create< + { key: string; isOnDemandRevalidate: boolean }, + ResponseCacheEntry | null, + string + >({ + // Ensure on-demand revalidate doesn't block normal requests, it should be + // safe to run an on-demand revalidate for the same key as a normal request. + cacheKeyFn: ({ key, isOnDemandRevalidate }) => + `${key}-${isOnDemandRevalidate ? '1' : '0'}`, + // We wait to do any async work until after we've added our promise to + // `pendingResponses` to ensure that any any other calls will reuse the + // same promise until we've fully finished our work. + schedulerFn: scheduleOnNextTick, + }) + + private previousCacheItem?: { key: string entry: ResponseCacheEntry | null expiresAt: number } - minimalMode?: boolean + + private minimalMode?: boolean constructor(minimalMode: boolean) { - this.pendingResponses = new Map() // this is a hack to avoid Webpack knowing this is equal to this.minimalMode // because we replace this.minimalMode to true in production bundles. const minimalModeKey = 'minimalMode' @@ -35,166 +51,130 @@ export default class ResponseCache { incrementalCache: IncrementalCache } ): Promise { - const { incrementalCache } = context - // ensure on-demand revalidate doesn't block normal requests - const pendingResponseKey = key - ? `${key}-${context.isOnDemandRevalidate ? '1' : '0'}` - : null - - const pendingResponse = pendingResponseKey - ? this.pendingResponses.get(pendingResponseKey) - : null - - if (pendingResponse) { - return pendingResponse - } - - let resolver: (cacheEntry: ResponseCacheEntry | null) => void = () => {} - let rejecter: (error: Error) => void = () => {} - const promise: Promise = new Promise( - (resolve, reject) => { - resolver = resolve - rejecter = reject - } - ) - if (pendingResponseKey) { - this.pendingResponses.set(pendingResponseKey, promise) - } - - let resolved = false - const resolve = (cacheEntry: ResponseCacheEntry | null) => { - if (pendingResponseKey) { - // Ensure all reads from the cache get the latest value. - this.pendingResponses.set( - pendingResponseKey, - Promise.resolve(cacheEntry) - ) - } - if (!resolved) { - resolved = true - resolver(cacheEntry) - } - } + // If there is no key for the cache, we can't possibly look this up in the + // cache so just return the result of the response generator. + if (!key) return responseGenerator(false, null) + + const { incrementalCache, isOnDemandRevalidate = false } = context + + return this.batcher.batch( + { key, isOnDemandRevalidate }, + async (cacheKey, resolve) => { + // We keep the previous cache entry around to leverage when the + // incremental cache is disabled in minimal mode. + if ( + this.minimalMode && + this.previousCacheItem?.key === cacheKey && + this.previousCacheItem.expiresAt > Date.now() + ) { + return this.previousCacheItem.entry + } - // we keep the previous cache entry around to leverage - // when the incremental cache is disabled in minimal mode - if ( - pendingResponseKey && - this.minimalMode && - this.previousCacheItem?.key === pendingResponseKey && - this.previousCacheItem.expiresAt > Date.now() - ) { - resolve(this.previousCacheItem.entry) - this.pendingResponses.delete(pendingResponseKey) - return promise - } + let resolved = false + let cachedResponse: IncrementalCacheItem = null + try { + cachedResponse = !this.minimalMode + ? await incrementalCache.get(key) + : null + + if (cachedResponse && !isOnDemandRevalidate) { + if (cachedResponse.value?.kind === 'FETCH') { + throw new Error( + `invariant: unexpected cachedResponse of kind fetch in response cache` + ) + } - // We wait to do any async work until after we've added our promise to - // `pendingResponses` to ensure that any any other calls will reuse the - // same promise until we've fully finished our work. - ;(async () => { - let cachedResponse: IncrementalCacheItem = null - try { - cachedResponse = - key && !this.minimalMode ? await incrementalCache.get(key) : null - - if (cachedResponse && !context.isOnDemandRevalidate) { - if (cachedResponse.value?.kind === 'FETCH') { - throw new Error( - `invariant: unexpected cachedResponse of kind fetch in response cache` - ) + resolve({ + isStale: cachedResponse.isStale, + revalidate: cachedResponse.curRevalidate, + value: + cachedResponse.value?.kind === 'PAGE' + ? { + kind: 'PAGE', + html: RenderResult.fromStatic(cachedResponse.value.html), + pageData: cachedResponse.value.pageData, + headers: cachedResponse.value.headers, + status: cachedResponse.value.status, + } + : cachedResponse.value, + }) + resolved = true + + if (!cachedResponse.isStale || context.isPrefetch) { + // The cached value is still valid, so we don't need + // to update it yet. + return null + } } - resolve({ - isStale: cachedResponse.isStale, - revalidate: cachedResponse.curRevalidate, - value: - cachedResponse.value?.kind === 'PAGE' - ? { - kind: 'PAGE', - html: RenderResult.fromStatic(cachedResponse.value.html), - pageData: cachedResponse.value.pageData, - headers: cachedResponse.value.headers, - status: cachedResponse.value.status, - } - : cachedResponse.value, - }) - if (!cachedResponse.isStale || context.isPrefetch) { - // The cached value is still valid, so we don't need - // to update it yet. - return + const cacheEntry = await responseGenerator(resolved, cachedResponse) + const resolveValue = + cacheEntry === null + ? null + : { + ...cacheEntry, + isMiss: !cachedResponse, + } + + // For on-demand revalidate wait to resolve until cache is set. + // Otherwise resolve now. + if (!isOnDemandRevalidate && !resolved) { + resolve(resolveValue) + resolved = true } - } - const cacheEntry = await responseGenerator(resolved, cachedResponse) - const resolveValue = - cacheEntry === null - ? null - : { - ...cacheEntry, - isMiss: !cachedResponse, + if (cacheEntry && typeof cacheEntry.revalidate !== 'undefined') { + if (this.minimalMode) { + this.previousCacheItem = { + key: cacheKey, + entry: cacheEntry, + expiresAt: Date.now() + 1000, } - - // for on-demand revalidate wait to resolve until cache is set - if (!context.isOnDemandRevalidate) { - resolve(resolveValue) - } - - if (key && cacheEntry && typeof cacheEntry.revalidate !== 'undefined') { - if (this.minimalMode) { - this.previousCacheItem = { - key: pendingResponseKey || key, - entry: cacheEntry, - expiresAt: Date.now() + 1000, + } else { + await incrementalCache.set( + key, + cacheEntry.value?.kind === 'PAGE' + ? { + kind: 'PAGE', + html: cacheEntry.value.html.toUnchunkedString(), + pageData: cacheEntry.value.pageData, + headers: cacheEntry.value.headers, + status: cacheEntry.value.status, + } + : cacheEntry.value, + { + revalidate: cacheEntry.revalidate, + } + ) } } else { - await incrementalCache.set( - key, - cacheEntry.value?.kind === 'PAGE' - ? { - kind: 'PAGE', - html: cacheEntry.value.html.toUnchunkedString(), - pageData: cacheEntry.value.pageData, - headers: cacheEntry.value.headers, - status: cacheEntry.value.status, - } - : cacheEntry.value, - { - revalidate: cacheEntry.revalidate, - } - ) + this.previousCacheItem = undefined } - } else { - this.previousCacheItem = undefined - } - if (context.isOnDemandRevalidate) { - resolve(resolveValue) - } - } catch (err) { - // when a getStaticProps path is erroring we automatically re-set the - // existing cache under a new expiration to prevent non-stop retrying - if (cachedResponse && key) { - await incrementalCache.set(key, cachedResponse.value, { - revalidate: Math.min( - Math.max(cachedResponse.revalidate || 3, 3), - 30 - ), - }) - } - // while revalidating in the background we can't reject as - // we already resolved the cache entry so log the error here - if (resolved) { - console.error(err) - } else { - rejecter(err as Error) - } - } finally { - if (pendingResponseKey) { - this.pendingResponses.delete(pendingResponseKey) + return resolveValue + } catch (err) { + // When a getStaticProps path is erroring we automatically re-set the + // existing cache under a new expiration to prevent non-stop retrying. + if (cachedResponse) { + await incrementalCache.set(key, cachedResponse.value, { + revalidate: Math.min( + Math.max(cachedResponse.revalidate || 3, 3), + 30 + ), + }) + } + + // While revalidating in the background we can't reject as we already + // resolved the cache entry so log the error here. + if (resolved) { + console.error(err) + return null + } + + // We haven't resolved yet, so let's throw to indicate an error. + throw err } } - })() - return promise + ) } } diff --git a/packages/next/src/shared/lib/utils.ts b/packages/next/src/shared/lib/utils.ts index 771f11b4bec23..61b8fc394f93a 100644 --- a/packages/next/src/shared/lib/utils.ts +++ b/packages/next/src/shared/lib/utils.ts @@ -7,6 +7,7 @@ import type { NextRouter } from './router/router' import type { ParsedUrlQuery } from 'querystring' import type { PreviewData } from 'next/types' import { COMPILER_NAMES } from './constants' +import type fs from 'fs' export type NextComponentType< Context extends BaseContext = NextPageContext, @@ -447,8 +448,8 @@ export class MiddlewareNotFoundError extends Error { } export interface CacheFs { - readFile(f: string): Promise - readFileSync(f: string): Buffer + readFile: typeof fs.promises.readFile + readFileSync: typeof fs.readFileSync writeFile(f: string, d: any): Promise mkdir(dir: string): Promise stat(f: string): Promise<{ mtime: Date }> diff --git a/packages/next/webpack.config.js b/packages/next/webpack.config.js index bc8a5c72a2f1e..ec4880d319f8b 100644 --- a/packages/next/webpack.config.js +++ b/packages/next/webpack.config.js @@ -208,6 +208,9 @@ module.exports = ({ dev, turbo, bundleType, experimental }) => { react$: `next/dist/compiled/react${ experimental ? '-experimental' : '' }/react.shared-subset`, + 'next/dist/compiled/react$': `next/dist/compiled/react${ + experimental ? '-experimental' : '' + }/react.shared-subset`, }, }, layer: 'react-server', @@ -220,6 +223,9 @@ module.exports = ({ dev, turbo, bundleType, experimental }) => { react$: `next/dist/compiled/react${ experimental ? '-experimental' : '' }/react.shared-subset`, + 'next/dist/compiled/react$': `next/dist/compiled/react${ + experimental ? '-experimental' : '' + }/react.shared-subset`, }, }, }, diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index af182a485e276..81edef1fd77a8 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.5.5-canary.0", + "version": "13.5.5-canary.2", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 7224e93a60c45..6c648bd5fc2d5 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.5.5-canary.0", + "version": "13.5.5-canary.2", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index c9abc15e988bf..fbef1a4a4f255 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "13.5.5-canary.0", + "version": "13.5.5-canary.2", "private": true, "repository": { "url": "vercel/next.js", @@ -23,7 +23,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "13.5.5-canary.0", + "next": "13.5.5-canary.2", "outdent": "0.8.0", "prettier": "2.5.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66c9b9eea2b79..22bfb1ffa1aab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -732,7 +732,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 13.5.5-canary.0 + specifier: 13.5.5-canary.2 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.3.3 @@ -793,7 +793,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 13.5.5-canary.0 + specifier: 13.5.5-canary.2 version: link:../next-env '@swc/helpers': specifier: 0.5.2 @@ -917,19 +917,19 @@ importers: specifier: 1.1.0 version: 1.1.0 '@next/polyfill-module': - specifier: 13.5.5-canary.0 + specifier: 13.5.5-canary.2 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 13.5.5-canary.0 + specifier: 13.5.5-canary.2 version: link:../next-polyfill-nomodule '@next/react-dev-overlay': - specifier: 13.5.5-canary.0 + specifier: 13.5.5-canary.2 version: link:../react-dev-overlay '@next/react-refresh-utils': - specifier: 13.5.5-canary.0 + specifier: 13.5.5-canary.2 version: link:../react-refresh-utils '@next/swc': - specifier: 13.5.5-canary.0 + specifier: 13.5.5-canary.2 version: link:../next-swc '@opentelemetry/api': specifier: 1.4.1 @@ -1586,7 +1586,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 13.5.5-canary.0 + specifier: 13.5.5-canary.2 version: link:../next outdent: specifier: 0.8.0 diff --git a/scripts/send-trace-to-jaeger/Cargo.lock b/scripts/send-trace-to-jaeger/Cargo.lock index db05bc0325a25..d0b5c5da023eb 100644 --- a/scripts/send-trace-to-jaeger/Cargo.lock +++ b/scripts/send-trace-to-jaeger/Cargo.lock @@ -22,9 +22,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" -version = "3.8.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "bytes" diff --git a/test/e2e/app-dir/app/index.test.ts b/test/e2e/app-dir/app/index.test.ts index 4aa079cc79023..e082ebfe292b0 100644 --- a/test/e2e/app-dir/app/index.test.ts +++ b/test/e2e/app-dir/app/index.test.ts @@ -15,6 +15,15 @@ createNextDescribe( }, ({ next, isNextDev: isDev, isNextStart, isNextDeploy }) => { if (isNextStart && !process.env.NEXT_EXPERIMENTAL_COMPILE) { + it('should not have loader generated function for edge runtime', async () => { + expect( + await next.readFile('.next/server/app/dashboard/page.js') + ).not.toContain('_stringifiedConfig') + expect(await next.readFile('.next/server/middleware.js')).not.toContain( + '_middlewareConfig' + ) + }) + it('should use RSC prefetch data from build', async () => { expect( await next.readFile('.next/server/app/linking.prefetch.rsc') diff --git a/test/e2e/app-dir/third-parties/app/google-maps-embed/page.js b/test/e2e/app-dir/third-parties/app/google-maps-embed/page.js index 28210fe9bab6f..f9e312c9116f9 100644 --- a/test/e2e/app-dir/third-parties/app/google-maps-embed/page.js +++ b/test/e2e/app-dir/third-parties/app/google-maps-embed/page.js @@ -2,7 +2,7 @@ import { GoogleMapsEmbed } from '@next/third-parties/google' const Page = () => { return ( -
+

Google Maps Embed

{ return ( -
+

Youtube Embed