From b436589ff86a8a4c5109120d07148ec75483ecdd Mon Sep 17 00:00:00 2001 From: Ryan LaBarre Date: Wed, 4 Oct 2023 08:18:54 -0700 Subject: [PATCH 01/10] Maintenance: minor example dep bumps to fix moderate vulns (#56375) ### What? Merged a bunch of dependabot alerts in my own canary branch, mainly postcss patch updates, and one graphql minor update, to fix moderate security vulnerabilities in examples. Spot checked most and look good still. EDIT: also one in scripts/send-trace-to-jaeger ### Why? Because safety ### How? Dependabot --------- Signed-off-by: dependabot[bot] Co-authored-by: Steven --- examples/api-routes-apollo-server/package.json | 2 +- examples/cms-contentful/package.json | 2 +- examples/cms-cosmic/package.json | 2 +- examples/cms-datocms/package.json | 2 +- examples/cms-drupal/package.json | 2 +- examples/cms-enterspeed/package.json | 2 +- examples/cms-ghost/package.json | 2 +- examples/cms-graphcms/package.json | 2 +- examples/cms-kontent-ai/package.json | 2 +- examples/cms-prepr/package.json | 2 +- examples/cms-prismic/package.json | 2 +- examples/cms-storyblok/package.json | 2 +- examples/cms-takeshape/package.json | 2 +- examples/cms-umbraco-heartcore/package.json | 2 +- examples/cms-wordpress/package.json | 2 +- examples/radix-ui/package.json | 2 +- scripts/send-trace-to-jaeger/Cargo.lock | 4 ++-- 17 files changed, 18 insertions(+), 18 deletions(-) mode change 100755 => 100644 examples/cms-umbraco-heartcore/package.json diff --git a/examples/api-routes-apollo-server/package.json b/examples/api-routes-apollo-server/package.json index e0dbb2f961eea..62d77ba20476b 100644 --- a/examples/api-routes-apollo-server/package.json +++ b/examples/api-routes-apollo-server/package.json @@ -9,7 +9,7 @@ "@apollo/server": "^4.1.1", "@as-integrations/next": "^1.1.0", "@graphql-tools/schema": "^9.0.9", - "graphql": "16.6.0", + "graphql": "16.8.1", "graphql-tag": "^2.12.6", "next": "latest", "react": "^18.2.0", diff --git a/examples/cms-contentful/package.json b/examples/cms-contentful/package.json index ff94b0010ea40..6f8cbac311b23 100644 --- a/examples/cms-contentful/package.json +++ b/examples/cms-contentful/package.json @@ -17,7 +17,7 @@ "contentful-import": "^9.0.4", "date-fns": "2.30.0", "next": "latest", - "postcss": "8.4.28", + "postcss": "8.4.31", "react": "^18.2.0", "react-dom": "^18.2.0", "tailwindcss": "^3.3.3", diff --git a/examples/cms-cosmic/package.json b/examples/cms-cosmic/package.json index f9f16d092100a..e4a7adee9cb9f 100644 --- a/examples/cms-cosmic/package.json +++ b/examples/cms-cosmic/package.json @@ -21,7 +21,7 @@ "@types/node": "^18.0.0", "@types/react": "^18.0.14", "autoprefixer": "10.4.7", - "postcss": "8.4.14", + "postcss": "8.4.31", "tailwindcss": "^3.1.4", "typescript": "^4.7.4" } diff --git a/examples/cms-datocms/package.json b/examples/cms-datocms/package.json index 824026ab1843e..72b156cf78f58 100644 --- a/examples/cms-datocms/package.json +++ b/examples/cms-datocms/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-drupal/package.json b/examples/cms-drupal/package.json index c93fc4ed075fa..e06b113ce89ff 100644 --- a/examples/cms-drupal/package.json +++ b/examples/cms-drupal/package.json @@ -15,7 +15,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-enterspeed/package.json b/examples/cms-enterspeed/package.json index ec83c566b3089..6bd95a5c90666 100644 --- a/examples/cms-enterspeed/package.json +++ b/examples/cms-enterspeed/package.json @@ -16,7 +16,7 @@ "@types/node": "18.0.0", "@types/react": "18.0.14", "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15", "typescript": "4.7.4" } diff --git a/examples/cms-ghost/package.json b/examples/cms-ghost/package.json index 1693030273811..ba4ee4cf1fefc 100644 --- a/examples/cms-ghost/package.json +++ b/examples/cms-ghost/package.json @@ -16,7 +16,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-graphcms/package.json b/examples/cms-graphcms/package.json index d8f489e42f184..0712af518d4d5 100644 --- a/examples/cms-graphcms/package.json +++ b/examples/cms-graphcms/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-kontent-ai/package.json b/examples/cms-kontent-ai/package.json index 1e967c6517d2f..ee16f680781a4 100644 --- a/examples/cms-kontent-ai/package.json +++ b/examples/cms-kontent-ai/package.json @@ -22,7 +22,7 @@ }, "devDependencies": { "autoprefixer": "10.4.7", - "postcss": "8.4.14", + "postcss": "8.4.31", "tailwindcss": "^3.0.15", "tslib": "2.4.0" } diff --git a/examples/cms-prepr/package.json b/examples/cms-prepr/package.json index ac66c6022e78e..8af08ce227afd 100644 --- a/examples/cms-prepr/package.json +++ b/examples/cms-prepr/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-prismic/package.json b/examples/cms-prismic/package.json index ecbe22f538405..db200ccdb7440 100644 --- a/examples/cms-prismic/package.json +++ b/examples/cms-prismic/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "autoprefixer": "10.4.10", - "postcss": "8.4.16", + "postcss": "8.4.31", "slice-machine-ui": "^0.4.2", "tailwindcss": "^3.1.8", "typescript": "^4.8.3" diff --git a/examples/cms-storyblok/package.json b/examples/cms-storyblok/package.json index 99b21b04ac7a0..42c49db957a99 100644 --- a/examples/cms-storyblok/package.json +++ b/examples/cms-storyblok/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-takeshape/package.json b/examples/cms-takeshape/package.json index 83e224c3549bc..61ea4c3c96155 100644 --- a/examples/cms-takeshape/package.json +++ b/examples/cms-takeshape/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-umbraco-heartcore/package.json b/examples/cms-umbraco-heartcore/package.json old mode 100755 new mode 100644 index d8f489e42f184..0712af518d4d5 --- a/examples/cms-umbraco-heartcore/package.json +++ b/examples/cms-umbraco-heartcore/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", + "postcss": "8.4.31", "tailwindcss": "^3.0.15" } } diff --git a/examples/cms-wordpress/package.json b/examples/cms-wordpress/package.json index 4ca98678fb7e1..6f194082af0ff 100644 --- a/examples/cms-wordpress/package.json +++ b/examples/cms-wordpress/package.json @@ -19,7 +19,7 @@ }, "devDependencies": { "autoprefixer": "10.4.7", - "postcss": "8.4.14", + "postcss": "8.4.31", "tailwindcss": "^3.0.24" } } diff --git a/examples/radix-ui/package.json b/examples/radix-ui/package.json index 3cb4e5d156791..ea437a347265a 100644 --- a/examples/radix-ui/package.json +++ b/examples/radix-ui/package.json @@ -16,7 +16,7 @@ "@types/node": "18.8.0", "@types/react": "18.0.21", "autoprefixer": "10.4.12", - "postcss": "8.4.17", + "postcss": "8.4.31", "tailwindcss": "3.1.8", "typescript": "4.8.4" } diff --git a/scripts/send-trace-to-jaeger/Cargo.lock b/scripts/send-trace-to-jaeger/Cargo.lock index db05bc0325a25..d0b5c5da023eb 100644 --- a/scripts/send-trace-to-jaeger/Cargo.lock +++ b/scripts/send-trace-to-jaeger/Cargo.lock @@ -22,9 +22,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" -version = "3.8.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "bytes" From f8308c9e39c098712fa3149cdeb3d5cbab27e5cb Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Wed, 4 Oct 2023 11:44:26 -0400 Subject: [PATCH 02/10] fix: avoid unnecessary `existSync` call (#56419) Co-authored-by: Jiachi Liu --- packages/next/src/server/next-server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index ca8510c57e29e..5804a9cd9d6f4 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -367,8 +367,8 @@ export default class NextNodeServer extends BaseServer { const buildIdFile = join(this.distDir, BUILD_ID_FILE) try { return fs.readFileSync(buildIdFile, 'utf8').trim() - } catch (err) { - if (!fs.existsSync(buildIdFile)) { + } catch (err: any) { + if (err.code === 'ENOENT') { throw new Error( `Could not find a production build in the '${this.distDir}' directory. Try building your app with 'next build' before starting the production server. https://nextjs.org/docs/messages/production-start-no-build-id` ) From a278e94df5f7a2e651e34a88d5403d8847dd65a7 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Wed, 4 Oct 2023 12:40:59 -0400 Subject: [PATCH 03/10] fix: avoid creation of buffers for read ops (#56421) Example usage of `readFileSync().toString()` first creates a `Buffer` instance and later does another C++ call to convert Buffer to UTF-8. This pull request reduces the C++ calls. --- .../helpers/create-incremental-cache.ts | 4 +- .../incremental-cache/file-system-cache.ts | 45 +++++++++---------- .../next/src/server/lib/node-fs-methods.ts | 4 +- packages/next/src/server/next-server.ts | 18 ++++---- packages/next/src/shared/lib/utils.ts | 5 ++- 5 files changed, 37 insertions(+), 39 deletions(-) diff --git a/packages/next/src/export/helpers/create-incremental-cache.ts b/packages/next/src/export/helpers/create-incremental-cache.ts index 233a68f680317..35a3ae75b345d 100644 --- a/packages/next/src/export/helpers/create-incremental-cache.ts +++ b/packages/next/src/export/helpers/create-incremental-cache.ts @@ -35,8 +35,8 @@ export function createIncrementalCache( notFoundRoutes: [], }), fs: { - readFile: (f) => fs.promises.readFile(f), - readFileSync: (f) => fs.readFileSync(f), + readFile: fs.promises.readFile, + readFileSync: fs.readFileSync, writeFile: (f, d) => fs.promises.writeFile(f, d), mkdir: (dir) => fs.promises.mkdir(dir, { recursive: true }), stat: (f) => fs.promises.stat(f), diff --git a/packages/next/src/server/lib/incremental-cache/file-system-cache.ts b/packages/next/src/server/lib/incremental-cache/file-system-cache.ts index 79e18ad3b996b..0d63dd3e8a709 100644 --- a/packages/next/src/server/lib/incremental-cache/file-system-cache.ts +++ b/packages/next/src/server/lib/incremental-cache/file-system-cache.ts @@ -75,7 +75,7 @@ export default class FileSystemCache implements CacheHandler { if (!this.tagsManifestPath || !this.fs || tagsManifest) return try { tagsManifest = JSON.parse( - this.fs.readFileSync(this.tagsManifestPath).toString('utf8') + this.fs.readFileSync(this.tagsManifestPath, 'utf8') ) } catch (err: any) { tagsManifest = { version: 1, items: {} } @@ -131,9 +131,7 @@ export default class FileSystemCache implements CacheHandler { const { mtime } = await this.fs.stat(filePath) const meta = JSON.parse( - ( - await this.fs.readFile(filePath.replace(/\.body$/, '.meta')) - ).toString('utf8') + await this.fs.readFile(filePath.replace(/\.body$/, '.meta'), 'utf8') ) const cacheEntry: CacheHandlerValue = { @@ -155,7 +153,7 @@ export default class FileSystemCache implements CacheHandler { pathname: fetchCache ? key : `${key}.html`, fetchCache, }) - const fileData = (await this.fs.readFile(filePath)).toString('utf-8') + const fileData = await this.fs.readFile(filePath, 'utf8') const { mtime } = await this.fs.stat(filePath) if (fetchCache) { @@ -178,27 +176,25 @@ export default class FileSystemCache implements CacheHandler { } } else { const pageData = isAppPath - ? ( + ? await this.fs.readFile( + ( + await this.getFsPath({ + pathname: `${key}.rsc`, + appDir: true, + }) + ).filePath, + 'utf8' + ) + : JSON.parse( await this.fs.readFile( ( await this.getFsPath({ - pathname: `${key}.rsc`, - appDir: true, + pathname: `${key}.json`, + appDir: false, }) - ).filePath + ).filePath, + 'utf8' ) - ).toString('utf8') - : JSON.parse( - ( - await this.fs.readFile( - ( - await this.getFsPath({ - pathname: `${key}.json`, - appDir: false, - }) - ).filePath - ) - ).toString('utf8') ) let meta: { status?: number; headers?: OutgoingHttpHeaders } = {} @@ -206,9 +202,10 @@ export default class FileSystemCache implements CacheHandler { if (isAppPath) { try { meta = JSON.parse( - ( - await this.fs.readFile(filePath.replace(/\.html$/, '.meta')) - ).toString('utf-8') + await this.fs.readFile( + filePath.replace(/\.html$/, '.meta'), + 'utf8' + ) ) } catch {} } diff --git a/packages/next/src/server/lib/node-fs-methods.ts b/packages/next/src/server/lib/node-fs-methods.ts index 490c4e0336c4a..121bf9cc89f40 100644 --- a/packages/next/src/server/lib/node-fs-methods.ts +++ b/packages/next/src/server/lib/node-fs-methods.ts @@ -2,8 +2,8 @@ import _fs from 'fs' import type { CacheFs } from '../../shared/lib/utils' export const nodeFs: CacheFs = { - readFile: (f) => _fs.promises.readFile(f), - readFileSync: (f) => _fs.readFileSync(f), + readFile: _fs.promises.readFile, + readFileSync: _fs.readFileSync, writeFile: (f, d) => _fs.promises.writeFile(f, d), mkdir: (dir) => _fs.promises.mkdir(dir, { recursive: true }), stat: (f) => _fs.promises.stat(f), diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 5804a9cd9d6f4..718214798b2c4 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -722,14 +722,13 @@ export default class NextNodeServer extends BaseServer { ) } - protected async getFallback(page: string): Promise { + protected getFallback(page: string): Promise { page = normalizePagePath(page) const cacheFs = this.getCacheFilesystem() - const html = await cacheFs.readFile( - join(this.serverDistDir, 'pages', `${page}.html`) + return cacheFs.readFile( + join(this.serverDistDir, 'pages', `${page}.html`), + 'utf8' ) - - return html.toString('utf8') } protected async handleNextImageRequest( @@ -985,10 +984,11 @@ export default class NextNodeServer extends BaseServer { return this.runApi(req, res, query, match) } - protected async getPrefetchRsc(pathname: string) { - return this.getCacheFilesystem() - .readFile(join(this.serverDistDir, 'app', `${pathname}.prefetch.rsc`)) - .then((res) => res.toString()) + protected getPrefetchRsc(pathname: string): Promise { + return this.getCacheFilesystem().readFile( + join(this.serverDistDir, 'app', `${pathname}.prefetch.rsc`), + 'utf8' + ) } protected getCacheFilesystem(): CacheFs { diff --git a/packages/next/src/shared/lib/utils.ts b/packages/next/src/shared/lib/utils.ts index 771f11b4bec23..61b8fc394f93a 100644 --- a/packages/next/src/shared/lib/utils.ts +++ b/packages/next/src/shared/lib/utils.ts @@ -7,6 +7,7 @@ import type { NextRouter } from './router/router' import type { ParsedUrlQuery } from 'querystring' import type { PreviewData } from 'next/types' import { COMPILER_NAMES } from './constants' +import type fs from 'fs' export type NextComponentType< Context extends BaseContext = NextPageContext, @@ -447,8 +448,8 @@ export class MiddlewareNotFoundError extends Error { } export interface CacheFs { - readFile(f: string): Promise - readFileSync(f: string): Buffer + readFile: typeof fs.promises.readFile + readFileSync: typeof fs.readFileSync writeFile(f: string, d: any): Promise mkdir(dir: string): Promise stat(f: string): Promise<{ mtime: Date }> From 338f80b4fd76ddfe8defac2bdcac7d31a2474dc4 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Wed, 4 Oct 2023 19:29:26 +0200 Subject: [PATCH 04/10] fix empty externals list, pnpm special case, and project path (#56402) ### What? * node.js doesn't allow module requests starting with `.` since node.js 18, so we need to remove out .pnpm special case * externals should be resolvable from the project dir instead of the root dir * fix a problem when the list of externals is empty ### Why? ### How? Closes WEB-1703 --- .../next-core/src/next_server/context.rs | 2 + .../next-core/src/next_server/resolve.rs | 72 +++++++++---------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/packages/next-swc/crates/next-core/src/next_server/context.rs b/packages/next-swc/crates/next-core/src/next_server/context.rs index a67ecdb111d2e..ac8dd43c0997a 100644 --- a/packages/next-swc/crates/next-core/src/next_server/context.rs +++ b/packages/next-swc/crates/next-core/src/next_server/context.rs @@ -109,6 +109,7 @@ pub async fn get_server_resolve_options_context( let unsupported_modules_resolve_plugin = UnsupportedModulesResolvePlugin::new(project_path); let server_component_externals_plugin = ExternalCjsModulesResolvePlugin::new( project_path, + project_path.root(), ExternalPredicate::Only(next_config.server_component_externals()).cell(), ); let ty = ty.into_value(); @@ -125,6 +126,7 @@ pub async fn get_server_resolve_options_context( }; let external_cjs_modules_plugin = ExternalCjsModulesResolvePlugin::new( project_path, + project_path.root(), ExternalPredicate::AllExcept(next_config.transpile_packages()).cell(), ); diff --git a/packages/next-swc/crates/next-core/src/next_server/resolve.rs b/packages/next-swc/crates/next-core/src/next_server/resolve.rs index 558ba777840a6..5a720139d2c56 100644 --- a/packages/next-swc/crates/next-core/src/next_server/resolve.rs +++ b/packages/next-swc/crates/next-core/src/next_server/resolve.rs @@ -1,6 +1,4 @@ use anyhow::Result; -use once_cell::sync::Lazy; -use regex::Regex; use turbo_tasks::Vc; use turbopack_binding::{ turbo::tasks_fs::{glob::Glob, FileJsonContent, FileSystemPath}, @@ -33,6 +31,7 @@ pub enum ExternalPredicate { /// possible to resolve them at runtime. #[turbo_tasks::value] pub(crate) struct ExternalCjsModulesResolvePlugin { + project_path: Vc, root: Vc, predicate: Vc, } @@ -40,8 +39,17 @@ pub(crate) struct ExternalCjsModulesResolvePlugin { #[turbo_tasks::value_impl] impl ExternalCjsModulesResolvePlugin { #[turbo_tasks::function] - pub fn new(root: Vc, predicate: Vc) -> Vc { - ExternalCjsModulesResolvePlugin { root, predicate }.cell() + pub fn new( + project_path: Vc, + root: Vc, + predicate: Vc, + ) -> Vc { + ExternalCjsModulesResolvePlugin { + project_path, + root, + predicate, + } + .cell() } } @@ -66,11 +74,9 @@ async fn is_node_resolveable( Ok(Vc::cell(true)) } -static PNPM: Lazy = Lazy::new(|| Regex::new(r"(?:/|^)node_modules/(.pnpm/.+)").unwrap()); - #[turbo_tasks::function] fn condition(root: Vc) -> Vc { - ResolvePluginCondition::new(root.root(), Glob::new("**/node_modules/**".to_string())) + ResolvePluginCondition::new(root, Glob::new("**/node_modules/**".to_string())) } #[turbo_tasks::value_impl] @@ -101,14 +107,20 @@ impl ResolvePlugin for ExternalCjsModulesResolvePlugin { ExternalPredicate::AllExcept(exceptions) => { let exception_glob = packages_glob(*exceptions).await?; - if exception_glob.execute(&raw_fs_path.path) { - return Ok(ResolveResultOption::none()); + if let Some(exception_glob) = *exception_glob { + if exception_glob.await?.execute(&raw_fs_path.path) { + return Ok(ResolveResultOption::none()); + } } } ExternalPredicate::Only(externals) => { let external_glob = packages_glob(*externals).await?; - if !external_glob.execute(&raw_fs_path.path) { + if let Some(external_glob) = *external_glob { + if !external_glob.await?.execute(&raw_fs_path.path) { + return Ok(ResolveResultOption::none()); + } + } else { return Ok(ResolveResultOption::none()); } } @@ -141,42 +153,30 @@ impl ResolvePlugin for ExternalCjsModulesResolvePlugin { // check if we can resolve the package from the project dir with node.js resolve // options (might be hidden by pnpm) - if *is_node_resolveable(self.root.root(), request, fs_path).await? { + if *is_node_resolveable(self.project_path, request, fs_path).await? { // mark as external return Ok(ResolveResultOption::some( ResolveResult::primary(ResolveResultItem::OriginalReferenceExternal).cell(), )); } - // Special behavior for pnpm as we could reference all .pnpm modules by - // referencing the `.pnpm` folder as module, e. g. - // /node_modules/.pnpm/some-package@2.29.2/node_modules/some-package/dir/file.js - // becomes - // .pnpm/some-package@2.29.2/node_modules/some-package/dir/file.js - if let Some(captures) = PNPM.captures(&fs_path.await?.path) { - if let Some(import_path) = captures.get(1) { - // we could load it directly as external, but we want to make sure node.js would - // resolve it the same way e. g. that we didn't follow any special resolve - // options, to come here like the `module` field in package.json - if *is_node_resolveable(context, request, fs_path).await? { - // mark as external - return Ok(ResolveResultOption::some( - ResolveResult::primary(ResolveResultItem::OriginalReferenceTypeExternal( - import_path.as_str().to_string(), - )) - .cell(), - )); - } - } - } Ok(ResolveResultOption::none()) } } +// TODO move that to turbo +#[turbo_tasks::value(transparent)] +pub struct OptionGlob(Option>); + #[turbo_tasks::function] -async fn packages_glob(packages: Vc>) -> Result> { - Ok(Glob::new(format!( - "**/node_modules/{{{}}}/**", - packages.await?.join(",") +async fn packages_glob(packages: Vc>) -> Result> { + let packages = packages.await?; + if packages.is_empty() { + return Ok(Vc::cell(None)); + } + Ok(Vc::cell(Some( + Glob::new(format!("**/node_modules/{{{}}}/**", packages.join(","))) + .resolve() + .await?, ))) } From 00aea747c5ce816a71e3c1f7730786c218de920e Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 4 Oct 2023 19:46:44 +0200 Subject: [PATCH 05/10] add `cargo fmt` to lint staged (#56430) Keep forgetting to run `cargo fmt` before committing Closes WEB-1707 --- lint-staged.config.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lint-staged.config.js b/lint-staged.config.js index b44eaea2a0a3f..8c1b2df519b67 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -27,6 +27,12 @@ module.exports = { `git add ${escapedFileNames}`, ] }, + '**/*.rs': (filenames) => { + const escapedFileNames = filenames + .map((filename) => (isWin ? filename : escape([filename]))) + .join(' ') + return [`cargo fmt -- ${escapedFileNames}`, `git add ${escapedFileNames}`] + }, } function escape(str) { From e09e0f5e5b6793d739cd4612d6033cd97c24597f Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 4 Oct 2023 19:55:50 +0200 Subject: [PATCH 06/10] chore: extract edge-app-route loader template (#56424) Closes WEB-1705 --- packages/next/src/build/entries.ts | 1 + packages/next/src/build/load-entrypoint.ts | 10 ++++++++-- packages/next/src/build/templates/edge-app-route.ts | 9 +++++++++ .../loaders/next-edge-app-route-loader/index.ts | 13 +++++-------- 4 files changed, 23 insertions(+), 10 deletions(-) create mode 100644 packages/next/src/build/templates/edge-app-route.ts diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index 301f7843d0481..551f2a5e960b0 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -356,6 +356,7 @@ export function getEdgeServerEntry(opts: { layer: WEBPACK_LAYERS.reactServerComponents, } } + if (isMiddlewareFile(opts.page)) { const loaderParams: MiddlewareLoaderOptions = { absolutePagePath: opts.absolutePagePath, diff --git a/packages/next/src/build/load-entrypoint.ts b/packages/next/src/build/load-entrypoint.ts index 880899b693680..ec97ac1527264 100644 --- a/packages/next/src/build/load-entrypoint.ts +++ b/packages/next/src/build/load-entrypoint.ts @@ -19,12 +19,18 @@ const TEMPLATES_ESM_FOLDER = path.normalize( * handle replacement values that are related to imports. * * @param entrypoint the entrypoint to load - * @param replacements the replacements to perform + * @param replacements string replacements to perform + * @param injections code injections to perform * @returns the loaded file with the replacements */ export async function loadEntrypoint( - entrypoint: 'pages' | 'pages-api' | 'app-page' | 'app-route', + entrypoint: + | 'pages' + | 'pages-api' + | 'app-page' + | 'app-route' + | 'edge-app-route', replacements: Record<`VAR_${string}`, string>, injections?: Record ): Promise { diff --git a/packages/next/src/build/templates/edge-app-route.ts b/packages/next/src/build/templates/edge-app-route.ts new file mode 100644 index 0000000000000..69b12643ecfad --- /dev/null +++ b/packages/next/src/build/templates/edge-app-route.ts @@ -0,0 +1,9 @@ +import { EdgeRouteModuleWrapper } from '../../server/web/edge-route-module-wrapper' + +// Import the userland code. +// @ts-expect-error - replaced by webpack/turbopack loader +import * as module from 'VAR_USERLAND' + +export const ComponentMod = module + +export default EdgeRouteModuleWrapper.wrap(module.routeModule) diff --git a/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts b/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts index 5d2ae74df14b0..e14db1b3b21b1 100644 --- a/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-edge-app-route-loader/index.ts @@ -4,6 +4,7 @@ import { NextConfig } from '../../../../server/config-shared' import { webpack } from 'next/dist/compiled/webpack/webpack' import { WEBPACK_RESOURCE_QUERIES } from '../../../../lib/constants' import { MiddlewareConfig } from '../../../analysis/get-page-static-info' +import { loadEntrypoint } from '../../../load-entrypoint' export type EdgeAppRouteLoaderQuery = { absolutePagePath: string @@ -15,7 +16,7 @@ export type EdgeAppRouteLoaderQuery = { } const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction = - function (this) { + async function (this) { const { page, absolutePagePath, @@ -52,13 +53,9 @@ const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction Date: Wed, 4 Oct 2023 19:01:01 +0000 Subject: [PATCH 07/10] v13.5.5-canary.2 --- 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 2bb6c27f8cbde..b4ff2eda07dcf 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "13.5.5-canary.1" + "version": "13.5.5-canary.2" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 93404b1ef0810..1ca01794a8ee1 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 9d11da711eba4..ab5cafb81e28c 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "description": "ESLint configuration used by Next.js.", "main": "index.js", "license": "MIT", @@ -10,7 +10,7 @@ }, "homepage": "https://nextjs.org/docs/app/building-your-application/configuring/eslint#eslint-config", "dependencies": { - "@next/eslint-plugin-next": "13.5.5-canary.1", + "@next/eslint-plugin-next": "13.5.5-canary.2", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", "eslint-import-resolver-node": "^0.3.6", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 1e5ff8a076060..d37cf3822c4f9 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "license": "MIT", diff --git a/packages/font/package.json b/packages/font/package.json index 5b563462678b4..e8dac1caf89d8 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,6 +1,6 @@ { "name": "@next/font", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 19ee7d55ef329..44ac0d40b40ac 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 5437d411eeaf8..81b150a3b928c 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 579f438eeb68b..e3c5a99ea5487 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index db3fd04a7e72d..102a8ed4d33ef 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 4c3ed5688ab10..cd3dca272564e 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 5cc8c1f2b8f2b..d498743e0287b 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 4573bcad893fa..c715e5a210921 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index 5827391664d0b..eb13e7a3d6d43 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", diff --git a/packages/next/package.json b/packages/next/package.json index e8165725764f8..842f76584ca4f 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -90,7 +90,7 @@ ] }, "dependencies": { - "@next/env": "13.5.5-canary.1", + "@next/env": "13.5.5-canary.2", "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", @@ -144,11 +144,11 @@ "@mswjs/interceptors": "0.23.0", "@napi-rs/cli": "2.16.2", "@napi-rs/triples": "1.1.0", - "@next/polyfill-module": "13.5.5-canary.1", - "@next/polyfill-nomodule": "13.5.5-canary.1", - "@next/react-dev-overlay": "13.5.5-canary.1", - "@next/react-refresh-utils": "13.5.5-canary.1", - "@next/swc": "13.5.5-canary.1", + "@next/polyfill-module": "13.5.5-canary.2", + "@next/polyfill-nomodule": "13.5.5-canary.2", + "@next/react-dev-overlay": "13.5.5-canary.2", + "@next/react-refresh-utils": "13.5.5-canary.2", + "@next/swc": "13.5.5-canary.2", "@opentelemetry/api": "1.4.1", "@playwright/test": "^1.35.1", "@segment/ajv-human-errors": "2.1.2", diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index f39784a4a8b93..81edef1fd77a8 100644 --- a/packages/react-dev-overlay/package.json +++ b/packages/react-dev-overlay/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-dev-overlay", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index c4e4fdf64b523..6c648bd5fc2d5 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index ed790a604f0f7..fbef1a4a4f255 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "13.5.5-canary.1", + "version": "13.5.5-canary.2", "private": true, "repository": { "url": "vercel/next.js", @@ -23,7 +23,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "13.5.5-canary.1", + "next": "13.5.5-canary.2", "outdent": "0.8.0", "prettier": "2.5.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a2eed184a4f5..22bfb1ffa1aab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -732,7 +732,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 13.5.5-canary.1 + specifier: 13.5.5-canary.2 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.3.3 @@ -793,7 +793,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 13.5.5-canary.1 + specifier: 13.5.5-canary.2 version: link:../next-env '@swc/helpers': specifier: 0.5.2 @@ -917,19 +917,19 @@ importers: specifier: 1.1.0 version: 1.1.0 '@next/polyfill-module': - specifier: 13.5.5-canary.1 + specifier: 13.5.5-canary.2 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 13.5.5-canary.1 + specifier: 13.5.5-canary.2 version: link:../next-polyfill-nomodule '@next/react-dev-overlay': - specifier: 13.5.5-canary.1 + specifier: 13.5.5-canary.2 version: link:../react-dev-overlay '@next/react-refresh-utils': - specifier: 13.5.5-canary.1 + specifier: 13.5.5-canary.2 version: link:../react-refresh-utils '@next/swc': - specifier: 13.5.5-canary.1 + specifier: 13.5.5-canary.2 version: link:../next-swc '@opentelemetry/api': specifier: 1.4.1 @@ -1586,7 +1586,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 13.5.5-canary.1 + specifier: 13.5.5-canary.2 version: link:../next outdent: specifier: 0.8.0 From 65b0bb24af8a14d20871d830acc5a12ee03989cd Mon Sep 17 00:00:00 2001 From: Josh Story Date: Wed, 4 Oct 2023 13:29:10 -0700 Subject: [PATCH 08/10] Separate RSC and SSR jsx-runtime modules (#56438) There should be no shared react packages in our server runtime. rsc should always be separate from ssr. This update reconfigures the runtiem to eliminate shared react modules. the jsx runtime will now be separate for RSC and SSR. this is necessary because the implementations for the jsx runtime rely on React and they need to see the right versions. Additionally I fixed an alias so that the shared subset react is used when using react-server condition. I also fixed a bug in 2 tests related to class/className. Note: this PR blocks upgrading React canary due to internal changes in how React state is managed in when using the `react-server` condition --- packages/next-swc/crates/next-core/src/next_import_map.rs | 8 ++++---- packages/next/src/build/webpack-config.ts | 8 ++++---- .../src/server/future/route-modules/app-page/module.ts | 3 --- .../route-modules/app-page/vendored/rsc/entrypoints.ts | 5 ++++- .../vendored/{shared => rsc}/react-jsx-dev-runtime.ts | 2 +- .../vendored/{shared => rsc}/react-jsx-runtime.ts | 2 +- .../route-modules/app-page/vendored/shared/entrypoints.ts | 4 ---- .../route-modules/app-page/vendored/ssr/entrypoints.ts | 6 ++++-- .../app-page/vendored/ssr/react-jsx-dev-runtime.ts | 3 +++ .../app-page/vendored/ssr/react-jsx-runtime.ts | 3 +++ packages/next/webpack.config.js | 6 ++++++ .../app-dir/third-parties/app/google-maps-embed/page.js | 2 +- test/e2e/app-dir/third-parties/app/youtube-embed/page.js | 2 +- 13 files changed, 32 insertions(+), 22 deletions(-) rename packages/next/src/server/future/route-modules/app-page/vendored/{shared => rsc}/react-jsx-dev-runtime.ts (82%) rename packages/next/src/server/future/route-modules/app-page/vendored/{shared => rsc}/react-jsx-runtime.ts (82%) delete mode 100644 packages/next/src/server/future/route-modules/app-page/vendored/shared/entrypoints.ts create mode 100644 packages/next/src/server/future/route-modules/app-page/vendored/ssr/react-jsx-dev-runtime.ts create mode 100644 packages/next/src/server/future/route-modules/app-page/vendored/ssr/react-jsx-runtime.ts diff --git a/packages/next-swc/crates/next-core/src/next_import_map.rs b/packages/next-swc/crates/next-core/src/next_import_map.rs index 63cf8cf828d4a..fc2a29ec84456 100644 --- a/packages/next-swc/crates/next-core/src/next_import_map.rs +++ b/packages/next-swc/crates/next-core/src/next_import_map.rs @@ -452,7 +452,7 @@ async fn insert_next_server_special_aliases( match runtime { NextRuntime::Edge => "next/dist/compiled/react/jsx-runtime", NextRuntime::NodeJs => { - "next/dist/server/future/route-modules/app-page/vendored/shared/\ + "next/dist/server/future/route-modules/app-page/vendored/ssr/\ react-jsx-runtime" } }, @@ -465,7 +465,7 @@ async fn insert_next_server_special_aliases( match runtime { NextRuntime::Edge => "next/dist/compiled/react/jsx-dev-runtime", NextRuntime::NodeJs => { - "next/dist/server/future/route-modules/app-page/vendored/shared/\ + "next/dist/server/future/route-modules/app-page/vendored/ssr/\ react-jsx-dev-runtime" } }, @@ -579,7 +579,7 @@ async fn insert_next_server_special_aliases( match runtime { NextRuntime::Edge => "next/dist/compiled/react/jsx-runtime", NextRuntime::NodeJs => { - "next/dist/server/future/route-modules/app-page/vendored/shared/\ + "next/dist/server/future/route-modules/app-page/vendored/rsc/\ react-jsx-runtime" } }, @@ -592,7 +592,7 @@ async fn insert_next_server_special_aliases( match runtime { NextRuntime::Edge => "next/dist/compiled/react/jsx-dev-runtime", NextRuntime::NodeJs => { - "next/dist/server/future/route-modules/app-page/vendored/shared/\ + "next/dist/server/future/route-modules/app-page/vendored/rsc/\ react-jsx-dev-runtime" } }, diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 2c417158370ab..de5d19591b091 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -176,16 +176,16 @@ function createRSCAliases( if (!opts.isEdgeServer) { if (opts.layer === WEBPACK_LAYERS.serverSideRendering) { alias = Object.assign(alias, { - 'react/jsx-runtime$': `next/dist/server/future/route-modules/app-page/vendored/shared/react-jsx-runtime`, - 'react/jsx-dev-runtime$': `next/dist/server/future/route-modules/app-page/vendored/shared/react-jsx-dev-runtime`, + 'react/jsx-runtime$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-jsx-runtime`, + 'react/jsx-dev-runtime$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-jsx-dev-runtime`, react$: `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react`, 'react-dom$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-dom`, 'react-server-dom-webpack/client.edge$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-server-dom-webpack-client-edge`, }) } else if (opts.layer === WEBPACK_LAYERS.reactServerComponents) { alias = Object.assign(alias, { - 'react/jsx-runtime$': `next/dist/server/future/route-modules/app-page/vendored/shared/react-jsx-runtime`, - 'react/jsx-dev-runtime$': `next/dist/server/future/route-modules/app-page/vendored/shared/react-jsx-dev-runtime`, + 'react/jsx-runtime$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-jsx-runtime`, + 'react/jsx-dev-runtime$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-jsx-dev-runtime`, react$: `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react`, 'react-dom$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-dom`, 'react-server-dom-webpack/server.edge$': `next/dist/server/future/route-modules/app-page/vendored/${opts.layer}/react-server-dom-webpack-server-edge`, diff --git a/packages/next/src/server/future/route-modules/app-page/module.ts b/packages/next/src/server/future/route-modules/app-page/module.ts index 5cc33c1a60376..5a34be6759ad4 100644 --- a/packages/next/src/server/future/route-modules/app-page/module.ts +++ b/packages/next/src/server/future/route-modules/app-page/module.ts @@ -15,13 +15,11 @@ import * as vendoredContexts from './vendored/contexts/entrypoints' let vendoredReactRSC let vendoredReactSSR -let vendoredReactShared // the vendored Reacts are loaded from their original source in the edge runtime if (process.env.NEXT_RUNTIME !== 'edge') { vendoredReactRSC = require('./vendored/rsc/entrypoints') vendoredReactSSR = require('./vendored/ssr/entrypoints') - vendoredReactShared = require('./vendored/shared/entrypoints') } type AppPageUserlandModule = { @@ -64,7 +62,6 @@ export class AppPageRouteModule extends RouteModule< const vendored = { 'react-rsc': vendoredReactRSC, 'react-ssr': vendoredReactSSR, - 'react-shared': vendoredReactShared, contexts: vendoredContexts, } diff --git a/packages/next/src/server/future/route-modules/app-page/vendored/rsc/entrypoints.ts b/packages/next/src/server/future/route-modules/app-page/vendored/rsc/entrypoints.ts index e693d48484574..5b7d91e3601b2 100644 --- a/packages/next/src/server/future/route-modules/app-page/vendored/rsc/entrypoints.ts +++ b/packages/next/src/server/future/route-modules/app-page/vendored/rsc/entrypoints.ts @@ -1,6 +1,7 @@ import * as React from 'react' - import * as ReactDOM from 'react-dom/server-rendering-stub' +import * as ReactJsxDevRuntime from 'react/jsx-dev-runtime' +import * as ReactJsxRuntime from 'react/jsx-runtime' function getAltProxyForBindingsDEV( type: 'Turbopack' | 'Webpack', @@ -68,6 +69,8 @@ if (process.env.TURBOPACK) { export { React, + ReactJsxDevRuntime, + ReactJsxRuntime, ReactDOM, ReactServerDOMWebpackServerEdge, ReactServerDOMTurbopackServerEdge, diff --git a/packages/next/src/server/future/route-modules/app-page/vendored/shared/react-jsx-dev-runtime.ts b/packages/next/src/server/future/route-modules/app-page/vendored/rsc/react-jsx-dev-runtime.ts similarity index 82% rename from packages/next/src/server/future/route-modules/app-page/vendored/shared/react-jsx-dev-runtime.ts rename to packages/next/src/server/future/route-modules/app-page/vendored/rsc/react-jsx-dev-runtime.ts index 9623bb4a90ae0..324b0f5a2faf8 100644 --- a/packages/next/src/server/future/route-modules/app-page/vendored/shared/react-jsx-dev-runtime.ts +++ b/packages/next/src/server/future/route-modules/app-page/vendored/rsc/react-jsx-dev-runtime.ts @@ -1,3 +1,3 @@ module.exports = require('../../module.compiled').vendored[ - 'react-shared' + 'react-rsc' ].ReactJsxDevRuntime diff --git a/packages/next/src/server/future/route-modules/app-page/vendored/shared/react-jsx-runtime.ts b/packages/next/src/server/future/route-modules/app-page/vendored/rsc/react-jsx-runtime.ts similarity index 82% rename from packages/next/src/server/future/route-modules/app-page/vendored/shared/react-jsx-runtime.ts rename to packages/next/src/server/future/route-modules/app-page/vendored/rsc/react-jsx-runtime.ts index b7d24f304f96b..7335eb8d8c1e1 100644 --- a/packages/next/src/server/future/route-modules/app-page/vendored/shared/react-jsx-runtime.ts +++ b/packages/next/src/server/future/route-modules/app-page/vendored/rsc/react-jsx-runtime.ts @@ -1,3 +1,3 @@ module.exports = require('../../module.compiled').vendored[ - 'react-shared' + 'react-rsc' ].ReactJsxRuntime diff --git a/packages/next/src/server/future/route-modules/app-page/vendored/shared/entrypoints.ts b/packages/next/src/server/future/route-modules/app-page/vendored/shared/entrypoints.ts deleted file mode 100644 index b5a1bf1a3fd02..0000000000000 --- a/packages/next/src/server/future/route-modules/app-page/vendored/shared/entrypoints.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as ReactJsxDevRuntime from 'react/jsx-dev-runtime' -import * as ReactJsxRuntime from 'react/jsx-runtime' - -export { ReactJsxDevRuntime, ReactJsxRuntime } diff --git a/packages/next/src/server/future/route-modules/app-page/vendored/ssr/entrypoints.ts b/packages/next/src/server/future/route-modules/app-page/vendored/ssr/entrypoints.ts index ad3ee57d8f1f5..70f432fff4e88 100644 --- a/packages/next/src/server/future/route-modules/app-page/vendored/ssr/entrypoints.ts +++ b/packages/next/src/server/future/route-modules/app-page/vendored/ssr/entrypoints.ts @@ -1,10 +1,10 @@ import * as React from 'react' - import * as ReactDOM from 'react-dom/server-rendering-stub' +import * as ReactJsxDevRuntime from 'react/jsx-dev-runtime' +import * as ReactJsxRuntime from 'react/jsx-runtime' // eslint-disable-next-line import/no-extraneous-dependencies import * as ReactDOMServerEdge from 'react-dom/server.edge' -// eslint-disable-next-line import/no-extraneous-dependencies function getAltProxyForBindingsDEV( type: 'Turbopack' | 'Webpack', @@ -52,6 +52,8 @@ if (process.env.TURBOPACK) { export { React, + ReactJsxDevRuntime, + ReactJsxRuntime, ReactDOM, ReactDOMServerEdge, ReactServerDOMTurbopackClientEdge, diff --git a/packages/next/src/server/future/route-modules/app-page/vendored/ssr/react-jsx-dev-runtime.ts b/packages/next/src/server/future/route-modules/app-page/vendored/ssr/react-jsx-dev-runtime.ts new file mode 100644 index 0000000000000..501951272d5a7 --- /dev/null +++ b/packages/next/src/server/future/route-modules/app-page/vendored/ssr/react-jsx-dev-runtime.ts @@ -0,0 +1,3 @@ +module.exports = require('../../module.compiled').vendored[ + 'react-ssr' +].ReactJsxDevRuntime diff --git a/packages/next/src/server/future/route-modules/app-page/vendored/ssr/react-jsx-runtime.ts b/packages/next/src/server/future/route-modules/app-page/vendored/ssr/react-jsx-runtime.ts new file mode 100644 index 0000000000000..82499202f03eb --- /dev/null +++ b/packages/next/src/server/future/route-modules/app-page/vendored/ssr/react-jsx-runtime.ts @@ -0,0 +1,3 @@ +module.exports = require('../../module.compiled').vendored[ + 'react-ssr' +].ReactJsxRuntime diff --git a/packages/next/webpack.config.js b/packages/next/webpack.config.js index bc8a5c72a2f1e..ec4880d319f8b 100644 --- a/packages/next/webpack.config.js +++ b/packages/next/webpack.config.js @@ -208,6 +208,9 @@ module.exports = ({ dev, turbo, bundleType, experimental }) => { react$: `next/dist/compiled/react${ experimental ? '-experimental' : '' }/react.shared-subset`, + 'next/dist/compiled/react$': `next/dist/compiled/react${ + experimental ? '-experimental' : '' + }/react.shared-subset`, }, }, layer: 'react-server', @@ -220,6 +223,9 @@ module.exports = ({ dev, turbo, bundleType, experimental }) => { react$: `next/dist/compiled/react${ experimental ? '-experimental' : '' }/react.shared-subset`, + 'next/dist/compiled/react$': `next/dist/compiled/react${ + experimental ? '-experimental' : '' + }/react.shared-subset`, }, }, }, diff --git a/test/e2e/app-dir/third-parties/app/google-maps-embed/page.js b/test/e2e/app-dir/third-parties/app/google-maps-embed/page.js index 28210fe9bab6f..f9e312c9116f9 100644 --- a/test/e2e/app-dir/third-parties/app/google-maps-embed/page.js +++ b/test/e2e/app-dir/third-parties/app/google-maps-embed/page.js @@ -2,7 +2,7 @@ import { GoogleMapsEmbed } from '@next/third-parties/google' const Page = () => { return ( -
+

Google Maps Embed

{ return ( -
+

Youtube Embed

From 187df91438200546516ed54ae7ef4e561a64c8b8 Mon Sep 17 00:00:00 2001 From: Hamir Mahal Date: Wed, 4 Oct 2023 13:41:47 -0700 Subject: [PATCH 09/10] fix: `.../templates/*/app/layout.*` import order (#56380) - fixes #56379 --- packages/create-next-app/templates/app-tw/js/app/layout.js | 2 +- packages/create-next-app/templates/app-tw/ts/app/layout.tsx | 2 +- packages/create-next-app/templates/app/js/app/layout.js | 2 +- packages/create-next-app/templates/app/ts/app/layout.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/create-next-app/templates/app-tw/js/app/layout.js b/packages/create-next-app/templates/app-tw/js/app/layout.js index c93f80617d1d1..821f712283060 100644 --- a/packages/create-next-app/templates/app-tw/js/app/layout.js +++ b/packages/create-next-app/templates/app-tw/js/app/layout.js @@ -1,5 +1,5 @@ -import './globals.css' import { Inter } from 'next/font/google' +import './globals.css' const inter = Inter({ subsets: ['latin'] }) diff --git a/packages/create-next-app/templates/app-tw/ts/app/layout.tsx b/packages/create-next-app/templates/app-tw/ts/app/layout.tsx index ae8456212360d..40e027fbefc15 100644 --- a/packages/create-next-app/templates/app-tw/ts/app/layout.tsx +++ b/packages/create-next-app/templates/app-tw/ts/app/layout.tsx @@ -1,6 +1,6 @@ -import './globals.css' import type { Metadata } from 'next' import { Inter } from 'next/font/google' +import './globals.css' const inter = Inter({ subsets: ['latin'] }) diff --git a/packages/create-next-app/templates/app/js/app/layout.js b/packages/create-next-app/templates/app/js/app/layout.js index c93f80617d1d1..821f712283060 100644 --- a/packages/create-next-app/templates/app/js/app/layout.js +++ b/packages/create-next-app/templates/app/js/app/layout.js @@ -1,5 +1,5 @@ -import './globals.css' import { Inter } from 'next/font/google' +import './globals.css' const inter = Inter({ subsets: ['latin'] }) diff --git a/packages/create-next-app/templates/app/ts/app/layout.tsx b/packages/create-next-app/templates/app/ts/app/layout.tsx index ae8456212360d..40e027fbefc15 100644 --- a/packages/create-next-app/templates/app/ts/app/layout.tsx +++ b/packages/create-next-app/templates/app/ts/app/layout.tsx @@ -1,6 +1,6 @@ -import './globals.css' import type { Metadata } from 'next' import { Inter } from 'next/font/google' +import './globals.css' const inter = Inter({ subsets: ['latin'] }) From ae1b89984d26b2af3658001fa19a19e1e77c312d Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 4 Oct 2023 15:21:39 -0600 Subject: [PATCH 10/10] Async Batcher (#56423) There's lots of situations in Next.js where we want to ensure that only one operation is in progress at a time for a given task. An example of this is our response cache. The expectation is that for multiple requests for the same page should only result in a single invocation. This isn't new behavior, but this abstracts the batching interface away so we don't duplicate it. --- packages/next/src/build/utils.ts | 1 + packages/next/src/export/worker.ts | 1 + packages/next/src/lib/batcher.test.ts | 78 +++++ packages/next/src/lib/batcher.ts | 94 ++++++ .../lib/polyfill-promise-with-resolvers.ts | 27 ++ packages/next/src/lib/worker.ts | 2 +- .../src/server/dev/on-demand-entry-handler.ts | 77 +++-- .../src/server/dev/static-paths-worker.ts | 1 + packages/next/src/server/lib/router-server.ts | 1 + .../src/server/lib/schedule-on-next-tick.ts | 5 +- packages/next/src/server/next-server.ts | 1 + packages/next/src/server/node-environment.ts | 28 -- .../next/src/server/response-cache/index.ts | 284 ++++++++---------- 13 files changed, 375 insertions(+), 225 deletions(-) create mode 100644 packages/next/src/lib/batcher.test.ts create mode 100644 packages/next/src/lib/batcher.ts create mode 100644 packages/next/src/lib/polyfill-promise-with-resolvers.ts diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index bd2976b75d88a..b12532aad7b14 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -21,6 +21,7 @@ import '../server/require-hook' import '../server/node-polyfill-fetch' import '../server/node-polyfill-crypto' import '../server/node-environment' +import '../lib/polyfill-promise-with-resolvers' import { green, yellow, red, cyan, bold, underline } from '../lib/picocolors' import getGzipSize from 'next/dist/compiled/gzip-size' diff --git a/packages/next/src/export/worker.ts b/packages/next/src/export/worker.ts index de3d1af82b84a..a3ee08796c1fd 100644 --- a/packages/next/src/export/worker.ts +++ b/packages/next/src/export/worker.ts @@ -11,6 +11,7 @@ import type { import '../server/node-polyfill-fetch' import '../server/node-polyfill-web-streams' import '../server/node-environment' +import '../lib/polyfill-promise-with-resolvers' process.env.NEXT_IS_EXPORT_WORKER = 'true' diff --git a/packages/next/src/lib/batcher.test.ts b/packages/next/src/lib/batcher.test.ts new file mode 100644 index 0000000000000..ef9224129f6d4 --- /dev/null +++ b/packages/next/src/lib/batcher.test.ts @@ -0,0 +1,78 @@ +import { Batcher } from './batcher' + +describe('Batcher', () => { + describe('batch', () => { + it('should execute the work function immediately', async () => { + const batcher = Batcher.create() + const workFn = jest.fn().mockResolvedValue(42) + + const result = await batcher.batch('key', workFn) + + expect(result).toBe(42) + expect(workFn).toHaveBeenCalledTimes(1) + }) + + it('should batch multiple calls to the same key', async () => { + const batcher = Batcher.create() + const workFn = jest.fn().mockResolvedValue(42) + + const result1 = batcher.batch('key', workFn) + const result2 = batcher.batch('key', workFn) + + expect(result1).toBeInstanceOf(Promise) + expect(result2).toBeInstanceOf(Promise) + expect(workFn).toHaveBeenCalledTimes(1) + + const [value1, value2] = await Promise.all([result1, result2]) + + expect(value1).toBe(42) + expect(value2).toBe(42) + expect(workFn).toHaveBeenCalledTimes(1) + }) + + it('should not batch calls to different keys', async () => { + const batcher = Batcher.create() + const workFn = jest.fn((key) => key) + + const result1 = batcher.batch('key1', workFn) + const result2 = batcher.batch('key2', workFn) + + expect(result1).toBeInstanceOf(Promise) + expect(result2).toBeInstanceOf(Promise) + expect(workFn).toHaveBeenCalledTimes(2) + + const [value1, value2] = await Promise.all([result1, result2]) + + expect(value1).toBe('key1') + expect(value2).toBe('key2') + expect(workFn).toHaveBeenCalledTimes(2) + }) + + it('should use the cacheKeyFn to generate cache keys', async () => { + const cacheKeyFn = jest.fn().mockResolvedValue('cache-key') + const batcher = Batcher.create({ cacheKeyFn }) + const workFn = jest.fn().mockResolvedValue(42) + + const result = await batcher.batch('key', workFn) + + expect(result).toBe(42) + expect(cacheKeyFn).toHaveBeenCalledWith('key') + expect(workFn).toHaveBeenCalledTimes(1) + }) + + it('should use the schedulerFn to schedule work', async () => { + const schedulerFn = jest.fn().mockImplementation((fn) => fn()) + const batcher = Batcher.create({ schedulerFn }) + const workFn = jest.fn().mockResolvedValue(42) + + const results = await Promise.all([ + batcher.batch('key', workFn), + batcher.batch('key', workFn), + batcher.batch('key', workFn), + ]) + + expect(results).toEqual([42, 42, 42]) + expect(workFn).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/packages/next/src/lib/batcher.ts b/packages/next/src/lib/batcher.ts new file mode 100644 index 0000000000000..480b70df15f00 --- /dev/null +++ b/packages/next/src/lib/batcher.ts @@ -0,0 +1,94 @@ +// This takes advantage of `Promise.withResolvers` which is polyfilled in +// this imported module. +import './polyfill-promise-with-resolvers' + +import { SchedulerFn } from '../server/lib/schedule-on-next-tick' + +type CacheKeyFn = ( + key: K +) => PromiseLike | C + +type BatcherOptions = { + cacheKeyFn?: CacheKeyFn + schedulerFn?: SchedulerFn +} + +type WorkFn = ( + key: C, + resolve: (value: V | PromiseLike) => void +) => Promise + +/** + * A wrapper for a function that will only allow one call to the function to + * execute at a time. + */ +export class Batcher { + private readonly pending = new Map>() + + protected constructor( + private readonly cacheKeyFn?: CacheKeyFn, + /** + * A function that will be called to schedule the wrapped function to be + * executed. This defaults to a function that will execute the function + * immediately. + */ + private readonly schedulerFn: SchedulerFn = (fn) => fn() + ) {} + + /** + * Creates a new instance of PendingWrapper. If the key extends a string or + * number, the key will be used as the cache key. If the key is an object, a + * cache key function must be provided. + */ + public static create( + options?: BatcherOptions + ): Batcher + public static create( + options: BatcherOptions & + Required, 'cacheKeyFn'>> + ): Batcher + public static create( + options?: BatcherOptions + ): Batcher { + return new Batcher(options?.cacheKeyFn, options?.schedulerFn) + } + + /** + * Wraps a function in a promise that will be resolved or rejected only once + * for a given key. This will allow multiple calls to the function to be + * made, but only one will be executed at a time. The result of the first + * call will be returned to all callers. + * + * @param key the key to use for the cache + * @param fn the function to wrap + * @returns a promise that resolves to the result of the function + */ + public async batch(key: K, fn: WorkFn): Promise { + const cacheKey = (this.cacheKeyFn ? await this.cacheKeyFn(key) : key) as C + if (cacheKey === null) { + return fn(cacheKey, Promise.resolve) + } + + const pending = this.pending.get(cacheKey) + if (pending) return pending + + const { promise, resolve, reject } = Promise.withResolvers() + this.pending.set(cacheKey, promise) + + this.schedulerFn(async () => { + try { + const result = await fn(cacheKey, resolve) + + // Resolving a promise multiple times is a no-op, so we can safely + // resolve all pending promises with the same result. + resolve(result) + } catch (err) { + reject(err) + } finally { + this.pending.delete(cacheKey) + } + }) + + return promise + } +} diff --git a/packages/next/src/lib/polyfill-promise-with-resolvers.ts b/packages/next/src/lib/polyfill-promise-with-resolvers.ts new file mode 100644 index 0000000000000..80144878eef2b --- /dev/null +++ b/packages/next/src/lib/polyfill-promise-with-resolvers.ts @@ -0,0 +1,27 @@ +// This adds a `Promise.withResolvers` polyfill. This will soon be adopted into +// the spec. +// +// TODO: remove this polyfill when it is adopted into the spec. +// +// https://tc39.es/proposal-promise-with-resolvers/ +// +if ( + !('withResolvers' in Promise) || + typeof Promise.withResolvers !== 'function' +) { + Promise.withResolvers = () => { + let resolvers: { + resolve: (value: T | PromiseLike) => void + reject: (reason: any) => void + } + + // Create the promise and assign the resolvers to the object. + const promise = new Promise((resolve, reject) => { + resolvers = { resolve, reject } + }) + + // We know that resolvers is defined because the Promise constructor runs + // synchronously. + return { promise, resolve: resolvers!.resolve, reject: resolvers!.reject } + } +} diff --git a/packages/next/src/lib/worker.ts b/packages/next/src/lib/worker.ts index bd0ebd2e06381..05f1bf4ccf2cd 100644 --- a/packages/next/src/lib/worker.ts +++ b/packages/next/src/lib/worker.ts @@ -3,7 +3,7 @@ import { Worker as JestWorker } from 'next/dist/compiled/jest-worker' import { getNodeOptionsWithoutInspect } from '../server/lib/utils' // We need this as we're using `Promise.withResolvers` which is not available in the node typings -import '../server/node-environment' +import '../lib/polyfill-promise-with-resolvers' type FarmOptions = ConstructorParameters[1] diff --git a/packages/next/src/server/dev/on-demand-entry-handler.ts b/packages/next/src/server/dev/on-demand-entry-handler.ts index b6cfa1e556b06..c5ef3a5802cef 100644 --- a/packages/next/src/server/dev/on-demand-entry-handler.ts +++ b/packages/next/src/server/dev/on-demand-entry-handler.ts @@ -39,6 +39,7 @@ import HotReloader from './hot-reloader-webpack' import { isAppPageRouteDefinition } from '../future/route-definitions/app-page-route-definition' import { scheduleOnNextTick } from '../lib/schedule-on-next-tick' import { RouteDefinition } from '../future/route-definitions/route-definition' +import { Batcher } from '../../lib/batcher' const debug = origDebug('next:on-demand-entry-handler') @@ -878,8 +879,34 @@ export function onDemandEntryHandler({ } } + type EnsurePageOptions = { + page: string + clientOnly: boolean + appPaths?: ReadonlyArray | null + match?: RouteMatch + isApp?: boolean + } + // Make sure that we won't have multiple invalidations ongoing concurrently. - const curEnsurePage = new Map>() + const batcher = Batcher.create< + Omit & { + definition?: RouteDefinition + }, + void, + string + >({ + // The cache key here is composed of the elements that affect the + // compilation, namely, the page, whether it's client only, and whether + // it's an app page. This ensures that we don't have multiple compilations + // for the same page happening concurrently. + // + // We don't include the whole match because it contains match specific + // parameters (like route params) that would just bust this cache. Any + // details that would possibly bust the cache should be listed here. + cacheKeyFn: (options) => JSON.stringify(options), + // Schedule the invocation of the ensurePageImpl function on the next tick. + schedulerFn: scheduleOnNextTick, + }) return { async ensurePage({ @@ -888,13 +915,7 @@ export function onDemandEntryHandler({ appPaths = null, match, isApp, - }: { - page: string - clientOnly: boolean - appPaths?: ReadonlyArray | null - match?: RouteMatch - isApp?: boolean - }) { + }: EnsurePageOptions) { // If the route is actually an app page route, then we should have access // to the app route match, and therefore, the appPaths from it. if ( @@ -905,43 +926,15 @@ export function onDemandEntryHandler({ appPaths = match.definition.appPaths } - // The cache key here is composed of the elements that affect the - // compilation, namely, the page, whether it's client only, and whether - // it's an app page. This ensures that we don't have multiple compilations + // Wrap the invocation of the ensurePageImpl function in the pending + // wrapper, which will ensure that we don't have multiple compilations // for the same page happening concurrently. - // - // We don't include the whole match because it contains match specific - // parameters (like route params) that would just bust this cache. Any - // details that would possibly bust the cache should be listed here. - const key = JSON.stringify({ - page, - clientOnly, - appPaths, - definition: match?.definition, - isApp, - }) - - // See if we're already building this page. - const pending = curEnsurePage.get(key) - if (pending) return pending - - const { promise, resolve, reject } = Promise.withResolvers() - curEnsurePage.set(key, promise) - - // Schedule the build to occur on the next tick, but don't wait and - // instead return the promise immediately. - scheduleOnNextTick(async () => { - try { + return batcher.batch( + { page, clientOnly, appPaths, definition: match?.definition, isApp }, + async () => { await ensurePageImpl({ page, clientOnly, appPaths, match, isApp }) - resolve() - } catch (err) { - reject(err) - } finally { - curEnsurePage.delete(key) } - }) - - return promise + ) }, onHMR(client: ws, getHmrServerError: () => Error | null) { let bufferedHmrServerError: Error | null = null diff --git a/packages/next/src/server/dev/static-paths-worker.ts b/packages/next/src/server/dev/static-paths-worker.ts index cd7d5305fa306..d357a170fefbf 100644 --- a/packages/next/src/server/dev/static-paths-worker.ts +++ b/packages/next/src/server/dev/static-paths-worker.ts @@ -3,6 +3,7 @@ import type { NextConfigComplete } from '../config-shared' import '../require-hook' import '../node-polyfill-fetch' import '../node-environment' +import '../../lib/polyfill-promise-with-resolvers' import { buildAppStaticPaths, diff --git a/packages/next/src/server/lib/router-server.ts b/packages/next/src/server/lib/router-server.ts index c8158e295baf2..b16858a506458 100644 --- a/packages/next/src/server/lib/router-server.ts +++ b/packages/next/src/server/lib/router-server.ts @@ -7,6 +7,7 @@ import type { WorkerRequestHandler, WorkerUpgradeHandler } from './types' import '../node-polyfill-fetch' import '../node-environment' import '../require-hook' +import '../../lib/polyfill-promise-with-resolvers' import url from 'url' import path from 'path' diff --git a/packages/next/src/server/lib/schedule-on-next-tick.ts b/packages/next/src/server/lib/schedule-on-next-tick.ts index 7004d0e953ae3..8374d4bd692b0 100644 --- a/packages/next/src/server/lib/schedule-on-next-tick.ts +++ b/packages/next/src/server/lib/schedule-on-next-tick.ts @@ -1,10 +1,11 @@ -type ScheduledFn = () => T | PromiseLike +export type ScheduledFn = () => T | PromiseLike +export type SchedulerFn = (cb: ScheduledFn) => void /** * Schedules a function to be called on the next tick after the other promises * have been resolved. */ -export function scheduleOnNextTick(cb: ScheduledFn): void { +export const scheduleOnNextTick = (cb: ScheduledFn): void => { // We use Promise.resolve().then() here so that the operation is scheduled at // the end of the promise job queue, we then add it to the next process tick // to ensure it's evaluated afterwards. diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 718214798b2c4..ead7b51bc8fed 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -4,6 +4,7 @@ import './node-polyfill-fetch' import './node-polyfill-form' import './node-polyfill-web-streams' import './node-polyfill-crypto' +import '../lib/polyfill-promise-with-resolvers' import type { TLSSocket } from 'tls' import { diff --git a/packages/next/src/server/node-environment.ts b/packages/next/src/server/node-environment.ts index 88780ed6dc8b8..fdd0d585dc9ce 100644 --- a/packages/next/src/server/node-environment.ts +++ b/packages/next/src/server/node-environment.ts @@ -14,31 +14,3 @@ if (typeof (globalThis as any).WebSocket !== 'function') { }, }) } - -// This adds a `Promise.withResolvers` polyfill. This will soon be adopted into -// the spec. -// -// TODO: remove this polyfill when it is adopted into the spec. -// -// https://tc39.es/proposal-promise-with-resolvers/ -// -if ( - !('withResolvers' in Promise) || - typeof Promise.withResolvers !== 'function' -) { - Promise.withResolvers = () => { - let resolvers: { - resolve: (value: T | PromiseLike) => void - reject: (reason: any) => void - } - - // Create the promise and assign the resolvers to the object. - const promise = new Promise((resolve, reject) => { - resolvers = { resolve, reject } - }) - - // We know that resolvers is defined because the Promise constructor runs - // synchronously. - return { promise, resolve: resolvers!.resolve, reject: resolvers!.reject } - } -} diff --git a/packages/next/src/server/response-cache/index.ts b/packages/next/src/server/response-cache/index.ts index 7bd6710cc4a80..9a0451e4dbe78 100644 --- a/packages/next/src/server/response-cache/index.ts +++ b/packages/next/src/server/response-cache/index.ts @@ -6,20 +6,36 @@ import type { } from './types' import RenderResult from '../render-result' +import { Batcher } from '../../lib/batcher' +import { scheduleOnNextTick } from '../lib/schedule-on-next-tick' export * from './types' export default class ResponseCache { - pendingResponses: Map> - previousCacheItem?: { + private readonly batcher = Batcher.create< + { key: string; isOnDemandRevalidate: boolean }, + ResponseCacheEntry | null, + string + >({ + // Ensure on-demand revalidate doesn't block normal requests, it should be + // safe to run an on-demand revalidate for the same key as a normal request. + cacheKeyFn: ({ key, isOnDemandRevalidate }) => + `${key}-${isOnDemandRevalidate ? '1' : '0'}`, + // We wait to do any async work until after we've added our promise to + // `pendingResponses` to ensure that any any other calls will reuse the + // same promise until we've fully finished our work. + schedulerFn: scheduleOnNextTick, + }) + + private previousCacheItem?: { key: string entry: ResponseCacheEntry | null expiresAt: number } - minimalMode?: boolean + + private minimalMode?: boolean constructor(minimalMode: boolean) { - this.pendingResponses = new Map() // this is a hack to avoid Webpack knowing this is equal to this.minimalMode // because we replace this.minimalMode to true in production bundles. const minimalModeKey = 'minimalMode' @@ -35,166 +51,130 @@ export default class ResponseCache { incrementalCache: IncrementalCache } ): Promise { - const { incrementalCache } = context - // ensure on-demand revalidate doesn't block normal requests - const pendingResponseKey = key - ? `${key}-${context.isOnDemandRevalidate ? '1' : '0'}` - : null - - const pendingResponse = pendingResponseKey - ? this.pendingResponses.get(pendingResponseKey) - : null - - if (pendingResponse) { - return pendingResponse - } - - let resolver: (cacheEntry: ResponseCacheEntry | null) => void = () => {} - let rejecter: (error: Error) => void = () => {} - const promise: Promise = new Promise( - (resolve, reject) => { - resolver = resolve - rejecter = reject - } - ) - if (pendingResponseKey) { - this.pendingResponses.set(pendingResponseKey, promise) - } - - let resolved = false - const resolve = (cacheEntry: ResponseCacheEntry | null) => { - if (pendingResponseKey) { - // Ensure all reads from the cache get the latest value. - this.pendingResponses.set( - pendingResponseKey, - Promise.resolve(cacheEntry) - ) - } - if (!resolved) { - resolved = true - resolver(cacheEntry) - } - } + // If there is no key for the cache, we can't possibly look this up in the + // cache so just return the result of the response generator. + if (!key) return responseGenerator(false, null) + + const { incrementalCache, isOnDemandRevalidate = false } = context + + return this.batcher.batch( + { key, isOnDemandRevalidate }, + async (cacheKey, resolve) => { + // We keep the previous cache entry around to leverage when the + // incremental cache is disabled in minimal mode. + if ( + this.minimalMode && + this.previousCacheItem?.key === cacheKey && + this.previousCacheItem.expiresAt > Date.now() + ) { + return this.previousCacheItem.entry + } - // we keep the previous cache entry around to leverage - // when the incremental cache is disabled in minimal mode - if ( - pendingResponseKey && - this.minimalMode && - this.previousCacheItem?.key === pendingResponseKey && - this.previousCacheItem.expiresAt > Date.now() - ) { - resolve(this.previousCacheItem.entry) - this.pendingResponses.delete(pendingResponseKey) - return promise - } + let resolved = false + let cachedResponse: IncrementalCacheItem = null + try { + cachedResponse = !this.minimalMode + ? await incrementalCache.get(key) + : null + + if (cachedResponse && !isOnDemandRevalidate) { + if (cachedResponse.value?.kind === 'FETCH') { + throw new Error( + `invariant: unexpected cachedResponse of kind fetch in response cache` + ) + } - // We wait to do any async work until after we've added our promise to - // `pendingResponses` to ensure that any any other calls will reuse the - // same promise until we've fully finished our work. - ;(async () => { - let cachedResponse: IncrementalCacheItem = null - try { - cachedResponse = - key && !this.minimalMode ? await incrementalCache.get(key) : null - - if (cachedResponse && !context.isOnDemandRevalidate) { - if (cachedResponse.value?.kind === 'FETCH') { - throw new Error( - `invariant: unexpected cachedResponse of kind fetch in response cache` - ) + resolve({ + isStale: cachedResponse.isStale, + revalidate: cachedResponse.curRevalidate, + value: + cachedResponse.value?.kind === 'PAGE' + ? { + kind: 'PAGE', + html: RenderResult.fromStatic(cachedResponse.value.html), + pageData: cachedResponse.value.pageData, + headers: cachedResponse.value.headers, + status: cachedResponse.value.status, + } + : cachedResponse.value, + }) + resolved = true + + if (!cachedResponse.isStale || context.isPrefetch) { + // The cached value is still valid, so we don't need + // to update it yet. + return null + } } - resolve({ - isStale: cachedResponse.isStale, - revalidate: cachedResponse.curRevalidate, - value: - cachedResponse.value?.kind === 'PAGE' - ? { - kind: 'PAGE', - html: RenderResult.fromStatic(cachedResponse.value.html), - pageData: cachedResponse.value.pageData, - headers: cachedResponse.value.headers, - status: cachedResponse.value.status, - } - : cachedResponse.value, - }) - if (!cachedResponse.isStale || context.isPrefetch) { - // The cached value is still valid, so we don't need - // to update it yet. - return + const cacheEntry = await responseGenerator(resolved, cachedResponse) + const resolveValue = + cacheEntry === null + ? null + : { + ...cacheEntry, + isMiss: !cachedResponse, + } + + // For on-demand revalidate wait to resolve until cache is set. + // Otherwise resolve now. + if (!isOnDemandRevalidate && !resolved) { + resolve(resolveValue) + resolved = true } - } - const cacheEntry = await responseGenerator(resolved, cachedResponse) - const resolveValue = - cacheEntry === null - ? null - : { - ...cacheEntry, - isMiss: !cachedResponse, + if (cacheEntry && typeof cacheEntry.revalidate !== 'undefined') { + if (this.minimalMode) { + this.previousCacheItem = { + key: cacheKey, + entry: cacheEntry, + expiresAt: Date.now() + 1000, } - - // for on-demand revalidate wait to resolve until cache is set - if (!context.isOnDemandRevalidate) { - resolve(resolveValue) - } - - if (key && cacheEntry && typeof cacheEntry.revalidate !== 'undefined') { - if (this.minimalMode) { - this.previousCacheItem = { - key: pendingResponseKey || key, - entry: cacheEntry, - expiresAt: Date.now() + 1000, + } else { + await incrementalCache.set( + key, + cacheEntry.value?.kind === 'PAGE' + ? { + kind: 'PAGE', + html: cacheEntry.value.html.toUnchunkedString(), + pageData: cacheEntry.value.pageData, + headers: cacheEntry.value.headers, + status: cacheEntry.value.status, + } + : cacheEntry.value, + { + revalidate: cacheEntry.revalidate, + } + ) } } else { - await incrementalCache.set( - key, - cacheEntry.value?.kind === 'PAGE' - ? { - kind: 'PAGE', - html: cacheEntry.value.html.toUnchunkedString(), - pageData: cacheEntry.value.pageData, - headers: cacheEntry.value.headers, - status: cacheEntry.value.status, - } - : cacheEntry.value, - { - revalidate: cacheEntry.revalidate, - } - ) + this.previousCacheItem = undefined } - } else { - this.previousCacheItem = undefined - } - if (context.isOnDemandRevalidate) { - resolve(resolveValue) - } - } catch (err) { - // when a getStaticProps path is erroring we automatically re-set the - // existing cache under a new expiration to prevent non-stop retrying - if (cachedResponse && key) { - await incrementalCache.set(key, cachedResponse.value, { - revalidate: Math.min( - Math.max(cachedResponse.revalidate || 3, 3), - 30 - ), - }) - } - // while revalidating in the background we can't reject as - // we already resolved the cache entry so log the error here - if (resolved) { - console.error(err) - } else { - rejecter(err as Error) - } - } finally { - if (pendingResponseKey) { - this.pendingResponses.delete(pendingResponseKey) + return resolveValue + } catch (err) { + // When a getStaticProps path is erroring we automatically re-set the + // existing cache under a new expiration to prevent non-stop retrying. + if (cachedResponse) { + await incrementalCache.set(key, cachedResponse.value, { + revalidate: Math.min( + Math.max(cachedResponse.revalidate || 3, 3), + 30 + ), + }) + } + + // While revalidating in the background we can't reject as we already + // resolved the cache entry so log the error here. + if (resolved) { + console.error(err) + return null + } + + // We haven't resolved yet, so let's throw to indicate an error. + throw err } } - })() - return promise + ) } }