From 8b01e2578e6cc396cb68e27c455fdd4449ecef05 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Mon, 23 Oct 2023 22:01:00 -0700 Subject: [PATCH 01/15] Fix nested unstable_cache revalidating (#57316) This ensures when we call `revalidateTag` and there are nested `unstable_cache` entries we properly revalidate. --- .../web/spec-extension/unstable-cache.ts | 3 ++ .../e2e/app-dir/app-static/app-static.test.ts | 3 ++ .../revalidate-360/page.js | 28 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/packages/next/src/server/web/spec-extension/unstable-cache.ts b/packages/next/src/server/web/spec-extension/unstable-cache.ts index 3d49fdfe10d39..3f6c1e9ca3f8f 100644 --- a/packages/next/src/server/web/spec-extension/unstable-cache.ts +++ b/packages/next/src/server/web/spec-extension/unstable-cache.ts @@ -82,6 +82,9 @@ export function unstable_cache( const cacheKey = await incrementalCache?.fetchCacheKey(joinedKey) const cacheEntry = cacheKey && + // when we are nested inside of other unstable_cache's + // we should bypass cache similar to fetches + store?.fetchCache !== 'force-no-store' && !( store?.isOnDemandRevalidate || incrementalCache.isOnDemandRevalidate ) && diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 55b8e9c135cb7..faf10fa70d32c 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -222,6 +222,7 @@ createNextDescribe( const $ = cheerio.load(html) const initLayoutData = $('#layout-data').text() const initPageData = $('#page-data').text() + const initNestedCacheData = $('#nested-cache').text() const routeHandlerRes = await next.fetch( '/route-handler/revalidate-360' @@ -254,6 +255,7 @@ createNextDescribe( const new$ = cheerio.load(newHtml) const newLayoutData = new$('#layout-data').text() const newPageData = new$('#page-data').text() + const newNestedCacheData = new$('#nested-cache').text() const newRouteHandlerRes = await next.fetch( '/route-handler/revalidate-360' @@ -271,6 +273,7 @@ createNextDescribe( expect(newEdgeRouteHandlerData).toBeTruthy() expect(newLayoutData).not.toBe(initLayoutData) expect(newPageData).not.toBe(initPageData) + expect(newNestedCacheData).not.toBe(initNestedCacheData) expect(newRouteHandlerData).not.toEqual(initRouteHandlerData) expect(newEdgeRouteHandlerData).not.toEqual(initEdgeRouteHandlerRes) return 'success' diff --git a/test/e2e/app-dir/app-static/app/variable-revalidate/revalidate-360/page.js b/test/e2e/app-dir/app-static/app/variable-revalidate/revalidate-360/page.js index c1054734c3ba3..43d8f69fe3035 100644 --- a/test/e2e/app-dir/app-static/app/variable-revalidate/revalidate-360/page.js +++ b/test/e2e/app-dir/app-static/app/variable-revalidate/revalidate-360/page.js @@ -54,6 +54,33 @@ export default async function Page() { } )() + const cacheInner = unstable_cache( + async () => { + console.log('calling cacheInner') + const data = await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random?something', + { + next: { revalidate: 15, tags: ['thankyounext'] }, + } + ).then((res) => res.text()) + return data + }, + [], + { revalidate: 360 } + ) + + const cacheOuter = unstable_cache( + () => { + console.log('cacheOuter') + return cacheInner() + }, + [], + { + revalidate: 1000, + tags: ['thankyounext'], + } + )() + return ( <>

/variable-revalidate/revalidate-360

@@ -65,6 +92,7 @@ export default async function Page() { revalidate 10 (tags: thankyounext): {JSON.stringify(cachedData)}

{Date.now()}

+

nested cache: {cacheOuter}

) } From 2275bf1a22a173279af819bd7e78e04a8fe18430 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Mon, 23 Oct 2023 22:06:36 -0700 Subject: [PATCH 02/15] Revert "Add `__nextjs_pure` helper " (#57318) This seems to be causing our Windows x86_64 next-swc build to stall for some reason so reverting so we can unblock canaries while we investigate further Reverts vercel/next.js#57286 x-ref: https://github.com/vercel/next.js/actions/runs/6620707993/job/17986458061 x-ref: https://github.com/vercel/next.js/actions/runs/6620707993/job/17986458061 --- .../crates/core/src/import_analyzer.rs | 104 ------------------ packages/next-swc/crates/core/src/lib.rs | 3 - packages/next-swc/crates/core/src/pure.rs | 83 -------------- .../next-swc/crates/core/tests/fixture.rs | 21 ---- .../tests/fixture/pure/no-name-clash/input.js | 3 - .../fixture/pure/no-name-clash/output.js | 2 - .../core/tests/fixture/pure/simple/input.js | 3 - .../core/tests/fixture/pure/simple/output.js | 2 - packages/next/src/build/swc/helpers.ts | 3 - 9 files changed, 224 deletions(-) delete mode 100644 packages/next-swc/crates/core/src/import_analyzer.rs delete mode 100644 packages/next-swc/crates/core/src/pure.rs delete mode 100644 packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/input.js delete mode 100644 packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/output.js delete mode 100644 packages/next-swc/crates/core/tests/fixture/pure/simple/input.js delete mode 100644 packages/next-swc/crates/core/tests/fixture/pure/simple/output.js delete mode 100644 packages/next/src/build/swc/helpers.ts diff --git a/packages/next-swc/crates/core/src/import_analyzer.rs b/packages/next-swc/crates/core/src/import_analyzer.rs deleted file mode 100644 index 887bf16ce77b7..0000000000000 --- a/packages/next-swc/crates/core/src/import_analyzer.rs +++ /dev/null @@ -1,104 +0,0 @@ -use turbopack_binding::swc::core::{ - atoms::JsWord, - common::collections::{AHashMap, AHashSet}, - ecma::{ - ast::{ - Expr, Id, ImportDecl, ImportNamedSpecifier, ImportSpecifier, MemberExpr, MemberProp, - Module, ModuleExportName, - }, - visit::{noop_visit_type, Visit, VisitWith}, - }, -}; - -#[derive(Debug, Default)] -pub(crate) struct ImportMap { - /// Map from module name to (module path, exported symbol) - imports: AHashMap, - - namespace_imports: AHashMap, - - imported_modules: AHashSet, -} - -#[allow(unused)] -impl ImportMap { - pub fn is_module_imported(&mut self, module: &JsWord) -> bool { - self.imported_modules.contains(module) - } - - /// Returns true if `e` is an import of `orig_name` from `module`. - pub fn is_import(&self, e: &Expr, module: &str, orig_name: &str) -> bool { - match e { - Expr::Ident(i) => { - if let Some((i_src, i_sym)) = self.imports.get(&i.to_id()) { - i_src == module && i_sym == orig_name - } else { - false - } - } - - Expr::Member(MemberExpr { - obj: box Expr::Ident(obj), - prop: MemberProp::Ident(prop), - .. - }) => { - if let Some(obj_src) = self.namespace_imports.get(&obj.to_id()) { - obj_src == module && prop.sym == *orig_name - } else { - false - } - } - - _ => false, - } - } - - pub fn analyze(m: &Module) -> Self { - let mut data = ImportMap::default(); - - m.visit_with(&mut Analyzer { data: &mut data }); - - data - } -} - -struct Analyzer<'a> { - data: &'a mut ImportMap, -} - -impl Visit for Analyzer<'_> { - noop_visit_type!(); - - fn visit_import_decl(&mut self, import: &ImportDecl) { - self.data.imported_modules.insert(import.src.value.clone()); - - for s in &import.specifiers { - let (local, orig_sym) = match s { - ImportSpecifier::Named(ImportNamedSpecifier { - local, imported, .. - }) => match imported { - Some(imported) => (local.to_id(), orig_name(imported)), - _ => (local.to_id(), local.sym.clone()), - }, - ImportSpecifier::Default(s) => (s.local.to_id(), "default".into()), - ImportSpecifier::Namespace(s) => { - self.data - .namespace_imports - .insert(s.local.to_id(), import.src.value.clone()); - continue; - } - }; - - self.data - .imports - .insert(local, (import.src.value.clone(), orig_sym)); - } - } -} - -fn orig_name(n: &ModuleExportName) -> JsWord { - match n { - ModuleExportName::Ident(v) => v.sym.clone(), - ModuleExportName::Str(v) => v.value.clone(), - } -} diff --git a/packages/next-swc/crates/core/src/lib.rs b/packages/next-swc/crates/core/src/lib.rs index 8ac9ee5914dfd..f810a4ceb0eb9 100644 --- a/packages/next-swc/crates/core/src/lib.rs +++ b/packages/next-swc/crates/core/src/lib.rs @@ -58,13 +58,11 @@ pub mod amp_attributes; mod auto_cjs; pub mod cjs_optimizer; pub mod disallow_re_export_all_in_page; -mod import_analyzer; pub mod named_import_transform; pub mod next_ssg; pub mod optimize_barrel; pub mod optimize_server_react; pub mod page_config; -pub mod pure; pub mod react_server_components; pub mod server_actions; pub mod shake_exports; @@ -190,7 +188,6 @@ where }; chain!( - pure::pure_magic(comments.clone()), disallow_re_export_all_in_page::disallow_re_export_all_in_page(opts.is_page_file), match &opts.server_components { Some(config) if config.truthy() => diff --git a/packages/next-swc/crates/core/src/pure.rs b/packages/next-swc/crates/core/src/pure.rs deleted file mode 100644 index 10d948295bad0..0000000000000 --- a/packages/next-swc/crates/core/src/pure.rs +++ /dev/null @@ -1,83 +0,0 @@ -use turbopack_binding::swc::core::{ - common::{comments::Comments, errors::HANDLER, util::take::Take, Spanned, DUMMY_SP}, - ecma::{ - ast::{CallExpr, Callee, EmptyStmt, Expr, Module, ModuleDecl, ModuleItem, Stmt}, - visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith}, - }, -}; - -use crate::import_analyzer::ImportMap; - -pub fn pure_magic(comments: C) -> impl VisitMut + Fold -where - C: Comments, -{ - as_folder(PureTransform { - imports: Default::default(), - comments, - }) -} - -struct PureTransform -where - C: Comments, -{ - imports: ImportMap, - comments: C, -} - -const MODULE: &str = "next/dist/build/swc/helpers"; -const FN_NAME: &str = "__nextjs_pure"; - -impl VisitMut for PureTransform -where - C: Comments, -{ - fn visit_mut_expr(&mut self, e: &mut Expr) { - e.visit_mut_children_with(self); - - if let Expr::Call(CallExpr { - span, - callee: Callee::Expr(callee), - args, - .. - }) = e - { - if !self.imports.is_import(callee, MODULE, FN_NAME) { - return; - } - - if args.len() != 1 { - HANDLER.with(|handler| { - handler - .struct_span_err(*span, "markAsPure() does not support multiple arguments") - .emit(); - }); - return; - } - - *e = *args[0].expr.take(); - - self.comments.add_pure_comment(e.span().lo); - } - } - - fn visit_mut_module(&mut self, m: &mut Module) { - self.imports = ImportMap::analyze(m); - - m.visit_mut_children_with(self); - } - - fn visit_mut_module_item(&mut self, m: &mut ModuleItem) { - if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = m { - if import.src.value == MODULE { - *m = ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); - return; - } - } - - m.visit_mut_children_with(self); - } - - noop_visit_mut_type!(); -} diff --git a/packages/next-swc/crates/core/tests/fixture.rs b/packages/next-swc/crates/core/tests/fixture.rs index 9e8c9296e96be..61177ccf0f75c 100644 --- a/packages/next-swc/crates/core/tests/fixture.rs +++ b/packages/next-swc/crates/core/tests/fixture.rs @@ -8,7 +8,6 @@ use next_swc::{ optimize_barrel::optimize_barrel, optimize_server_react::optimize_server_react, page_config::page_config_test, - pure::pure_magic, react_server_components::server_components, server_actions::{ server_actions, {self}, @@ -572,23 +571,3 @@ where { serde_json::from_str(s).expect("failed to deserialize") } - -#[fixture("tests/fixture/pure/**/input.js")] -fn pure(input: PathBuf) { - let output = input.parent().unwrap().join("output.js"); - test_fixture( - syntax(), - &|tr| { - let unresolved_mark = Mark::new(); - let top_level_mark = Mark::new(); - - chain!( - resolver(unresolved_mark, top_level_mark, false), - pure_magic(tr.comments.clone()) - ) - }, - &input, - &output, - Default::default(), - ); -} diff --git a/packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/input.js b/packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/input.js deleted file mode 100644 index 747b0d7e6ac2e..0000000000000 --- a/packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/input.js +++ /dev/null @@ -1,3 +0,0 @@ -import { __nextjs_pure } from 'not-next-magic' - -__nextjs_pure(console.log('test!')) diff --git a/packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/output.js b/packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/output.js deleted file mode 100644 index 33c9697daf622..0000000000000 --- a/packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/output.js +++ /dev/null @@ -1,2 +0,0 @@ -import { __nextjs_pure } from 'not-next-magic'; -__nextjs_pure(console.log("test!")); diff --git a/packages/next-swc/crates/core/tests/fixture/pure/simple/input.js b/packages/next-swc/crates/core/tests/fixture/pure/simple/input.js deleted file mode 100644 index ef9d6ffcb14ad..0000000000000 --- a/packages/next-swc/crates/core/tests/fixture/pure/simple/input.js +++ /dev/null @@ -1,3 +0,0 @@ -import { __nextjs_pure } from 'next/dist/build/swc/helpers' - -__nextjs_pure(console.log('test!')) diff --git a/packages/next-swc/crates/core/tests/fixture/pure/simple/output.js b/packages/next-swc/crates/core/tests/fixture/pure/simple/output.js deleted file mode 100644 index 2a10791f7f85f..0000000000000 --- a/packages/next-swc/crates/core/tests/fixture/pure/simple/output.js +++ /dev/null @@ -1,2 +0,0 @@ -; -/*#__PURE__*/ console.log("test!"); diff --git a/packages/next/src/build/swc/helpers.ts b/packages/next/src/build/swc/helpers.ts deleted file mode 100644 index bea1cf7eae167..0000000000000 --- a/packages/next/src/build/swc/helpers.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function __nextjs_pure(expr: T): T { - return expr -} From c77fec827f5147c161dc9fe9b4a38e4f253f12dd Mon Sep 17 00:00:00 2001 From: vercel-release-bot Date: Tue, 24 Oct 2023 05:34:15 +0000 Subject: [PATCH 03/15] v13.5.7-canary.22 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- packages/eslint-config-next/package.json | 4 ++-- packages/eslint-plugin-next/package.json | 2 +- packages/font/package.json | 2 +- packages/next-bundle-analyzer/package.json | 2 +- packages/next-codemod/package.json | 2 +- packages/next-env/package.json | 2 +- packages/next-mdx/package.json | 2 +- packages/next-plugin-storybook/package.json | 2 +- packages/next-polyfill-module/package.json | 2 +- packages/next-polyfill-nomodule/package.json | 2 +- packages/next-swc/package.json | 2 +- packages/next/package.json | 14 +++++++------- packages/react-dev-overlay/package.json | 2 +- packages/react-refresh-utils/package.json | 2 +- packages/third-parties/package.json | 4 ++-- pnpm-lock.yaml | 18 +++++++++--------- 18 files changed, 34 insertions(+), 34 deletions(-) diff --git a/lerna.json b/lerna.json index 54ab7252d87fe..94c8ebea80cf4 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "13.5.7-canary.21" + "version": "13.5.7-canary.22" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 769d7a1dd1db3..8fe25402c4d10 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.7-canary.21", + "version": "13.5.7-canary.22", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 6b274c272cac3..43e6c5618de9f 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.7-canary.21", + "version": "13.5.7-canary.22", "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.7-canary.21", + "@next/eslint-plugin-next": "13.5.7-canary.22", "@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 bacd1bf88d367..e2295f0596123 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.7-canary.21", + "version": "13.5.7-canary.22", "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 08ad38de8f7fa..8c4b157f3fcdb 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "13.5.7-canary.21", + "version": "13.5.7-canary.22", "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 96a472de1b996..062e43d4c210e 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.7-canary.21", + "version": "13.5.7-canary.22", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 3162247b6f286..3878aa9fb1219 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "13.5.7-canary.21", + "version": "13.5.7-canary.22", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index d09821e2a08ad..bfbda21bee9a0 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "13.5.7-canary.21", + "version": "13.5.7-canary.22", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index b4d4b91000a20..efb74d4690da1 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "13.5.7-canary.21", + "version": "13.5.7-canary.22", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index b1442fd007de0..498d2dfcadcfc 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.7-canary.21", + "version": "13.5.7-canary.22", "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 0345d9280d562..04301d76592ca 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.7-canary.21", + "version": "13.5.7-canary.22", "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 2b8db8a59a634..1b0f0d3de4eb7 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.7-canary.21", + "version": "13.5.7-canary.22", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index c7f99bc64d19e..a7fd47b62a712 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "13.5.7-canary.21", + "version": "13.5.7-canary.22", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", diff --git a/packages/next/package.json b/packages/next/package.json index 8cc6f083fa647..7b080a40db3cc 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "13.5.7-canary.21", + "version": "13.5.7-canary.22", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -92,7 +92,7 @@ ] }, "dependencies": { - "@next/env": "13.5.7-canary.21", + "@next/env": "13.5.7-canary.22", "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", @@ -146,11 +146,11 @@ "@mswjs/interceptors": "0.23.0", "@napi-rs/cli": "2.16.2", "@napi-rs/triples": "1.1.0", - "@next/polyfill-module": "13.5.7-canary.21", - "@next/polyfill-nomodule": "13.5.7-canary.21", - "@next/react-dev-overlay": "13.5.7-canary.21", - "@next/react-refresh-utils": "13.5.7-canary.21", - "@next/swc": "13.5.7-canary.21", + "@next/polyfill-module": "13.5.7-canary.22", + "@next/polyfill-nomodule": "13.5.7-canary.22", + "@next/react-dev-overlay": "13.5.7-canary.22", + "@next/react-refresh-utils": "13.5.7-canary.22", + "@next/swc": "13.5.7-canary.22", "@opentelemetry/api": "1.4.1", "@playwright/test": "^1.35.1", "@taskr/clear": "1.1.0", diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index 64ed98e436224..4acc65ddd7c36 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.7-canary.21", + "version": "13.5.7-canary.22", "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 6d813c3fab92b..6baac79718f8f 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.7-canary.21", + "version": "13.5.7-canary.22", "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 20f07da25318e..288bdf1ac2a7a 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "13.5.7-canary.21", + "version": "13.5.7-canary.22", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -22,7 +22,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "13.5.7-canary.21", + "next": "13.5.7-canary.22", "outdent": "0.8.0", "prettier": "2.5.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13fb1e7398c7c..36ebbd6354179 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -735,7 +735,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 13.5.7-canary.21 + specifier: 13.5.7-canary.22 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.3.3 @@ -796,7 +796,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 13.5.7-canary.21 + specifier: 13.5.7-canary.22 version: link:../next-env '@swc/helpers': specifier: 0.5.2 @@ -920,19 +920,19 @@ importers: specifier: 1.1.0 version: 1.1.0 '@next/polyfill-module': - specifier: 13.5.7-canary.21 + specifier: 13.5.7-canary.22 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 13.5.7-canary.21 + specifier: 13.5.7-canary.22 version: link:../next-polyfill-nomodule '@next/react-dev-overlay': - specifier: 13.5.7-canary.21 + specifier: 13.5.7-canary.22 version: link:../react-dev-overlay '@next/react-refresh-utils': - specifier: 13.5.7-canary.21 + specifier: 13.5.7-canary.22 version: link:../react-refresh-utils '@next/swc': - specifier: 13.5.7-canary.21 + specifier: 13.5.7-canary.22 version: link:../next-swc '@opentelemetry/api': specifier: 1.4.1 @@ -1583,7 +1583,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 13.5.7-canary.21 + specifier: 13.5.7-canary.22 version: link:../next outdent: specifier: 0.8.0 @@ -24948,7 +24948,7 @@ packages: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231024.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-231024.2} + resolution: {registry: https://registry.npmjs.org/, tarball: https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231024.2} id: '@gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231024.2' name: '@vercel/turbopack-ecmascript-runtime' version: 0.0.0 From 56d74f42f4cd78d5093f5ce0d94288e5af796c72 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Mon, 23 Oct 2023 22:48:41 -0700 Subject: [PATCH 04/15] bundle analyzer artifacts (#57307) This uploads bundle analyzer artifacts during CI --- .github/workflows/build_reusable.yml | 13 ++++++++++++- .github/workflows/pull_request_stats.yml | 1 + packages/next/webpack.config.js | 13 +++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_reusable.yml b/.github/workflows/build_reusable.yml index 849580725961a..87ad391ddb36d 100644 --- a/.github/workflows/build_reusable.yml +++ b/.github/workflows/build_reusable.yml @@ -21,6 +21,10 @@ on: required: false description: 'whether to skip building native modules' type: string + uploadAnalyzerArtifacts: + required: false + description: 'whether to upload analyzer artifacts' + type: string skipForDocsOnly: required: false description: 'skip for docs only changes' @@ -166,7 +170,7 @@ jobs: - run: pnpm install if: ${{ inputs.skipInstallBuild != 'yes' }} - - run: pnpm build + - run: ANALYZE=1 pnpm build if: ${{ inputs.skipInstallBuild != 'yes' }} - run: pnpm playwright install-deps @@ -186,6 +190,13 @@ jobs: name: turbo run summary path: .turbo/runs + - name: Upload bundle analyzer artifacts + uses: actions/upload-artifact@v3 + if: ${{ inputs.uploadAnalyzerArtifacts == 'yes' }} + with: + name: webpack bundle analysis stats + path: packages/next/dist/compiled/next-server/report.*.html + - name: Upload test reports artifact uses: actions/upload-artifact@v3 if: ${{ inputs.afterBuild }} diff --git a/.github/workflows/pull_request_stats.yml b/.github/workflows/pull_request_stats.yml index c88e5a76ad256..f3cc73e5ae5ce 100644 --- a/.github/workflows/pull_request_stats.yml +++ b/.github/workflows/pull_request_stats.yml @@ -26,6 +26,7 @@ jobs: secrets: inherit with: uploadSwcArtifact: 'yes' + uploadAnalyzerArtifacts: 'yes' stats: name: PR Stats diff --git a/packages/next/webpack.config.js b/packages/next/webpack.config.js index 1a2039ea4464f..39f025222e2b4 100644 --- a/packages/next/webpack.config.js +++ b/packages/next/webpack.config.js @@ -187,6 +187,19 @@ module.exports = ({ dev, turbo, bundleType, experimental }) => { bundleType ), openAnalyzer: false, + ...(process.env.CI + ? { + analyzerMode: 'static', + reportFilename: path.join( + __dirname, + `dist/compiled/next-server/report.${dev ? 'dev' : 'prod'}-${ + turbo ? 'turbo' : 'webpack' + }-${ + experimental ? 'experimental' : 'stable' + }-${bundleType}.html` + ), + } + : {}), }), ].filter(Boolean), stats: { From 02fad829338f0fce2611ec1133fa9dabe974253b Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Mon, 23 Oct 2023 23:19:28 -0700 Subject: [PATCH 05/15] Revert "Increase build-native CI job timeout (#57314)" (#57325) This reverts commit 003ec7a15b56accd86fa85931735dc089b727e06. Re-adding this as a safe guard as the stalling was a real issue --- .github/workflows/build_and_deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_and_deploy.yml b/.github/workflows/build_and_deploy.yml index fd02dcb53df73..599ba653c36c1 100644 --- a/.github/workflows/build_and_deploy.yml +++ b/.github/workflows/build_and_deploy.yml @@ -228,7 +228,7 @@ jobs: name: stable - ${{ matrix.settings.target }} - node@16 runs-on: ${{ matrix.settings.host }} - timeout-minutes: 45 + timeout-minutes: 30 steps: # https://github.com/actions/virtual-environments/issues/1187 - name: tune linux network From c05a11924d03dd4c6cdd465a6da595a1d959ea4e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 23 Oct 2023 23:48:26 -0700 Subject: [PATCH 06/15] PPR Fetch Fix (#57327) --- .../src/client/components/maybe-postpone.ts | 22 ++++ .../components/static-generation-bailout.ts | 46 +++---- packages/next/src/server/lib/patch-fetch.ts | 116 ++++++++++-------- .../web/spec-extension/unstable-cache.ts | 2 +- 4 files changed, 109 insertions(+), 77 deletions(-) create mode 100644 packages/next/src/client/components/maybe-postpone.ts diff --git a/packages/next/src/client/components/maybe-postpone.ts b/packages/next/src/client/components/maybe-postpone.ts new file mode 100644 index 0000000000000..33efca0dfcc08 --- /dev/null +++ b/packages/next/src/client/components/maybe-postpone.ts @@ -0,0 +1,22 @@ +import type { StaticGenerationStore } from './static-generation-async-storage.external' + +export function maybePostpone( + staticGenerationStore: StaticGenerationStore, + reason: string +) { + // If we aren't performing a static generation or we aren't using PPR then + // we don't need to postpone. + if ( + !staticGenerationStore.isStaticGeneration || + !staticGenerationStore.experimental.ppr + ) { + return + } + + // App Route's cannot be postponed, so we only postpone if it's a page. If the + // postpone API is available, use it now. + const React = require('react') as typeof import('react') + if (typeof React.unstable_postpone !== 'function') return + + React.unstable_postpone(reason) +} diff --git a/packages/next/src/client/components/static-generation-bailout.ts b/packages/next/src/client/components/static-generation-bailout.ts index 76a188dd7ff95..473091a6a6be3 100644 --- a/packages/next/src/client/components/static-generation-bailout.ts +++ b/packages/next/src/client/components/static-generation-bailout.ts @@ -1,4 +1,5 @@ import { DynamicServerError } from './hooks-server-context' +import { maybePostpone } from './maybe-postpone' import { staticGenerationAsyncStorage } from './static-generation-async-storage.external' class StaticGenBailoutError extends Error { @@ -25,45 +26,38 @@ export const staticGenerationBailout: StaticGenerationBailout = ( opts ) => { const staticGenerationStore = staticGenerationAsyncStorage.getStore() + if (!staticGenerationStore) return false - if (staticGenerationStore?.forceStatic) { + if (staticGenerationStore.forceStatic) { return true } - if (staticGenerationStore?.dynamicShouldError) { + if (staticGenerationStore.dynamicShouldError) { throw new StaticGenBailoutError( formatErrorMessage(reason, { ...opts, dynamic: opts?.dynamic ?? 'error' }) ) } - if (staticGenerationStore && !staticGenerationStore?.experimental.ppr) { - staticGenerationStore.revalidate = 0 + const message = formatErrorMessage(reason, { + ...opts, + // this error should be caught by Next to bail out of static generation + // in case it's uncaught, this link provides some additional context as to why + link: 'https://nextjs.org/docs/messages/dynamic-server-error', + }) - if (!opts?.dynamic) { - // we can statically prefetch pages that opt into dynamic, - // but not things like headers/cookies - staticGenerationStore.staticPrefetchBailout = true - } - } - - if (staticGenerationStore?.isStaticGeneration) { - const message = formatErrorMessage(reason, { - ...opts, - // this error should be caught by Next to bail out of static generation - // in case it's uncaught, this link provides some additional context as to why - link: 'https://nextjs.org/docs/messages/dynamic-server-error', - }) + maybePostpone(staticGenerationStore, message) - if (staticGenerationStore?.experimental.ppr) { - const React = require('react') as typeof import('react') + // As this is a bailout, we don't want to revalidate, so set the revalidate + // to 0. + staticGenerationStore.revalidate = 0 - // App Route's cannot be postponed, so we only postpone if it's a page. - if (typeof React.unstable_postpone === 'function') { - // This throws a postpone error similar to the below error. - React.unstable_postpone(message) - } - } + if (!opts?.dynamic) { + // we can statically prefetch pages that opt into dynamic, + // but not things like headers/cookies + staticGenerationStore.staticPrefetchBailout = true + } + if (staticGenerationStore.isStaticGeneration) { const err = new DynamicServerError(message) staticGenerationStore.dynamicUsageDescription = reason staticGenerationStore.dynamicUsageStack = err.stack diff --git a/packages/next/src/server/lib/patch-fetch.ts b/packages/next/src/server/lib/patch-fetch.ts index 0d89fce3d3ceb..70d157fd56f0f 100644 --- a/packages/next/src/server/lib/patch-fetch.ts +++ b/packages/next/src/server/lib/patch-fetch.ts @@ -12,6 +12,7 @@ import { NEXT_CACHE_TAG_MAX_LENGTH, } from '../../lib/constants' import * as Log from '../../build/output/log' +import { maybePostpone } from '../../client/components/maybe-postpone' const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge' @@ -70,13 +71,8 @@ const getDerivedTags = (pathname: string): string[] => { return derivedTags } -export function addImplicitTags( - staticGenerationStore: ReturnType -) { +export function addImplicitTags(staticGenerationStore: StaticGenerationStore) { const newTags: string[] = [] - if (!staticGenerationStore) { - return newTags - } const { pagePath, urlPathname } = staticGenerationStore if (!Array.isArray(staticGenerationStore.tags)) { @@ -106,7 +102,7 @@ export function addImplicitTags( } function trackFetchMetric( - staticGenerationStore: ReturnType, + staticGenerationStore: StaticGenerationStore, ctx: { url: string status: number @@ -276,11 +272,19 @@ export function patchFetch({ if (_cache === 'force-cache') { curRevalidate = false - } - if (['no-cache', 'no-store'].includes(_cache || '')) { + } else if ( + _cache === 'no-cache' || + _cache === 'no-store' || + isForceNoStore || + isOnlyNoStore + ) { curRevalidate = 0 + } + + if (_cache === 'no-cache' || _cache === 'no-store') { cacheReason = `cache: ${_cache}` } + if (typeof curRevalidate === 'number' || curRevalidate === false) { revalidate = curRevalidate } @@ -306,7 +310,6 @@ export function patchFetch({ staticGenerationStore.revalidate === 0 if (isForceNoStore) { - revalidate = 0 cacheReason = 'fetchCache = force-no-store' } @@ -320,7 +323,6 @@ export function patchFetch({ `cache: 'force-cache' used on fetch for ${fetchUrl} with 'export const fetchCache = 'only-no-store'` ) } - revalidate = 0 cacheReason = 'fetchCache = only-no-store' } @@ -370,6 +372,11 @@ export function patchFetch({ (typeof staticGenerationStore.revalidate === 'number' && revalidate < staticGenerationStore.revalidate)))) ) { + // If enabled, we should bail out of static generation. + if (revalidate === 0) { + maybePostpone(staticGenerationStore, 'revalidate: 0') + } + staticGenerationStore.revalidate = revalidate } @@ -566,16 +573,47 @@ export function patchFetch({ } } - if (staticGenerationStore.isStaticGeneration) { - if (init && typeof init === 'object') { - const cache = init.cache - // Delete `cache` property as Cloudflare Workers will throw an error - if (isEdgeRuntime) { - delete init.cache - } - if (cache === 'no-store') { - staticGenerationStore.revalidate = 0 - const dynamicUsageReason = `no-store fetch ${input}${ + if ( + staticGenerationStore.isStaticGeneration && + init && + typeof init === 'object' + ) { + const { cache } = init + + // Delete `cache` property as Cloudflare Workers will throw an error + if (isEdgeRuntime) delete init.cache + + if (cache === 'no-store') { + const dynamicUsageReason = `no-store fetch ${input}${ + staticGenerationStore.urlPathname + ? ` ${staticGenerationStore.urlPathname}` + : '' + }` + const err = new DynamicServerError(dynamicUsageReason) + staticGenerationStore.dynamicUsageErr = err + staticGenerationStore.dynamicUsageStack = err.stack + staticGenerationStore.dynamicUsageDescription = dynamicUsageReason + + // If enabled, we should bail out of static generation. + maybePostpone(staticGenerationStore, dynamicUsageReason) + + // PPR is not enabled, or React postpone is not available, we + // should set the revalidate to 0. + staticGenerationStore.revalidate = 0 + } + + const hasNextConfig = 'next' in init + const { next = {} } = init + if ( + typeof next.revalidate === 'number' && + (typeof staticGenerationStore.revalidate === 'undefined' || + (typeof staticGenerationStore.revalidate === 'number' && + next.revalidate < staticGenerationStore.revalidate)) + ) { + const forceDynamic = staticGenerationStore.forceDynamic + + if (!forceDynamic && next.revalidate === 0) { + const dynamicUsageReason = `revalidate: 0 fetch ${input}${ staticGenerationStore.urlPathname ? ` ${staticGenerationStore.urlPathname}` : '' @@ -584,39 +622,17 @@ export function patchFetch({ staticGenerationStore.dynamicUsageErr = err staticGenerationStore.dynamicUsageStack = err.stack staticGenerationStore.dynamicUsageDescription = dynamicUsageReason - } - const hasNextConfig = 'next' in init - const next = init.next || {} - if ( - typeof next.revalidate === 'number' && - (typeof staticGenerationStore.revalidate === 'undefined' || - (typeof staticGenerationStore.revalidate === 'number' && - next.revalidate < staticGenerationStore.revalidate)) - ) { - const forceDynamic = staticGenerationStore.forceDynamic - - if (!forceDynamic || next.revalidate !== 0) { - staticGenerationStore.revalidate = next.revalidate - } + // If enabled, we should bail out of static generation. + maybePostpone(staticGenerationStore, dynamicUsageReason) + } - if (!forceDynamic && next.revalidate === 0) { - const dynamicUsageReason = `revalidate: ${ - next.revalidate - } fetch ${input}${ - staticGenerationStore.urlPathname - ? ` ${staticGenerationStore.urlPathname}` - : '' - }` - const err = new DynamicServerError(dynamicUsageReason) - staticGenerationStore.dynamicUsageErr = err - staticGenerationStore.dynamicUsageStack = err.stack - staticGenerationStore.dynamicUsageDescription = - dynamicUsageReason - } + if (!forceDynamic || next.revalidate !== 0) { + staticGenerationStore.revalidate = next.revalidate } - if (hasNextConfig) delete init.next } + + if (hasNextConfig) delete init.next } return doOriginalFetch(false, cacheReasonOverride).finally(handleUnlock) diff --git a/packages/next/src/server/web/spec-extension/unstable-cache.ts b/packages/next/src/server/web/spec-extension/unstable-cache.ts index 3f6c1e9ca3f8f..e58f6101c6376 100644 --- a/packages/next/src/server/web/spec-extension/unstable-cache.ts +++ b/packages/next/src/server/web/spec-extension/unstable-cache.ts @@ -77,7 +77,7 @@ export function unstable_cache( } } } - const implicitTags = addImplicitTags(store) + const implicitTags = store ? addImplicitTags(store) : [] const cacheKey = await incrementalCache?.fetchCacheKey(joinedKey) const cacheEntry = From 2b16a745ae2845d183aa3faf812565048800eada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Tue, 24 Oct 2023 00:29:24 -0700 Subject: [PATCH 07/15] feat: Add `__nextjs_pure` back (#57328) ### What? Add a magic to next-swc, agina. ### Why? It's required, but it causes some issues with build process. ### How? Closes WEB-1838 --- .../crates/core/src/import_analyzer.rs | 104 ++++++++++++++++++ packages/next-swc/crates/core/src/lib.rs | 5 +- packages/next-swc/crates/core/src/pure.rs | 88 +++++++++++++++ .../next-swc/crates/core/tests/fixture.rs | 21 ++++ .../tests/fixture/pure/no-name-clash/input.js | 3 + .../fixture/pure/no-name-clash/output.js | 2 + .../core/tests/fixture/pure/simple/input.js | 3 + .../core/tests/fixture/pure/simple/output.js | 2 + packages/next/src/build/swc/helpers.ts | 3 + 9 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 packages/next-swc/crates/core/src/import_analyzer.rs create mode 100644 packages/next-swc/crates/core/src/pure.rs create mode 100644 packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/input.js create mode 100644 packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/output.js create mode 100644 packages/next-swc/crates/core/tests/fixture/pure/simple/input.js create mode 100644 packages/next-swc/crates/core/tests/fixture/pure/simple/output.js create mode 100644 packages/next/src/build/swc/helpers.ts diff --git a/packages/next-swc/crates/core/src/import_analyzer.rs b/packages/next-swc/crates/core/src/import_analyzer.rs new file mode 100644 index 0000000000000..887bf16ce77b7 --- /dev/null +++ b/packages/next-swc/crates/core/src/import_analyzer.rs @@ -0,0 +1,104 @@ +use turbopack_binding::swc::core::{ + atoms::JsWord, + common::collections::{AHashMap, AHashSet}, + ecma::{ + ast::{ + Expr, Id, ImportDecl, ImportNamedSpecifier, ImportSpecifier, MemberExpr, MemberProp, + Module, ModuleExportName, + }, + visit::{noop_visit_type, Visit, VisitWith}, + }, +}; + +#[derive(Debug, Default)] +pub(crate) struct ImportMap { + /// Map from module name to (module path, exported symbol) + imports: AHashMap, + + namespace_imports: AHashMap, + + imported_modules: AHashSet, +} + +#[allow(unused)] +impl ImportMap { + pub fn is_module_imported(&mut self, module: &JsWord) -> bool { + self.imported_modules.contains(module) + } + + /// Returns true if `e` is an import of `orig_name` from `module`. + pub fn is_import(&self, e: &Expr, module: &str, orig_name: &str) -> bool { + match e { + Expr::Ident(i) => { + if let Some((i_src, i_sym)) = self.imports.get(&i.to_id()) { + i_src == module && i_sym == orig_name + } else { + false + } + } + + Expr::Member(MemberExpr { + obj: box Expr::Ident(obj), + prop: MemberProp::Ident(prop), + .. + }) => { + if let Some(obj_src) = self.namespace_imports.get(&obj.to_id()) { + obj_src == module && prop.sym == *orig_name + } else { + false + } + } + + _ => false, + } + } + + pub fn analyze(m: &Module) -> Self { + let mut data = ImportMap::default(); + + m.visit_with(&mut Analyzer { data: &mut data }); + + data + } +} + +struct Analyzer<'a> { + data: &'a mut ImportMap, +} + +impl Visit for Analyzer<'_> { + noop_visit_type!(); + + fn visit_import_decl(&mut self, import: &ImportDecl) { + self.data.imported_modules.insert(import.src.value.clone()); + + for s in &import.specifiers { + let (local, orig_sym) = match s { + ImportSpecifier::Named(ImportNamedSpecifier { + local, imported, .. + }) => match imported { + Some(imported) => (local.to_id(), orig_name(imported)), + _ => (local.to_id(), local.sym.clone()), + }, + ImportSpecifier::Default(s) => (s.local.to_id(), "default".into()), + ImportSpecifier::Namespace(s) => { + self.data + .namespace_imports + .insert(s.local.to_id(), import.src.value.clone()); + continue; + } + }; + + self.data + .imports + .insert(local, (import.src.value.clone(), orig_sym)); + } + } +} + +fn orig_name(n: &ModuleExportName) -> JsWord { + match n { + ModuleExportName::Ident(v) => v.sym.clone(), + ModuleExportName::Str(v) => v.value.clone(), + } +} diff --git a/packages/next-swc/crates/core/src/lib.rs b/packages/next-swc/crates/core/src/lib.rs index f810a4ceb0eb9..557b1a3f43256 100644 --- a/packages/next-swc/crates/core/src/lib.rs +++ b/packages/next-swc/crates/core/src/lib.rs @@ -58,11 +58,13 @@ pub mod amp_attributes; mod auto_cjs; pub mod cjs_optimizer; pub mod disallow_re_export_all_in_page; +mod import_analyzer; pub mod named_import_transform; pub mod next_ssg; pub mod optimize_barrel; pub mod optimize_server_react; pub mod page_config; +pub mod pure; pub mod react_server_components; pub mod server_actions; pub mod shake_exports; @@ -310,7 +312,7 @@ where Some(config) => Either::Left(server_actions::server_actions( &file.name, config.clone(), - comments, + comments.clone(), )), None => Either::Right(noop()), }, @@ -320,6 +322,7 @@ where }, None => Either::Right(noop()), }, + pure::pure_magic(comments), ) } diff --git a/packages/next-swc/crates/core/src/pure.rs b/packages/next-swc/crates/core/src/pure.rs new file mode 100644 index 0000000000000..877b0a00139cd --- /dev/null +++ b/packages/next-swc/crates/core/src/pure.rs @@ -0,0 +1,88 @@ +use turbopack_binding::swc::core::{ + common::{comments::Comments, errors::HANDLER, util::take::Take, Span, Spanned, DUMMY_SP}, + ecma::{ + ast::{CallExpr, Callee, EmptyStmt, Expr, Module, ModuleDecl, ModuleItem, Stmt}, + visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith}, + }, +}; + +use crate::import_analyzer::ImportMap; + +pub fn pure_magic(comments: C) -> impl Fold +where + C: Comments, +{ + as_folder(PureTransform { + imports: Default::default(), + comments, + }) +} + +struct PureTransform +where + C: Comments, +{ + imports: ImportMap, + comments: C, +} + +const MODULE: &str = "next/dist/build/swc/helpers"; +const FN_NAME: &str = "__nextjs_pure"; + +impl VisitMut for PureTransform +where + C: Comments, +{ + fn visit_mut_expr(&mut self, e: &mut Expr) { + e.visit_mut_children_with(self); + + if let Expr::Call(CallExpr { + span, + callee: Callee::Expr(callee), + args, + .. + }) = e + { + if !self.imports.is_import(callee, MODULE, FN_NAME) { + return; + } + + if args.len() != 1 { + HANDLER.with(|handler| { + handler + .struct_span_err(*span, "markAsPure() does not support multiple arguments") + .emit(); + }); + return; + } + + *e = *args[0].expr.take(); + + let mut lo = e.span().lo; + if lo.is_dummy() { + lo = Span::dummy_with_cmt().lo; + } + + self.comments.add_pure_comment(lo); + } + } + + fn visit_mut_module(&mut self, m: &mut Module) { + self.imports = ImportMap::analyze(m); + + m.visit_mut_children_with(self); + } + + fn visit_mut_module_item(&mut self, m: &mut ModuleItem) { + if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = m { + if import.src.value == MODULE { + *m = ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })); + return; + } + } + + m.visit_mut_children_with(self); + } + + noop_visit_mut_type!(); +} diff --git a/packages/next-swc/crates/core/tests/fixture.rs b/packages/next-swc/crates/core/tests/fixture.rs index 61177ccf0f75c..9e8c9296e96be 100644 --- a/packages/next-swc/crates/core/tests/fixture.rs +++ b/packages/next-swc/crates/core/tests/fixture.rs @@ -8,6 +8,7 @@ use next_swc::{ optimize_barrel::optimize_barrel, optimize_server_react::optimize_server_react, page_config::page_config_test, + pure::pure_magic, react_server_components::server_components, server_actions::{ server_actions, {self}, @@ -571,3 +572,23 @@ where { serde_json::from_str(s).expect("failed to deserialize") } + +#[fixture("tests/fixture/pure/**/input.js")] +fn pure(input: PathBuf) { + let output = input.parent().unwrap().join("output.js"); + test_fixture( + syntax(), + &|tr| { + let unresolved_mark = Mark::new(); + let top_level_mark = Mark::new(); + + chain!( + resolver(unresolved_mark, top_level_mark, false), + pure_magic(tr.comments.clone()) + ) + }, + &input, + &output, + Default::default(), + ); +} diff --git a/packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/input.js b/packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/input.js new file mode 100644 index 0000000000000..747b0d7e6ac2e --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/input.js @@ -0,0 +1,3 @@ +import { __nextjs_pure } from 'not-next-magic' + +__nextjs_pure(console.log('test!')) diff --git a/packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/output.js b/packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/output.js new file mode 100644 index 0000000000000..33c9697daf622 --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/pure/no-name-clash/output.js @@ -0,0 +1,2 @@ +import { __nextjs_pure } from 'not-next-magic'; +__nextjs_pure(console.log("test!")); diff --git a/packages/next-swc/crates/core/tests/fixture/pure/simple/input.js b/packages/next-swc/crates/core/tests/fixture/pure/simple/input.js new file mode 100644 index 0000000000000..ef9d6ffcb14ad --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/pure/simple/input.js @@ -0,0 +1,3 @@ +import { __nextjs_pure } from 'next/dist/build/swc/helpers' + +__nextjs_pure(console.log('test!')) diff --git a/packages/next-swc/crates/core/tests/fixture/pure/simple/output.js b/packages/next-swc/crates/core/tests/fixture/pure/simple/output.js new file mode 100644 index 0000000000000..2a10791f7f85f --- /dev/null +++ b/packages/next-swc/crates/core/tests/fixture/pure/simple/output.js @@ -0,0 +1,2 @@ +; +/*#__PURE__*/ console.log("test!"); diff --git a/packages/next/src/build/swc/helpers.ts b/packages/next/src/build/swc/helpers.ts new file mode 100644 index 0000000000000..bea1cf7eae167 --- /dev/null +++ b/packages/next/src/build/swc/helpers.ts @@ -0,0 +1,3 @@ +export function __nextjs_pure(expr: T): T { + return expr +} From 5428e5a7a1d8ec5758aa852e0b8a1e04be261f4a Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 24 Oct 2023 00:43:34 -0700 Subject: [PATCH 08/15] Fix trace ignores (#57331) Fixes some of the ignores that were moved around in https://github.com/vercel/next.js/pull/57280 x-ref: https://github.com/vercel/vercel/actions/runs/6620930801/job/17989126942?pr=10756 --- packages/next/src/build/collect-build-traces.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next/src/build/collect-build-traces.ts b/packages/next/src/build/collect-build-traces.ts index f8e1be4e96fd1..9b50e7d23abd0 100644 --- a/packages/next/src/build/collect-build-traces.ts +++ b/packages/next/src/build/collect-build-traces.ts @@ -268,16 +268,16 @@ export async function collectBuildTraces({ isStandalone ? null : '**/next/dist/compiled/jest-worker/**/*', '**/next/dist/compiled/webpack/(bundle4|bundle5).js', '**/node_modules/webpack5/**/*', - '**/next/dist/server/lib/squoosh/**/*.wasm', '**/next/dist/server/lib/route-resolver*', 'next/dist/compiled/@next/react-dev-overlay/dist/**/*', 'next/dist/compiled/semver/semver/**/*.js', - '**/next/dist/pages/**/*', + ...(ciEnvironment.hasNextSupport ? [ // only ignore image-optimizer code when // this is being handled outside of next-server '**/next/dist/server/image-optimizer.js', + '**/next/dist/server/lib/squoosh/**/*.wasm', ] : []), @@ -294,7 +294,7 @@ export async function collectBuildTraces({ ...sharedIgnores, '**/*.d.ts', '**/*.map', - + '**/next/dist/pages/**/*', ...(ciEnvironment.hasNextSupport ? ['**/node_modules/sharp/**/*'] : []), ].filter(nonNullable) From 2fc2a52652f64fcabb0ff1038f7fd286f291dd46 Mon Sep 17 00:00:00 2001 From: vercel-release-bot Date: Tue, 24 Oct 2023 08:22:51 +0000 Subject: [PATCH 09/15] v13.5.7-canary.23 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- packages/eslint-config-next/package.json | 4 ++-- packages/eslint-plugin-next/package.json | 2 +- packages/font/package.json | 2 +- packages/next-bundle-analyzer/package.json | 2 +- packages/next-codemod/package.json | 2 +- packages/next-env/package.json | 2 +- packages/next-mdx/package.json | 2 +- packages/next-plugin-storybook/package.json | 2 +- packages/next-polyfill-module/package.json | 2 +- packages/next-polyfill-nomodule/package.json | 2 +- packages/next-swc/package.json | 2 +- packages/next/package.json | 14 +++++++------- packages/react-dev-overlay/package.json | 2 +- packages/react-refresh-utils/package.json | 2 +- packages/third-parties/package.json | 4 ++-- pnpm-lock.yaml | 16 ++++++++-------- 18 files changed, 33 insertions(+), 33 deletions(-) diff --git a/lerna.json b/lerna.json index 94c8ebea80cf4..33301fda3e099 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "13.5.7-canary.22" + "version": "13.5.7-canary.23" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 8fe25402c4d10..1fb059e004d0e 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.7-canary.22", + "version": "13.5.7-canary.23", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 43e6c5618de9f..2ca749a42d2ab 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.7-canary.22", + "version": "13.5.7-canary.23", "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.7-canary.22", + "@next/eslint-plugin-next": "13.5.7-canary.23", "@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 e2295f0596123..1498e57ed6887 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.7-canary.22", + "version": "13.5.7-canary.23", "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 8c4b157f3fcdb..01654c0feed90 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "13.5.7-canary.22", + "version": "13.5.7-canary.23", "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 062e43d4c210e..4473dcf205408 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.7-canary.22", + "version": "13.5.7-canary.23", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 3878aa9fb1219..ab3940fd4db29 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "13.5.7-canary.22", + "version": "13.5.7-canary.23", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index bfbda21bee9a0..0ad731b15a608 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "13.5.7-canary.22", + "version": "13.5.7-canary.23", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index efb74d4690da1..f1315995c1ebd 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "13.5.7-canary.22", + "version": "13.5.7-canary.23", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 498d2dfcadcfc..c5fceee52ec83 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.7-canary.22", + "version": "13.5.7-canary.23", "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 04301d76592ca..9c0d284fe3f7b 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.7-canary.22", + "version": "13.5.7-canary.23", "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 1b0f0d3de4eb7..d5c802ea85743 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.7-canary.22", + "version": "13.5.7-canary.23", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index a7fd47b62a712..0d9f85803cbd8 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "13.5.7-canary.22", + "version": "13.5.7-canary.23", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", diff --git a/packages/next/package.json b/packages/next/package.json index 7b080a40db3cc..cca63aaa54e08 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "13.5.7-canary.22", + "version": "13.5.7-canary.23", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -92,7 +92,7 @@ ] }, "dependencies": { - "@next/env": "13.5.7-canary.22", + "@next/env": "13.5.7-canary.23", "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", @@ -146,11 +146,11 @@ "@mswjs/interceptors": "0.23.0", "@napi-rs/cli": "2.16.2", "@napi-rs/triples": "1.1.0", - "@next/polyfill-module": "13.5.7-canary.22", - "@next/polyfill-nomodule": "13.5.7-canary.22", - "@next/react-dev-overlay": "13.5.7-canary.22", - "@next/react-refresh-utils": "13.5.7-canary.22", - "@next/swc": "13.5.7-canary.22", + "@next/polyfill-module": "13.5.7-canary.23", + "@next/polyfill-nomodule": "13.5.7-canary.23", + "@next/react-dev-overlay": "13.5.7-canary.23", + "@next/react-refresh-utils": "13.5.7-canary.23", + "@next/swc": "13.5.7-canary.23", "@opentelemetry/api": "1.4.1", "@playwright/test": "^1.35.1", "@taskr/clear": "1.1.0", diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index 4acc65ddd7c36..57d954c6c5404 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.7-canary.22", + "version": "13.5.7-canary.23", "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 6baac79718f8f..842546c9544b4 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.7-canary.22", + "version": "13.5.7-canary.23", "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 288bdf1ac2a7a..0c390f4678084 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "13.5.7-canary.22", + "version": "13.5.7-canary.23", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -22,7 +22,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "13.5.7-canary.22", + "next": "13.5.7-canary.23", "outdent": "0.8.0", "prettier": "2.5.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36ebbd6354179..9e428a95cc159 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -735,7 +735,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 13.5.7-canary.22 + specifier: 13.5.7-canary.23 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.3.3 @@ -796,7 +796,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 13.5.7-canary.22 + specifier: 13.5.7-canary.23 version: link:../next-env '@swc/helpers': specifier: 0.5.2 @@ -920,19 +920,19 @@ importers: specifier: 1.1.0 version: 1.1.0 '@next/polyfill-module': - specifier: 13.5.7-canary.22 + specifier: 13.5.7-canary.23 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 13.5.7-canary.22 + specifier: 13.5.7-canary.23 version: link:../next-polyfill-nomodule '@next/react-dev-overlay': - specifier: 13.5.7-canary.22 + specifier: 13.5.7-canary.23 version: link:../react-dev-overlay '@next/react-refresh-utils': - specifier: 13.5.7-canary.22 + specifier: 13.5.7-canary.23 version: link:../react-refresh-utils '@next/swc': - specifier: 13.5.7-canary.22 + specifier: 13.5.7-canary.23 version: link:../next-swc '@opentelemetry/api': specifier: 1.4.1 @@ -1583,7 +1583,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 13.5.7-canary.22 + specifier: 13.5.7-canary.23 version: link:../next outdent: specifier: 0.8.0 From 06b9fb7bb82191ecbaae36896a228afac6fdbca7 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 24 Oct 2023 02:23:52 -0700 Subject: [PATCH 10/15] PPR Support (#57319) --- packages/next/src/server/base-server.ts | 363 ++++++++++++------ .../normalizers/request/postponed.test.ts | 69 ++++ .../future/normalizers/request/postponed.ts | 26 ++ .../src/server/lib/router-utils/filesystem.ts | 20 +- .../server/lib/router-utils/resolve-routes.ts | 13 + packages/next/src/server/next-server.ts | 2 +- packages/next/src/server/request-meta.ts | 14 + .../next/src/server/response-cache/types.ts | 6 +- .../src/shared/lib/router/utils/app-paths.ts | 18 + 9 files changed, 410 insertions(+), 121 deletions(-) create mode 100644 packages/next/src/server/future/normalizers/request/postponed.test.ts create mode 100644 packages/next/src/server/future/normalizers/request/postponed.ts diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 9f20f4608d169..ce1d5f92b77d2 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -5,7 +5,11 @@ import type { LoadComponentsReturnType } from './load-components' import type { MiddlewareRouteMatch } from '../shared/lib/router/utils/middleware-route-matcher' import type { Params } from '../shared/lib/router/utils/route-matcher' import type { NextConfig, NextConfigComplete } from './config-shared' -import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta' +import type { + NextParsedUrlQuery, + NextUrlWithParsedQuery, + RequestMeta, +} from './request-meta' import type { ParsedUrlQuery } from 'querystring' import type { RenderOptsPartial as PagesRenderOptsPartial } from './render' import type { RenderOptsPartial as AppRenderOptsPartial } from './app-render/types' @@ -19,7 +23,7 @@ import { } from '../shared/lib/utils' import type { PreviewData, ServerRuntime, SizeLimit } from 'next/types' import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' -import type { OutgoingHttpHeaders } from 'http2' +import type { OutgoingHttpHeaders } from 'http' import type { BaseNextRequest, BaseNextResponse } from './base-http' import type { ManifestRewriteRoute, @@ -70,6 +74,7 @@ import { addRequestMeta, getRequestMeta, removeRequestMeta, + setRequestMeta, } from './request-meta' import { removePathPrefix } from '../shared/lib/router/utils/remove-path-prefix' import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' @@ -121,6 +126,7 @@ import { matchNextDataPathname } from './lib/match-next-data-pathname' import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-asset-path' import { stripInternalHeaders } from './internal-utils' import { RSCPathnameNormalizer } from './future/normalizers/request/rsc' +import { PostponedPathnameNormalizer } from './future/normalizers/request/postponed' export type FindComponentsResult = { components: LoadComponentsReturnType @@ -255,7 +261,7 @@ export interface BaseRequestHandler { req: BaseNextRequest, res: BaseNextResponse, parsedUrl?: NextUrlWithParsedQuery | undefined - ): Promise + ): Promise | void } export type RequestContext = { @@ -386,6 +392,7 @@ export default abstract class Server { protected readonly localeNormalizer?: LocaleRouteNormalizer protected readonly normalizers: { + readonly postponed: PostponedPathnameNormalizer readonly rsc: RSCPathnameNormalizer } @@ -453,6 +460,9 @@ export default abstract class Server { this.hasAppDir = this.getHasAppDir(dev) this.normalizers = { + postponed: new PostponedPathnameNormalizer( + this.hasAppDir && this.nextConfig.experimental.ppr + ), rsc: new RSCPathnameNormalizer(this.hasAppDir), } @@ -549,7 +559,6 @@ export default abstract class Server { true ) - // Update the URL. if (req.url) { const parsed = parseUrl(req.url) parsed.pathname = parsedUrl.pathname @@ -557,6 +566,40 @@ export default abstract class Server { } } + protected handleNextPostponedRequest: RouteHandler = async ( + req, + _res, + parsedUrl + ) => { + if ( + !parsedUrl.pathname || + req.method !== 'POST' || + !this.normalizers.postponed.match(parsedUrl.pathname) + ) { + return + } + + parsedUrl.pathname = this.normalizers.postponed.normalize( + parsedUrl.pathname, + true + ) + + if (req.url) { + const parsed = parseUrl(req.url) + parsed.pathname = parsedUrl.pathname + req.url = formatUrl(parsed) + } + + // Read the body in chunks. If it errors here it's because the chunks + // being decoded are not strings. + let postponed = '' + for await (const chunk of req.body) { + postponed += chunk + } + + addRequestMeta(req, 'postponed', postponed) + } + private handleNextDataRequest: RouteHandler = async (req, res, parsedUrl) => { const middleware = this.getMiddleware() const params = matchNextDataPathname(parsedUrl.pathname) @@ -840,6 +883,9 @@ export default abstract class Server { let finished = await this.handleRSCRequest(req, res, parsedUrl) if (finished) return + finished = await this.handleNextPostponedRequest(req, res, parsedUrl) + if (finished) return + if (this.minimalMode && req.headers['x-now-route-matches']) { for (const param of FLIGHT_PARAMETERS) { delete req.headers[param.toString().toLowerCase()] @@ -891,6 +937,11 @@ export default abstract class Server { if (this.normalizers.rsc.match(matchedPath)) { matchedPath = this.normalizers.rsc.normalize(matchedPath, true) + } else if (this.normalizers.postponed.match(matchedPath)) { + matchedPath = this.normalizers.postponed.normalize( + matchedPath, + true + ) } let urlPathname = new URL(req.url, 'http://localhost').pathname @@ -1326,6 +1377,21 @@ export default abstract class Server { if (this.minimalMode && this.hasAppDir) { finished = await this.handleRSCRequest(req, res, url) if (finished) return true + + if (this.nextConfig.experimental.ppr) { + finished = await this.handleNextPostponedRequest(req, res, url) + if (finished) return true + } + } + } + + /** + * @internal - this method is internal to Next.js and should not be used directly by end-users + */ + public getRequestHandlerWithMetadata(meta: RequestMeta): BaseRequestHandler { + return (req, res, parsedUrl) => { + setRequestMeta(req, meta) + return this.handleRequest(req, res, parsedUrl) } } @@ -1737,6 +1803,17 @@ export default abstract class Server { // Don't delete headers[RSC] yet, it still needs to be used in renderToHTML later const isFlightRequest = Boolean(req.headers[RSC.toLowerCase()]) + // If we're in minimal mode, then try to get the postponed information from + // the request metadata. If available, use it for resuming the postponed + // render. + let resumed: { postponed: string } | null = null + if (this.minimalMode) { + const postponed = getRequestMeta(req, 'postponed') + if (postponed) { + resumed = { postponed } + } + } + // For pages we need to ensure the correct Vary header is set too, to avoid // caching issues when navigating between pages and app if (!isAppPath && isFlightRequest) { @@ -1754,11 +1831,11 @@ export default abstract class Server { res.statusCode = parseInt(pathname.slice(1), 10) } - // static pages can only respond to GET/HEAD - // requests so ensure we respond with 405 for - // invalid requests if ( + // Server actions can use non-GET/HEAD methods. !isServerAction && + // Resume can use non-GET/HEAD methods. + !resumed && !is404Page && !is500Page && pathname !== '/_error' && @@ -1912,14 +1989,20 @@ export default abstract class Server { urlPathname = this.stripNextDataPath(urlPathname) } - let ssgCacheKey = - isPreviewMode || !isSSG || opts.supportsDynamicHTML || isServerAction - ? null // Preview mode, on-demand revalidate, server actions, flight request can bypass the cache - : `${locale ? `/${locale}` : ''}${ - (pathname === '/' || resolvedUrlPathname === '/') && locale - ? '' - : resolvedUrlPathname - }${query.amp ? '.amp' : ''}` + let ssgCacheKey: string | null = null + if ( + !isPreviewMode && + isSSG && + !opts.supportsDynamicHTML && + !isServerAction && + !resumed + ) { + ssgCacheKey = `${locale ? `/${locale}` : ''}${ + (pathname === '/' || resolvedUrlPathname === '/') && locale + ? '' + : resolvedUrlPathname + }${query.amp ? '.amp' : ''}` + } if ((is404Page || is500Page) && isSSG) { ssgCacheKey = `${locale ? `/${locale}` : ''}${pathname}${ @@ -1972,9 +2055,11 @@ export default abstract class Server { | 'https', }) - const doRender: ( - postponed?: string - ) => Promise = async (postponed?: string) => { + type Renderer = ( + postponed: string | undefined + ) => Promise + + const doRender: Renderer = async (postponed) => { // In development, we always want to generate dynamic HTML. const supportsDynamicHTML = (!isDataReq && opts.dev) || !(isSSG || hasStaticPaths) || !!postponed @@ -2265,7 +2350,12 @@ export default abstract class Server { const cacheEntry = await this.responseCache.get( ssgCacheKey, - async (hasResolved, hadCache): Promise => { + async ( + hasResolved, + previousCacheEntry + ): Promise => { + // If this is a resume request, get the postponed. + const postponed = resumed ? resumed.postponed : undefined const isProduction = !this.renderOpts.dev const didRespond = hasResolved || res.sent @@ -2292,20 +2382,23 @@ export default abstract class Server { if ( isOnDemandRevalidate && revalidateOnlyGenerated && - !hadCache && + !previousCacheEntry && !this.minimalMode ) { await this.render404(req, res) return null } - if (hadCache?.isStale === -1) { + if (previousCacheEntry?.isStale === -1) { isOnDemandRevalidate = true } // only allow on-demand revalidate for fallback: true/blocking // or for prerendered fallback: false paths - if (isOnDemandRevalidate && (fallbackMode !== false || hadCache)) { + if ( + isOnDemandRevalidate && + (fallbackMode !== false || previousCacheEntry) + ) { fallbackMode = 'blocking' } @@ -2379,7 +2472,10 @@ export default abstract class Server { // We need to generate the fallback on-demand for development. else { query.__nextFallback = 'true' - const result = await doRender() + + // We pass `undefined` as there cannot be a postponed state in + // development. + const result = await doRender(undefined) if (!result) { return null } @@ -2390,7 +2486,7 @@ export default abstract class Server { } } - const result = await doRender() + const result = await doRender(postponed) if (!result) { return null } @@ -2439,6 +2535,11 @@ export default abstract class Server { const { value: cachedData } = cacheEntry + // If the cache value is an image, we should error early. + if (cachedData?.kind === 'IMAGE') { + throw new Error('invariant SSG should not return an image cache value') + } + // Coerce the revalidate parameter from the render. let revalidate: Revalidate | undefined if ( @@ -2465,6 +2566,17 @@ export default abstract class Server { } cacheEntry.revalidate = revalidate + // If there's a callback for `onCacheEntry`, call it with the cache entry + // and the revalidate options. + const onCacheEntry = getRequestMeta(req, 'onCacheEntry') + if (onCacheEntry) { + const finished = await onCacheEntry(cacheEntry) + if (finished) { + // TODO: maybe we have to end the request? + return null + } + } + if (!cachedData) { if (cacheEntry.revalidate) { res.setHeader('Cache-Control', formatRevalidate(cacheEntry.revalidate)) @@ -2473,13 +2585,14 @@ export default abstract class Server { res.statusCode = 404 res.body('{"notFound":true}').send() return null - } else { - if (this.renderOpts.dev) { - query.__nextNotFoundSrcPage = pathname - } - await this.render404(req, res, { pathname, query }, false) - return null } + + if (this.renderOpts.dev) { + query.__nextNotFoundSrcPage = pathname + } + + await this.render404(req, res, { pathname, query }, false) + return null } else if (cachedData.kind === 'REDIRECT') { if (cacheEntry.revalidate) { res.setHeader('Cache-Control', formatRevalidate(cacheEntry.revalidate)) @@ -2498,8 +2611,6 @@ export default abstract class Server { await handleRedirect(cachedData.props) return null } - } else if (cachedData.kind === 'IMAGE') { - throw new Error('invariant SSG should not return an image cache value') } else if (cachedData.kind === 'ROUTE') { const headers = { ...cachedData.headers } @@ -2516,97 +2627,85 @@ export default abstract class Server { }) ) return null - } else { - if (isAppPath) { - if ( - this.minimalMode && - isSSG && - cachedData.headers?.[NEXT_CACHE_TAGS_HEADER] - ) { - res.setHeader( - NEXT_CACHE_TAGS_HEADER, - cachedData.headers[NEXT_CACHE_TAGS_HEADER] as string - ) - } - if (isDataReq && typeof cachedData.pageData !== 'string') { - throw new Error( - 'invariant: Expected pageData to be a string for app data request but received ' + - typeof cachedData.pageData + - '. This is a bug in Next.js.' - ) - } + } else if (isAppPath) { + // If the request has a postponed state and it's a resume request we + // should error. + if (cachedData.postponed && resumed) { + throw new Error( + 'Invariant: postponed state should not be present on a resume request' + ) + } - if (cachedData.status) { - res.statusCode = cachedData.status - } + if ( + this.minimalMode && + isSSG && + cachedData.headers?.[NEXT_CACHE_TAGS_HEADER] + ) { + res.setHeader( + NEXT_CACHE_TAGS_HEADER, + cachedData.headers[NEXT_CACHE_TAGS_HEADER] as string + ) + } + if (isDataReq && typeof cachedData.pageData !== 'string') { + throw new Error( + 'invariant: Expected pageData to be a string for app data request but received ' + + typeof cachedData.pageData + + '. This is a bug in Next.js.' + ) + } - // Mark that the request did postpone if this is a data request or we're - // testing. It's used to verify that we're actually serving a postponed - // request so we can trust the cache headers. - if ( - cachedData.postponed && - (isDataReq || process.env.__NEXT_TEST_MODE) - ) { - res.setHeader(NEXT_DID_POSTPONE_HEADER, '1') - } + if (cachedData.status) { + res.statusCode = cachedData.status + } - if (isDataReq) { - if (cachedData.postponed && !isAppPrefetch) { - const result = await doRender(cachedData.postponed) - if (!result) { - return null - } + // Mark that the request did postpone if this is a data request or we're + // testing. It's used to verify that we're actually serving a postponed + // request so we can trust the cache headers. + if (cachedData.postponed && (isDataReq || process.env.__NEXT_TEST_MODE)) { + res.setHeader(NEXT_DID_POSTPONE_HEADER, '1') + } - if (result.value?.kind !== 'PAGE') { - throw new Error('Invariant: Expected a page response') - } + if (isDataReq) { + // If this isn't a prefetch and this isn't a resume request, we want to + // respond with the dynamic flight data. In the case that this is a + // resume request the page data will already be dynamic. + if (!isAppPrefetch && !resumed) { + const result = await doRender(cachedData.postponed) + if (!result) { + return null + } - if (!result.value.pageData) { - throw new Error('Invariant: Expected pageData to be defined') - } + if (result.value?.kind !== 'PAGE') { + throw new Error('Invariant: Expected a page response') + } - return { - type: 'rsc', - body: RenderResult.fromStatic(result.value.pageData as string), - revalidate: cacheEntry.revalidate, - } + if (!result.value.pageData) { + throw new Error('Invariant: Expected pageData to be present') } return { type: 'rsc', - body: RenderResult.fromStatic(cachedData.pageData as string), + body: RenderResult.fromStatic(result.value.pageData as string), revalidate: cacheEntry.revalidate, } } - let body = cachedData.html - - // If the request has a postponed state, let's try and resume it... - if (cachedData.postponed) { - const transformer = new TransformStream() - - // Perform the second render, now with the postponed state. - doRender(cachedData.postponed) - .then(async (result) => { - if (!result) { - throw new Error('Invariant: Expected a result to be returned') - } - - if (result.value?.kind !== 'PAGE') { - throw new Error('Invariant: Expected a page response') - } - - // Pipe the resume result to the transformer. - await result.value.html.pipeTo(transformer.writable) - }) - .catch((err) => { - console.error('Error while resuming postponed state', err) - }) - - // Chain this new transformer readable to the existing body. - body.chain(transformer.readable) + // As this isn't a prefetch request, we should serve the static flight + // data. + return { + type: 'rsc', + body: RenderResult.fromStatic(cachedData.pageData as string), + revalidate: cacheEntry.revalidate, } + } + + // This is a request for HTML data. + let body = cachedData.html + // If there's no postponed state, we should just serve the HTML. This + // should also be the case for a resume request because it's completed + // as a server render (rather than a static render). + if (!cachedData.postponed) { return { type: 'html', body, @@ -2614,11 +2713,53 @@ export default abstract class Server { } } + // This request has postponed, so let's create a new transformer that the + // dynamic data can pipe to that will attach the dynamic data to the end + // of the response. + const transformer = new TransformStream() + body.chain(transformer.readable) + + // Perform the render again, but this time, provide the postponed state. + // We don't await because we want the result to start streaming now, and + // we've already chained the transformer's readable to the render result. + doRender(cachedData.postponed) + .then(async (result) => { + if (!result) { + throw new Error('Invariant: expected a result to be returned') + } + + if (result.value?.kind !== 'PAGE') { + throw new Error( + `Invariant: expected a page response, got ${result.value?.kind}` + ) + } + + // Pipe the resume result to the transformer. + await result.value.html.pipeTo(transformer.writable) + }) + .catch((err) => { + // An error occurred during piping or preparing the render, abort + // the transformers writer so we can terminate the stream. + transformer.writable.abort(err).catch((e) => { + console.error("couldn't abort transformer", e) + }) + }) + return { - type: isDataReq ? 'json' : 'html', - body: isDataReq - ? RenderResult.fromStatic(JSON.stringify(cachedData.pageData)) - : cachedData.html, + type: 'html', + body, + revalidate: cacheEntry.revalidate, + } + } else if (isDataReq) { + return { + type: 'json', + body: RenderResult.fromStatic(JSON.stringify(cachedData.pageData)), + revalidate: cacheEntry.revalidate, + } + } else { + return { + type: 'html', + body: cachedData.html, revalidate: cacheEntry.revalidate, } } diff --git a/packages/next/src/server/future/normalizers/request/postponed.test.ts b/packages/next/src/server/future/normalizers/request/postponed.test.ts new file mode 100644 index 0000000000000..c70d597771900 --- /dev/null +++ b/packages/next/src/server/future/normalizers/request/postponed.test.ts @@ -0,0 +1,69 @@ +import { PostponedPathnameNormalizer } from './postponed' + +describe('PostponedPathnameNormalizer', () => { + describe('match', () => { + it('should not match if it is disabled', () => { + const pathnames = [ + '/_next/postponed/foo', + '/_next/postponed/bar', + '/_next/postponed/baz', + ] + const normalizer = new PostponedPathnameNormalizer(false) + for (const pathname of pathnames) { + expect(normalizer.match(pathname)).toBe(false) + } + }) + + it('should match if it is enabled', () => { + const pathnames = [ + '/_next/postponed/foo', + '/_next/postponed/bar', + '/_next/postponed/baz', + ] + const normalizer = new PostponedPathnameNormalizer(true) + for (const pathname of pathnames) { + expect(normalizer.match(pathname)).toBe(true) + } + }) + + it('should not match for other pathnames', () => { + const pathnames = ['/_next/foo', '/_next/bar', '/_next/baz'] + const normalizer = new PostponedPathnameNormalizer(true) + for (const pathname of pathnames) { + expect(normalizer.match(pathname)).toBe(false) + } + }) + }) + + describe('normalize', () => { + it('should not normalize if it is disabled', () => { + const pathnames = [ + '/_next/postponed/foo', + '/_next/postponed/bar', + '/_next/postponed/baz', + ] + const normalizer = new PostponedPathnameNormalizer(false) + for (const pathname of pathnames) { + expect(normalizer.normalize(pathname)).toBe(pathname) + } + }) + + it('should not normalize if it is enabled but not matched', () => { + const pathnames = ['/_next/foo', '/_next/bar', '/_next/baz'] + const normalizer = new PostponedPathnameNormalizer(true) + for (const pathname of pathnames) { + expect(normalizer.normalize(pathname)).toBe(pathname) + } + }) + + it('should normalize if it is enabled and matched', () => { + const pathnames = ['/foo', '/bar', '/baz'] + const normalizer = new PostponedPathnameNormalizer(true) + for (const pathname of pathnames) { + expect(normalizer.normalize(`/_next/postponed${pathname}`, true)).toBe( + pathname + ) + } + }) + }) +}) diff --git a/packages/next/src/server/future/normalizers/request/postponed.ts b/packages/next/src/server/future/normalizers/request/postponed.ts new file mode 100644 index 0000000000000..179188ae02ad5 --- /dev/null +++ b/packages/next/src/server/future/normalizers/request/postponed.ts @@ -0,0 +1,26 @@ +import type { Normalizer } from '../normalizer' + +export class PostponedPathnameNormalizer implements Normalizer { + constructor(private readonly ppr: boolean | undefined) {} + + public match(pathname: string) { + // If PPR isn't enabled, we don't match. + if (!this.ppr) return false + + // If the pathname doesn't start with the prefix, we don't match. + if (!pathname.startsWith('/_next/postponed')) return false + + return true + } + + public normalize(pathname: string, matched?: boolean): string { + // If PPR isn't enabled, we don't need to normalize. + if (!this.ppr) return pathname + + // If we're not matched and we don't match, we don't need to normalize. + if (!matched && !this.match(pathname)) return pathname + + // Remove the prefix. + return pathname.substring('/_next/postponed'.length) || '/' + } +} diff --git a/packages/next/src/server/lib/router-utils/filesystem.ts b/packages/next/src/server/lib/router-utils/filesystem.ts index 05b97245eb5ce..1e262d90a59fd 100644 --- a/packages/next/src/server/lib/router-utils/filesystem.ts +++ b/packages/next/src/server/lib/router-utils/filesystem.ts @@ -26,9 +26,7 @@ import { getRouteMatcher } from '../../../shared/lib/router/utils/route-matcher' import { pathHasPrefix } from '../../../shared/lib/router/utils/path-has-prefix' import { normalizeLocalePath } from '../../../shared/lib/i18n/normalize-locale-path' import { removePathPrefix } from '../../../shared/lib/router/utils/remove-path-prefix' - import { getMiddlewareRouteMatcher } from '../../../shared/lib/router/utils/middleware-route-matcher' - import { APP_PATH_ROUTES_MANIFEST, BUILD_ID_FILE, @@ -40,6 +38,7 @@ import { import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-sep' import { normalizeMetadataRoute } from '../../../lib/metadata/get-metadata-route' import { RSCPathnameNormalizer } from '../../future/normalizers/request/rsc' +import { PostponedPathnameNormalizer } from '../../future/normalizers/request/postponed' export type FsOutput = { type: @@ -372,6 +371,7 @@ export async function setupFsCheck(opts: { // Because we can't know if the app directory is enabled or not at this // stage, we assume that it is. rsc: new RSCPathnameNormalizer(true), + postponed: new PostponedPathnameNormalizer(opts.config.experimental.ppr), } return { @@ -409,12 +409,6 @@ export async function setupFsCheck(opts: { return lruResult } - // Handle minimal mode case with .rsc output path (this is - // mostly for testing). - if (opts.minimalMode && normalizers.rsc.match(itemPath)) { - itemPath = normalizers.rsc.normalize(itemPath, true) - } - const { basePath } = opts.config if (basePath && !pathHasPrefix(itemPath, basePath)) { @@ -422,6 +416,16 @@ export async function setupFsCheck(opts: { } itemPath = removePathPrefix(itemPath, basePath) || '/' + // Simulate minimal mode requests by normalizing RSC and postponed + // requests. + if (opts.minimalMode) { + if (normalizers.rsc.match(itemPath)) { + itemPath = normalizers.rsc.normalize(itemPath, true) + } else if (normalizers.postponed.match(itemPath)) { + itemPath = normalizers.postponed.normalize(itemPath, true) + } + } + if (itemPath !== '/' && itemPath.endsWith('/')) { itemPath = itemPath.substring(0, itemPath.length - 1) } diff --git a/packages/next/src/server/lib/router-utils/resolve-routes.ts b/packages/next/src/server/lib/router-utils/resolve-routes.ts index 1cbaad8cbb97c..377ab17503982 100644 --- a/packages/next/src/server/lib/router-utils/resolve-routes.ts +++ b/packages/next/src/server/lib/router-utils/resolve-routes.ts @@ -29,6 +29,7 @@ import { normalizeLocalePath } from '../../../shared/lib/i18n/normalize-locale-p import { removePathPrefix } from '../../../shared/lib/router/utils/remove-path-prefix' import { NextDataPathnameNormalizer } from '../../future/normalizers/request/next-data' import { BasePathPathnameNormalizer } from '../../future/normalizers/request/base-path' +import { PostponedPathnameNormalizer } from '../../future/normalizers/request/postponed' import { addRequestMeta } from '../../request-meta' import { @@ -295,6 +296,9 @@ export function getResolveRoutes( const normalizers = { basePath: new BasePathPathnameNormalizer(config.basePath), data: new NextDataPathnameNormalizer(fsChecker.buildId), + postponed: new PostponedPathnameNormalizer( + config.experimental.ppr === true + ), } async function handleRoute( @@ -372,10 +376,19 @@ export function getResolveRoutes( normalized = normalizers.basePath.normalize(normalized, true) } + let updated = false if (normalizers.data.match(normalized)) { + updated = true parsedUrl.query.__nextDataReq = '1' normalized = normalizers.data.normalize(normalized, true) + } else if (normalizers.postponed.match(normalized)) { + updated = true + normalized = normalizers.postponed.normalize(normalized, true) + } + // If we updated the pathname, and it had a base path, re-add the + // base path. + if (updated) { if (hadBasePath) { normalized = path.posix.join(config.basePath, normalized) } diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 67a9e66d2446c..3b7861013cd63 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -122,7 +122,7 @@ export interface NodeRequestHandler { req: IncomingMessage | BaseNextRequest, res: ServerResponse | BaseNextResponse, parsedUrl?: NextUrlWithParsedQuery | undefined - ): Promise + ): Promise | void } const MiddlewareMatcherCache = new WeakMap< diff --git a/packages/next/src/server/request-meta.ts b/packages/next/src/server/request-meta.ts index c3c70c0a66a81..a4bfee782304e 100644 --- a/packages/next/src/server/request-meta.ts +++ b/packages/next/src/server/request-meta.ts @@ -67,6 +67,20 @@ export interface RequestMeta { * The incremental cache to use for the request. */ incrementalCache?: any + + /** + * Postponed state to use for resumption. If present it's assumed that the + * request is for a page that has postponed (there are no guarantees that the + * page actually has postponed though as it would incur an additional cache + * lookup). + */ + postponed?: string + + /** + * If provided, this will be called when a response cache entry was generated + * or looked up in the cache. + */ + onCacheEntry?: (cacheEntry: any) => Promise | boolean | void } /** diff --git a/packages/next/src/server/response-cache/types.ts b/packages/next/src/server/response-cache/types.ts index 0cefa921c3575..ee94046b0049a 100644 --- a/packages/next/src/server/response-cache/types.ts +++ b/packages/next/src/server/response-cache/types.ts @@ -102,9 +102,13 @@ export type ResponseCacheEntry = { isMiss?: boolean } +/** + * @param hasResolved whether the responseGenerator has resolved it's promise + * @param previousCacheEntry the previous cache entry if it exists or the current + */ export type ResponseGenerator = ( hasResolved: boolean, - cacheEntry?: IncrementalCacheItem + previousCacheEntry?: IncrementalCacheItem ) => Promise export type IncrementalCacheItem = { diff --git a/packages/next/src/shared/lib/router/utils/app-paths.ts b/packages/next/src/shared/lib/router/utils/app-paths.ts index fa7a80a4f6731..541fbdc398866 100644 --- a/packages/next/src/shared/lib/router/utils/app-paths.ts +++ b/packages/next/src/shared/lib/router/utils/app-paths.ts @@ -1,5 +1,6 @@ import { ensureLeadingSlash } from '../../page-path/ensure-leading-slash' import { isGroupSegment } from '../../segment' +import { parse, format } from 'url' /** * Normalizes an app route so it represents the actual request path. Essentially @@ -62,3 +63,20 @@ export function normalizeRscURL(url: string) { '$1' ) } + +/** + * Strips the `/_next/postponed` prefix if it's in the pathname. + * + * @param url the url to normalize + */ +export function normalizePostponedURL(url: string) { + const parsed = parse(url) + let { pathname } = parsed + if (pathname && pathname.startsWith('/_next/postponed')) { + pathname = pathname.substring('/_next/postponed'.length) || '/' + + return format({ ...parsed, pathname }) + } + + return url +} From 741a08bc2548c523b7af978b34bf0250c9714305 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 24 Oct 2023 07:18:11 -0700 Subject: [PATCH 11/15] fix: ensure generateStaticParams isn't required for PPR (#57333) --- packages/next/src/build/index.ts | 3 +++ test/e2e/app-dir/ppr/app/suspense/node/nested/[slug]/page.jsx | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 4d02c29bfaa5f..2f4fa5511b726 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -1604,6 +1604,9 @@ export default async function build( isPPR = workerResult.isPPR isSSG = true isStatic = true + + appStaticPaths.set(originalAppPath, []) + appStaticPathsEncoded.set(originalAppPath, []) } if ( diff --git a/test/e2e/app-dir/ppr/app/suspense/node/nested/[slug]/page.jsx b/test/e2e/app-dir/ppr/app/suspense/node/nested/[slug]/page.jsx index 4cf631f2e5596..b927b8017d796 100644 --- a/test/e2e/app-dir/ppr/app/suspense/node/nested/[slug]/page.jsx +++ b/test/e2e/app-dir/ppr/app/suspense/node/nested/[slug]/page.jsx @@ -1,5 +1 @@ export { SuspensePage as default } from '../../../../../components/page' - -export const generateStaticParams = async () => { - return [] -} From 63aa0fe8ac3e647e43042dc6b8f35f6e768a7429 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Tue, 24 Oct 2023 09:37:37 -0700 Subject: [PATCH 12/15] `taint` flag should enable experimental react in turbopack (#57315) Matches behavior here: https://github.com/vercel/next.js/blob/canary/packages/next/src/lib/needs-experimental-react.ts#L4 --- packages/next-swc/crates/next-api/src/app.rs | 8 ------- .../crates/next-api/src/server_actions.rs | 17 --------------- .../next-core/src/next_client/transforms.rs | 5 +---- .../crates/next-core/src/next_config.rs | 10 ++++----- .../crates/next-core/src/next_import_map.rs | 21 +++++++------------ .../next-core/src/next_server/transforms.rs | 9 ++------ 6 files changed, 15 insertions(+), 55 deletions(-) diff --git a/packages/next-swc/crates/next-api/src/app.rs b/packages/next-swc/crates/next-api/src/app.rs index 6e1af53a14ed4..eab5ef193d0e7 100644 --- a/packages/next-swc/crates/next-api/src/app.rs +++ b/packages/next-swc/crates/next-api/src/app.rs @@ -830,10 +830,6 @@ impl AppEndpoint { NextRuntime::Edge, Vc::upcast(this.app_project.edge_rsc_module_context()), Vc::upcast(chunking_context), - this.app_project - .project() - .next_config() - .enable_server_actions(), ) .await?; server_assets.push(manifest); @@ -984,10 +980,6 @@ impl AppEndpoint { NextRuntime::NodeJs, Vc::upcast(this.app_project.rsc_module_context()), Vc::upcast(this.app_project.project().server_chunking_context()), - this.app_project - .project() - .next_config() - .enable_server_actions(), ) .await?; server_assets.push(manifest); diff --git a/packages/next-swc/crates/next-api/src/server_actions.rs b/packages/next-swc/crates/next-api/src/server_actions.rs index 3ef3583dee0b5..412e99ff6104d 100644 --- a/packages/next-swc/crates/next-api/src/server_actions.rs +++ b/packages/next-swc/crates/next-api/src/server_actions.rs @@ -49,27 +49,10 @@ pub(crate) async fn create_server_actions_manifest( runtime: NextRuntime, asset_context: Vc>, chunking_context: Vc>, - enable_server_actions: Vc, ) -> Result<( Option>>, Vc>, )> { - // If actions aren't enabled, then there's no need to scan the module graph. We - // still need to generate an empty manifest so that the TS side can merge - // the manifest later on. - if !*enable_server_actions.await? { - let manifest = build_manifest( - node_root, - pathname, - page_name, - runtime, - ModuleActionMap::empty(), - Default::default(), - ) - .await?; - return Ok((None, manifest)); - } - let actions = get_actions(Vc::upcast(entry)); let loader = build_server_actions_loader(node_root, page_name, actions, asset_context).await?; let Some(evaluable) = Vc::try_resolve_sidecast::>(loader).await? diff --git a/packages/next-swc/crates/next-core/src/next_client/transforms.rs b/packages/next-swc/crates/next-core/src/next_client/transforms.rs index 15a07842fdf48..b387763cf01fb 100644 --- a/packages/next-swc/crates/next-core/src/next_client/transforms.rs +++ b/packages/next-swc/crates/next-core/src/next_client/transforms.rs @@ -24,7 +24,6 @@ pub async fn get_next_client_transforms_rules( let mut rules = vec![]; let modularize_imports_config = &next_config.await?.modularize_imports; - let enable_server_actions = *next_config.enable_server_actions().await?; if let Some(modularize_imports_config) = modularize_imports_config { rules.push(get_next_modularize_imports_rule(modularize_imports_config)); } @@ -39,9 +38,7 @@ pub async fn get_next_client_transforms_rules( Some(pages_dir) } ClientContextType::App { .. } => { - if enable_server_actions { - rules.push(get_server_actions_transform_rule(ActionsTransform::Client)); - } + rules.push(get_server_actions_transform_rule(ActionsTransform::Client)); None } ClientContextType::Fallback | ClientContextType::Other => None, diff --git a/packages/next-swc/crates/next-core/src/next_config.rs b/packages/next-swc/crates/next-core/src/next_config.rs index f7ed3693e1ad4..887baca13e79b 100644 --- a/packages/next-swc/crates/next-core/src/next_config.rs +++ b/packages/next-swc/crates/next-core/src/next_config.rs @@ -743,15 +743,13 @@ impl NextConfig { } #[turbo_tasks::function] - pub async fn enable_server_actions(self: Vc) -> Result> { - Ok(Vc::cell( - self.await?.experimental.server_actions.unwrap_or(false), - )) + pub async fn enable_ppr(self: Vc) -> Result> { + Ok(Vc::cell(self.await?.experimental.ppr.unwrap_or(false))) } #[turbo_tasks::function] - pub async fn enable_ppr(self: Vc) -> Result> { - Ok(Vc::cell(self.await?.experimental.ppr.unwrap_or(false))) + pub async fn enable_taint(self: Vc) -> Result> { + Ok(Vc::cell(self.await?.experimental.taint.unwrap_or(false))) } } 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 3dda1728e18fa..f25b6d6f097f1 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 @@ -92,13 +92,12 @@ pub async fn get_next_client_import_map( ); } ClientContextType::App { app_dir } => { - let react_flavor = if *next_config.enable_server_actions().await? - || *next_config.enable_ppr().await? - { - "-experimental" - } else { - "" - }; + let react_flavor = + if *next_config.enable_ppr().await? || *next_config.enable_taint().await? { + "-experimental" + } else { + "" + }; import_map.insert_exact_alias( "react", @@ -644,13 +643,9 @@ async fn rsc_aliases( runtime: NextRuntime, next_config: Vc, ) -> Result<()> { - let server_actions = *next_config.enable_server_actions().await?; let ppr = *next_config.enable_ppr().await?; - let react_channel = if server_actions || ppr { - "-experimental" - } else { - "" - }; + let taint = *next_config.enable_taint().await?; + let react_channel = if ppr || taint { "-experimental" } else { "" }; let mut alias = indexmap! { "react" => format!("next/dist/compiled/react{react_channel}"), diff --git a/packages/next-swc/crates/next-core/src/next_server/transforms.rs b/packages/next-swc/crates/next-core/src/next_server/transforms.rs index 63a58d59a8420..85533ca34ca26 100644 --- a/packages/next-swc/crates/next-core/src/next_server/transforms.rs +++ b/packages/next-swc/crates/next-core/src/next_server/transforms.rs @@ -25,7 +25,6 @@ pub async fn get_next_server_transforms_rules( let mut rules = vec![]; let modularize_imports_config = &next_config.await?.modularize_imports; - let enable_server_actions = *next_config.enable_server_actions().await?; if let Some(modularize_imports_config) = modularize_imports_config { rules.push(get_next_modularize_imports_rule(modularize_imports_config)); } @@ -40,17 +39,13 @@ pub async fn get_next_server_transforms_rules( (false, Some(pages_dir)) } ServerContextType::AppSSR { .. } => { - if enable_server_actions { - rules.push(get_server_actions_transform_rule(ActionsTransform::Server)); - } + rules.push(get_server_actions_transform_rule(ActionsTransform::Server)); (false, None) } ServerContextType::AppRSC { client_transition, .. } => { - if enable_server_actions { - rules.push(get_server_actions_transform_rule(ActionsTransform::Server)); - } + rules.push(get_server_actions_transform_rule(ActionsTransform::Server)); if let Some(client_transition) = client_transition { rules.push(get_next_css_client_reference_transforms_rule( client_transition, From ae10b5c82b29b3b077378f05f75eb1b215b327f0 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 24 Oct 2023 09:38:30 -0700 Subject: [PATCH 13/15] Fix app ISR error handling (#57332) This ensures when an error occurs during a revalidate in app router that properly throw the error fully and don't store the error page in the cache which matches the expected behavior for full route ISR as errors are not meant to update the cache so that the last successful cache entry can continue being served. Fix was tested against the provided reproduction here https://app-dir-example-ghl01cxtn-basement.vercel.app/ Fixes: https://github.com/vercel/next.js/issues/53195 --- .../src/client/components/error-boundary.tsx | 20 +++++ .../e2e/app-dir/app-static/app-static.test.ts | 87 ++++++++++++++----- .../app/isr-error-handling/error.txt | 1 + .../app-static/app/isr-error-handling/page.js | 23 +++++ 4 files changed, 108 insertions(+), 23 deletions(-) create mode 100644 test/e2e/app-dir/app-static/app/isr-error-handling/error.txt create mode 100644 test/e2e/app-dir/app-static/app/isr-error-handling/page.js diff --git a/packages/next/src/client/components/error-boundary.tsx b/packages/next/src/client/components/error-boundary.tsx index dc064a2f07c34..9cd2e6f9da742 100644 --- a/packages/next/src/client/components/error-boundary.tsx +++ b/packages/next/src/client/components/error-boundary.tsx @@ -43,6 +43,24 @@ interface ErrorBoundaryHandlerState { previousPathname: string } +// if we are revalidating we want to re-throw the error so the +// function crashes so we can maintain our previous cache +// instead of caching the error page +function HandleISRError({ error }: { error: any }) { + if (typeof (fetch as any).__nextGetStaticStore === 'function') { + const store: + | undefined + | import('./static-generation-async-storage.external').StaticGenerationStore = + (fetch as any).__nextGetStaticStore()?.getStore() + + if (store?.isRevalidate || store?.isStaticGeneration) { + console.error(error) + throw error + } + } + return null +} + export class ErrorBoundaryHandler extends React.Component< ErrorBoundaryHandlerProps, ErrorBoundaryHandlerState @@ -86,6 +104,7 @@ export class ErrorBoundaryHandler extends React.Component< if (this.state.error) { return ( <> + {this.props.errorStyles} +

diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index faf10fa70d32c..43265a67bd84d 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -488,36 +488,34 @@ createNextDescribe( expect(files.sort()).toEqual( [ - '_not-found.html', - '_not-found.js', - '_not-found.rsc', - '_not-found_client-reference-manifest.js', 'page.js', 'index.rsc', 'index.html', 'blog/seb.rsc', 'blog/tim.rsc', + '_not-found.js', 'blog/seb.html', 'blog/tim.html', + 'isr-error-handling.rsc', + '_not-found.rsc', + '_not-found.html', 'blog/styfle.rsc', 'force-cache.rsc', 'blog/styfle.html', 'force-cache.html', + 'isr-error-handling/page.js', 'ssg-draft-mode.rsc', 'ssr-forced/page.js', - 'stale-cache-serving-edge/app-page/page.js', - 'stale-cache-serving-edge/app-page/page_client-reference-manifest.js', - 'stale-cache-serving-edge/route-handler/route.js', - 'stale-cache-serving/app-page.prefetch.rsc', - 'stale-cache-serving/app-page/page.js', - 'stale-cache-serving/app-page/page_client-reference-manifest.js', - 'stale-cache-serving/route-handler/route.js', + 'articles/works.rsc', 'custom.prefetch.rsc', 'force-cache/page.js', 'ssg-draft-mode.html', + 'articles/works.html', + 'no-store/static.rsc', '(new)/custom/page.js', 'force-static/page.js', 'response-url/page.js', + 'no-store/static.html', 'blog/[author]/page.js', 'default-cache/page.js', 'fetch-no-cache/page.js', @@ -529,16 +527,20 @@ createNextDescribe( 'force-static/second.rsc', 'ssg-draft-mode/test.rsc', 'ssr-forced.prefetch.rsc', + 'isr-error-handling.html', + 'articles/[slug]/page.js', + 'no-store/static/page.js', 'blog/seb/second-post.rsc', 'blog/tim/first-post.html', 'force-static/second.html', 'ssg-draft-mode/test.html', + 'no-store/dynamic/page.js', 'blog/seb/second-post.html', 'ssg-draft-mode/test-2.rsc', + 'response-url.prefetch.rsc', 'blog/styfle/first-post.rsc', 'default-cache.prefetch.rsc', 'dynamic-error/[id]/page.js', - 'response-url.prefetch.rsc', 'ssg-draft-mode/test-2.html', 'blog/styfle/first-post.html', 'blog/styfle/second-post.rsc', @@ -572,6 +574,7 @@ createNextDescribe( 'gen-params-dynamic/one.prefetch.rsc', 'ssg-draft-mode/[[...route]]/page.js', 'variable-revalidate/post-method.rsc', + 'stale-cache-serving/app-page/page.js', 'dynamic-no-gen-params/[slug]/page.js', 'ssr-auto/cache-no-store.prefetch.rsc', 'static-to-dynamic-error/[id]/page.js', @@ -590,6 +593,7 @@ createNextDescribe( 'react-fetch-deduping-node.prefetch.rsc', 'ssr-auto/fetch-revalidate-zero/page.js', 'variable-revalidate/authorization.html', + '_not-found_client-reference-manifest.js', 'force-dynamic-no-prerender/[id]/page.js', 'variable-revalidate/post-method/page.js', 'variable-revalidate/status-code/page.js', @@ -598,6 +602,8 @@ createNextDescribe( 'partial-gen-params/[lang]/[slug]/page.js', 'variable-revalidate/headers-instance.rsc', 'variable-revalidate/revalidate-3/page.js', + 'stale-cache-serving-edge/app-page/page.js', + 'stale-cache-serving/app-page.prefetch.rsc', 'force-dynamic-catch-all/slug.prefetch.rsc', 'hooks/use-search-params/force-static.html', 'hooks/use-search-params/with-suspense.rsc', @@ -606,15 +612,17 @@ createNextDescribe( 'variable-revalidate-edge/no-store/page.js', 'variable-revalidate/authorization/page.js', 'variable-revalidate/headers-instance.html', + 'variable-revalidate/no-store.prefetch.rsc', + 'stale-cache-serving/route-handler/route.js', 'hooks/use-search-params/with-suspense.html', 'route-handler-edge/revalidate-360/route.js', - 'variable-revalidate/no-store.prefetch.rsc', 'variable-revalidate/revalidate-360-isr.rsc', 'variable-revalidate/revalidate-360/page.js', 'ssr-auto/fetch-revalidate-zero.prefetch.rsc', 'static-to-dynamic-error-forced/[id]/page.js', 'variable-config-revalidate/revalidate-3.rsc', 'variable-revalidate/revalidate-360-isr.html', + 'isr-error-handling/page_client-reference-manifest.js', 'gen-params-dynamic-revalidate/[slug]/page.js', 'hooks/use-search-params/force-static/page.js', 'ssr-forced/page_client-reference-manifest.js', @@ -629,6 +637,7 @@ createNextDescribe( 'force-static/page_client-reference-manifest.js', 'response-url/page_client-reference-manifest.js', 'variable-revalidate/revalidate-360-isr/page.js', + 'stale-cache-serving-edge/route-handler/route.js', 'blog/[author]/page_client-reference-manifest.js', 'default-cache/page_client-reference-manifest.js', 'force-dynamic-prerender/frameworks.prefetch.rsc', @@ -642,6 +651,8 @@ createNextDescribe( 'partial-gen-params-no-additional-lang/fr/RAND.rsc', 'partial-gen-params-no-additional-slug/en/RAND.rsc', 'partial-gen-params-no-additional-slug/fr/RAND.rsc', + 'articles/[slug]/page_client-reference-manifest.js', + 'no-store/static/page_client-reference-manifest.js', 'partial-gen-params-no-additional-lang/en/RAND.html', 'partial-gen-params-no-additional-lang/en/first.rsc', 'partial-gen-params-no-additional-lang/fr/RAND.html', @@ -650,6 +661,7 @@ createNextDescribe( 'partial-gen-params-no-additional-slug/en/first.rsc', 'partial-gen-params-no-additional-slug/fr/RAND.html', 'partial-gen-params-no-additional-slug/fr/first.rsc', + 'no-store/dynamic/page_client-reference-manifest.js', 'partial-gen-params-no-additional-lang/en/first.html', 'partial-gen-params-no-additional-lang/en/second.rsc', 'partial-gen-params-no-additional-lang/fr/first.html', @@ -678,6 +690,7 @@ createNextDescribe( 'react-fetch-deduping-node/page_client-reference-manifest.js', 'variable-revalidate/cookie/page_client-reference-manifest.js', 'ssg-draft-mode/[[...route]]/page_client-reference-manifest.js', + 'stale-cache-serving/app-page/page_client-reference-manifest.js', 'dynamic-no-gen-params/[slug]/page_client-reference-manifest.js', 'static-to-dynamic-error/[id]/page_client-reference-manifest.js', 'variable-revalidate/encoding/page_client-reference-manifest.js', @@ -691,6 +704,7 @@ createNextDescribe( 'dynamic-no-gen-params-ssr/[slug]/page_client-reference-manifest.js', 'partial-gen-params/[lang]/[slug]/page_client-reference-manifest.js', 'variable-revalidate/revalidate-3/page_client-reference-manifest.js', + 'stale-cache-serving-edge/app-page/page_client-reference-manifest.js', 'variable-revalidate-edge/encoding/page_client-reference-manifest.js', 'variable-revalidate-edge/no-store/page_client-reference-manifest.js', 'variable-revalidate/authorization/page_client-reference-manifest.js', @@ -709,16 +723,6 @@ createNextDescribe( 'variable-revalidate-edge/post-method-request/page_client-reference-manifest.js', 'partial-gen-params-no-additional-lang/[lang]/[slug]/page_client-reference-manifest.js', 'partial-gen-params-no-additional-slug/[lang]/[slug]/page_client-reference-manifest.js', - 'articles/[slug]/page.js', - 'articles/[slug]/page_client-reference-manifest.js', - 'articles/works.html', - 'articles/works.rsc', - 'no-store/dynamic/page.js', - 'no-store/dynamic/page_client-reference-manifest.js', - 'no-store/static.html', - 'no-store/static.rsc', - 'no-store/static/page.js', - 'no-store/static/page_client-reference-manifest.js', ].sort() ) }) @@ -1027,6 +1031,22 @@ createNextDescribe( "initialRevalidateSeconds": false, "srcRoute": "/hooks/use-search-params/with-suspense", }, + "/isr-error-handling": { + "dataRoute": "/isr-error-handling.rsc", + "experimentalBypassFor": [ + { + "key": "Next-Action", + "type": "header", + }, + { + "key": "content-type", + "type": "header", + "value": "multipart/form-data", + }, + ], + "initialRevalidateSeconds": 3, + "srcRoute": "/isr-error-handling", + }, "/no-store/static": { "dataRoute": "/no-store/static.rsc", "experimentalBypassFor": [ @@ -1656,6 +1676,27 @@ createNextDescribe( 'Static generation failed due to dynamic usage on /ssr-auto/cache-no-store, reason: no-store fetch' ) }) + + // build cache not leveraged for custom cache handler so not seeded + if (!process.env.CUSTOM_CACHE_HANDLER) { + it('should correctly error and not update cache for ISR', async () => { + await next.patchFile('app/isr-error-handling/error.txt', 'yes') + + for (let i = 0; i < 3; i++) { + const res = await next.fetch('/isr-error-handling') + const html = await res.text() + const $ = cheerio.load(html) + const now = $('#now').text() + + expect(res.status).toBe(200) + expect(now).toBeTruthy() + + // wait revalidate period + await waitFor(3000) + } + expect(next.cliOutput).toContain('intentional error') + }) + } } it.each([ diff --git a/test/e2e/app-dir/app-static/app/isr-error-handling/error.txt b/test/e2e/app-dir/app-static/app/isr-error-handling/error.txt new file mode 100644 index 0000000000000..54299a48fb3ae --- /dev/null +++ b/test/e2e/app-dir/app-static/app/isr-error-handling/error.txt @@ -0,0 +1 @@ +no \ No newline at end of file diff --git a/test/e2e/app-dir/app-static/app/isr-error-handling/page.js b/test/e2e/app-dir/app-static/app/isr-error-handling/page.js new file mode 100644 index 0000000000000..7761f85b171a1 --- /dev/null +++ b/test/e2e/app-dir/app-static/app/isr-error-handling/page.js @@ -0,0 +1,23 @@ +import fs from 'fs' +import path from 'path' + +export const revalidate = 3 + +export default async function Page() { + const shouldError = ( + await fs.promises.readFile( + path.join(process.cwd(), 'app/isr-error-handling/error.txt'), + 'utf8' + ) + ).trim() + + if (shouldError === 'yes') { + throw new Error('intentional error') + } + + return ( +
+

{Date.now()}

+
+ ) +} From fdd8bd95e08eb5218ba84da8f6ff91ea6eef5179 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Tue, 24 Oct 2023 10:03:23 -0700 Subject: [PATCH 14/15] fix async-modules test (#57320) Doesn't seem like we need a `.babelrc` here and it's causing turbopack test failures --- test/integration/async-modules/.babelrc | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 test/integration/async-modules/.babelrc diff --git a/test/integration/async-modules/.babelrc b/test/integration/async-modules/.babelrc deleted file mode 100644 index 1ff94f7ed28e1..0000000000000 --- a/test/integration/async-modules/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["next/babel"] -} From 8734a03ff54b2ab40baf3a6e857ac87623a8452e Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 24 Oct 2023 10:16:25 -0700 Subject: [PATCH 15/15] fix metadata url resolving with path posix (#57343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit x-ref: [https://dev.azure.com/nextjs/next.js/_build/results?buildId=71881&view=logs&jobId=8[…]-584d-6f5c-57bad8880974&t=7ae70e63-3625-50f4-6764-5b3e72b4bd7a](https://dev.azure.com/nextjs/next.js/_build/results?buildId=71881&view=logs&jobId=8af7cf9c-43a1-584d-6f5c-57bad8880974&j=8af7cf9c-43a1-584d-6f5c-57bad8880974&t=7ae70e63-3625-50f4-6764-5b3e72b4bd7a) Follow up for: https://github.com/vercel/next.js/pull/57265 --- packages/next/src/lib/metadata/resolvers/resolve-url.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/src/lib/metadata/resolvers/resolve-url.ts b/packages/next/src/lib/metadata/resolvers/resolve-url.ts index 05f9daa71575d..0cc0cd3483f23 100644 --- a/packages/next/src/lib/metadata/resolvers/resolve-url.ts +++ b/packages/next/src/lib/metadata/resolvers/resolve-url.ts @@ -74,7 +74,7 @@ function resolveUrl( // Resolve with `pathname` if `url` is a relative path. function resolveRelativeUrl(url: string | URL, pathname: string): string | URL { if (typeof url === 'string' && url.startsWith('./')) { - return path.resolve(pathname, url) + return path.posix.resolve(pathname, url) } return url }