From c1117bcb254130da1167cca43fd93dec0695f46e Mon Sep 17 00:00:00 2001 From: vercel-release-bot Date: Wed, 22 Jan 2025 23:24:09 +0000 Subject: [PATCH 01/61] v15.2.0-canary.21 --- 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-refresh-utils/package.json | 2 +- packages/third-parties/package.json | 4 ++-- pnpm-lock.yaml | 16 ++++++++-------- 17 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lerna.json b/lerna.json index 9cec6233167aa6..2aa5efee6e3b13 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "15.2.0-canary.20" + "version": "15.2.0-canary.21" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index d44785a2055ce0..53013047ed5ec7 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "15.2.0-canary.20", + "version": "15.2.0-canary.21", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index ae9e00ac3a0173..8c55583a59d3d4 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "15.2.0-canary.20", + "version": "15.2.0-canary.21", "description": "ESLint configuration used by Next.js.", "main": "index.js", "license": "MIT", @@ -10,7 +10,7 @@ }, "homepage": "https://nextjs.org/docs/app/api-reference/config/eslint", "dependencies": { - "@next/eslint-plugin-next": "15.2.0-canary.20", + "@next/eslint-plugin-next": "15.2.0-canary.21", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 930a57d985fa7d..76e137b9b7aa30 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": "15.2.0-canary.20", + "version": "15.2.0-canary.21", "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 5c066c22b3308a..833edeec0a78aa 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "15.2.0-canary.20", + "version": "15.2.0-canary.21", "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 17eadf31cfa62d..40fe0269cf190f 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "15.2.0-canary.20", + "version": "15.2.0-canary.21", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 60bff718768330..0c5b23ee6d1040 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "15.2.0-canary.20", + "version": "15.2.0-canary.21", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index aa3bae4bcdf8fe..7b389f3704650e 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "15.2.0-canary.20", + "version": "15.2.0-canary.21", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 0107d32d6b163d..4589df43d61a49 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "15.2.0-canary.20", + "version": "15.2.0-canary.21", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 4705194ae56b37..f07107796fd4c1 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "15.2.0-canary.20", + "version": "15.2.0-canary.21", "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 361ab76007301e..00adef22a39690 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "15.2.0-canary.20", + "version": "15.2.0-canary.21", "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 e72aa576458de8..30c427e0510300 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "15.2.0-canary.20", + "version": "15.2.0-canary.21", "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 32fd73d640cb2b..f440e5e77204a1 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "15.2.0-canary.20", + "version": "15.2.0-canary.21", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", diff --git a/packages/next/package.json b/packages/next/package.json index 2fcffa3efb33ac..ce410f58204ccc 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "15.2.0-canary.20", + "version": "15.2.0-canary.21", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -99,7 +99,7 @@ ] }, "dependencies": { - "@next/env": "15.2.0-canary.20", + "@next/env": "15.2.0-canary.21", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -164,11 +164,11 @@ "@jest/types": "29.5.0", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/font": "15.2.0-canary.20", - "@next/polyfill-module": "15.2.0-canary.20", - "@next/polyfill-nomodule": "15.2.0-canary.20", - "@next/react-refresh-utils": "15.2.0-canary.20", - "@next/swc": "15.2.0-canary.20", + "@next/font": "15.2.0-canary.21", + "@next/polyfill-module": "15.2.0-canary.21", + "@next/polyfill-nomodule": "15.2.0-canary.21", + "@next/react-refresh-utils": "15.2.0-canary.21", + "@next/swc": "15.2.0-canary.21", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.41.2", "@storybook/addon-essentials": "^8.4.7", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 4e9abc3216093f..0f90faec63f3ca 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": "15.2.0-canary.20", + "version": "15.2.0-canary.21", "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 859410653646fa..98c78e8f24400a 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "15.2.0-canary.20", + "version": "15.2.0-canary.21", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "15.2.0-canary.20", + "next": "15.2.0-canary.21", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "5.7.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd7e2405de53a6..7522189e22a6e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -793,7 +793,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 15.2.0-canary.20 + specifier: 15.2.0-canary.21 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.10.3 @@ -857,7 +857,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 15.2.0-canary.20 + specifier: 15.2.0-canary.21 version: link:../next-env '@swc/counter': specifier: 0.1.3 @@ -985,19 +985,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 15.2.0-canary.20 + specifier: 15.2.0-canary.21 version: link:../font '@next/polyfill-module': - specifier: 15.2.0-canary.20 + specifier: 15.2.0-canary.21 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 15.2.0-canary.20 + specifier: 15.2.0-canary.21 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 15.2.0-canary.20 + specifier: 15.2.0-canary.21 version: link:../react-refresh-utils '@next/swc': - specifier: 15.2.0-canary.20 + specifier: 15.2.0-canary.21 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1661,7 +1661,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 15.2.0-canary.20 + specifier: 15.2.0-canary.21 version: link:../next outdent: specifier: 0.8.0 From 33dc0f2f90bfa1509d447a137fdc7fe6c5c92e97 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Thu, 23 Jan 2025 00:38:10 +0100 Subject: [PATCH 02/61] Error handling for hanging promises in `"use cache"` closures (#74750) Based on #74652, this adds error handling for `"use cache"` closures that use closed-over hanging promises. It uses a similar technique to encode the hanging promises into the serialized bound arguments string. In addition, we need to notify the cache signal when decrypting the bound args, since this operation does not resolve in the current task. This is now the same behavior as for encrypting the bound args (see #73521). --- .../server/app-render/dynamic-rendering.ts | 30 ++++++++ .../next/src/server/app-render/encryption.ts | 74 ++++++++++++++++--- .../server/app-render/postponed-state.test.ts | 33 +++++---- .../server/resume-data-cache/cache-store.ts | 6 ++ .../resume-data-cache/resume-data-cache.ts | 20 +++++ .../stream-utils/node-web-streams-helper.ts | 7 +- .../src/server/use-cache/use-cache-wrapper.ts | 32 ++------ .../dynamic-io.server-action.test.ts | 7 +- .../app/bound-args/page.tsx | 25 +++++++ .../use-cache-hanging-inputs/next.config.js | 1 + .../use-cache-hanging-inputs.test.ts | 55 ++++++++++++++ 11 files changed, 236 insertions(+), 54 deletions(-) create mode 100644 test/e2e/app-dir/use-cache-hanging-inputs/app/bound-args/page.tsx diff --git a/packages/next/src/server/app-render/dynamic-rendering.ts b/packages/next/src/server/app-render/dynamic-rendering.ts index 45c9501a08be67..06b958e5a3e5a1 100644 --- a/packages/next/src/server/app-render/dynamic-rendering.ts +++ b/packages/next/src/server/app-render/dynamic-rendering.ts @@ -41,6 +41,7 @@ import { VIEWPORT_BOUNDARY_NAME, OUTLET_BOUNDARY_NAME, } from '../../lib/metadata/metadata-constants' +import { scheduleOnNextTick } from '../../lib/scheduler' const hasPostpone = typeof React.unstable_postpone === 'function' @@ -518,6 +519,35 @@ export function createPostponedAbortSignal(reason: string): AbortSignal { return controller.signal } +/** + * In a prerender, we may end up with hanging Promises as inputs due them + * stalling on connection() or because they're loading dynamic data. In that + * case we need to abort the encoding of arguments since they'll never complete. + */ +export function createHangingInputAbortSignal( + workUnitStore: PrerenderStoreModern +): AbortSignal { + const controller = new AbortController() + + if (workUnitStore.cacheSignal) { + // If we have a cacheSignal it means we're in a prospective render. If the input + // we're waiting on is coming from another cache, we do want to wait for it so that + // we can resolve this cache entry too. + workUnitStore.cacheSignal.inputReady().then(() => { + controller.abort() + }) + } else { + // Otherwise we're in the final render and we should already have all our caches + // filled. We might still be waiting on some microtasks so we wait one tick before + // giving up. When we give up, we still want to render the content of this cache + // as deeply as we can so that we can suspend as deeply as possible in the tree + // or not at all if we don't end up waiting for the input. + scheduleOnNextTick(() => controller.abort()) + } + + return controller.signal +} + export function annotateDynamicAccess( expression: string, prerenderStore: PrerenderStoreModern diff --git a/packages/next/src/server/app-render/encryption.ts b/packages/next/src/server/app-render/encryption.ts index 83b799673c57b6..8e5ecaf4a0311e 100644 --- a/packages/next/src/server/app-render/encryption.ts +++ b/packages/next/src/server/app-render/encryption.ts @@ -21,12 +21,16 @@ import { getRenderResumeDataCache, workUnitAsyncStorage, } from './work-unit-async-storage.external' +import { createHangingInputAbortSignal } from './dynamic-rendering' const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge' const textEncoder = new TextEncoder() const textDecoder = new TextDecoder() +/** + * Decrypt the serialized string with the action id as the salt. + */ async function decodeActionBoundArg(actionId: string, arg: string) { const key = await getActionEncryptionKey() if (typeof key === 'undefined') { @@ -81,17 +85,29 @@ async function encodeActionBoundArg(actionId: string, arg: string) { export async function encryptActionBoundArgs(actionId: string, args: any[]) { const { clientModules } = getClientReferenceManifestForRsc() - // Create an error before any asynchrounous calls, to capture the original + // Create an error before any asynchronous calls, to capture the original // call stack in case we need it when the serialization errors. const error = new Error() Error.captureStackTrace(error, encryptActionBoundArgs) let didCatchError = false + const workUnitStore = workUnitAsyncStorage.getStore() + + const hangingInputAbortSignal = + workUnitStore?.type === 'prerender' + ? createHangingInputAbortSignal(workUnitStore) + : undefined + // Using Flight to serialize the args into a string. const serialized = await streamToString( renderToReadableStream(args, clientModules, { + signal: hangingInputAbortSignal, onError(err) { + if (hangingInputAbortSignal?.aborted) { + return + } + // We're only reporting one error at a time, starting with the first. if (didCatchError) { return @@ -103,7 +119,11 @@ export async function encryptActionBoundArgs(actionId: string, args: any[]) { // stack, because err.stack is a useless Flight Server call stack. error.message = err instanceof Error ? err.message : String(err) }, - }) + }), + // We pass the abort signal to `streamToString` so that no chunks are + // included that are emitted after the signal was already aborted. This + // ensures that we can encode hanging promises. + hangingInputAbortSignal ) if (didCatchError) { @@ -117,8 +137,6 @@ export async function encryptActionBoundArgs(actionId: string, args: any[]) { throw error } - const workUnitStore = workUnitAsyncStorage.getStore() - if (!workUnitStore) { return encodeActionBoundArg(actionId, serialized) } @@ -151,20 +169,58 @@ export async function encryptActionBoundArgs(actionId: string, args: any[]) { // Decrypts the action's bound args from the encrypted string. export async function decryptActionBoundArgs( actionId: string, - encrypted: Promise + encryptedPromise: Promise ) { + const encrypted = await encryptedPromise + const workUnitStore = workUnitAsyncStorage.getStore() + + let decrypted: string | undefined + + if (workUnitStore) { + const cacheSignal = + workUnitStore.type === 'prerender' ? workUnitStore.cacheSignal : undefined + + const prerenderResumeDataCache = getPrerenderResumeDataCache(workUnitStore) + const renderResumeDataCache = getRenderResumeDataCache(workUnitStore) + + decrypted = + prerenderResumeDataCache?.decryptedBoundArgs.get(encrypted) ?? + renderResumeDataCache?.decryptedBoundArgs.get(encrypted) + + if (!decrypted) { + cacheSignal?.beginRead() + decrypted = await decodeActionBoundArg(actionId, encrypted) + cacheSignal?.endRead() + prerenderResumeDataCache?.decryptedBoundArgs.set(encrypted, decrypted) + } + } else { + decrypted = await decodeActionBoundArg(actionId, encrypted) + } + const { edgeRscModuleMapping, rscModuleMapping } = getClientReferenceManifestForRsc() - // Decrypt the serialized string with the action id as the salt. - const decrypted = await decodeActionBoundArg(actionId, await encrypted) - // Using Flight to deserialize the args from the string. const deserialized = await createFromReadableStream( new ReadableStream({ start(controller) { controller.enqueue(textEncoder.encode(decrypted)) - controller.close() + + if (workUnitStore?.type === 'prerender') { + // Explicitly don't close the stream here (until prerendering is + // complete) so that hanging promises are not rejected. + if (workUnitStore.renderSignal.aborted) { + controller.close() + } else { + workUnitStore.renderSignal.addEventListener( + 'abort', + () => controller.close(), + { once: true } + ) + } + } else { + controller.close() + } }, }), { diff --git a/packages/next/src/server/app-render/postponed-state.test.ts b/packages/next/src/server/app-render/postponed-state.test.ts index 6ec47afb14a7c1..f94fc5563a873c 100644 --- a/packages/next/src/server/app-render/postponed-state.test.ts +++ b/packages/next/src/server/app-render/postponed-state.test.ts @@ -36,22 +36,23 @@ describe('getDynamicHTMLPostponedState', () => { const parsed = parsePostponedState(state, { slug: '123' }) expect(parsed).toMatchInlineSnapshot(` - { - "data": { - "123": "123", - "nested": { - "123": "123", - }, - }, - "renderResumeDataCache": { - "cache": Map { - "1" => Promise {}, - }, - "encryptedBoundArgs": Map {}, - "fetch": Map {}, - }, - "type": 2, - } + { + "data": { + "123": "123", + "nested": { + "123": "123", + }, + }, + "renderResumeDataCache": { + "cache": Map { + "1" => Promise {}, + }, + "decryptedBoundArgs": Map {}, + "encryptedBoundArgs": Map {}, + "fetch": Map {}, + }, + "type": 2, + } `) const value = await parsed.renderResumeDataCache.cache.get('1') diff --git a/packages/next/src/server/resume-data-cache/cache-store.ts b/packages/next/src/server/resume-data-cache/cache-store.ts index 0616b7f447ec2d..c01a2aa0b95e9b 100644 --- a/packages/next/src/server/resume-data-cache/cache-store.ts +++ b/packages/next/src/server/resume-data-cache/cache-store.ts @@ -20,6 +20,12 @@ export type FetchCacheStore = CacheStore */ export type EncryptedBoundArgsCacheStore = CacheStore +/** + * An in-memory-only cache store for decrypted bound args of inline server + * functions. + */ +export type DecryptedBoundArgsCacheStore = CacheStore + /** * Serialized format for "use cache" entries */ diff --git a/packages/next/src/server/resume-data-cache/resume-data-cache.ts b/packages/next/src/server/resume-data-cache/resume-data-cache.ts index 2c0db5b72bc919..27c71757f7533a 100644 --- a/packages/next/src/server/resume-data-cache/resume-data-cache.ts +++ b/packages/next/src/server/resume-data-cache/resume-data-cache.ts @@ -5,6 +5,7 @@ import { type EncryptedBoundArgsCacheStore, serializeUseCacheCacheStore, parseUseCacheCacheStore, + type DecryptedBoundArgsCacheStore, } from './cache-store' /** @@ -29,6 +30,14 @@ export interface RenderResumeDataCache { * The 'set' operation is omitted to enforce immutability. */ readonly encryptedBoundArgs: Omit + + /** + * A read-only Map store for decrypted bound args of inline server functions. + * This is only intended for in-memory usage during pre-rendering, and must + * not be persisted in the resume store. The 'set' operation is omitted to + * enforce immutability. + */ + readonly decryptedBoundArgs: Omit } /** @@ -56,6 +65,14 @@ export interface PrerenderResumeDataCache { * pre-rendering. */ readonly encryptedBoundArgs: EncryptedBoundArgsCacheStore + + /** + * A mutable Map store for decrypted bound args of inline server functions. + * This is only intended for in-memory usage during pre-rendering, and must + * not be persisted in the resume store. Supports both 'get' and 'set' + * operations to build the cache during pre-rendering. + */ + readonly decryptedBoundArgs: DecryptedBoundArgsCacheStore } type ResumeStoreSerialized = { @@ -125,6 +142,7 @@ export function createPrerenderResumeDataCache(): PrerenderResumeDataCache { cache: new Map(), fetch: new Map(), encryptedBoundArgs: new Map(), + decryptedBoundArgs: new Map(), } } @@ -162,6 +180,7 @@ export function createRenderResumeDataCache( cache: new Map(), fetch: new Map(), encryptedBoundArgs: new Map(), + decryptedBoundArgs: new Map(), } } @@ -182,6 +201,7 @@ export function createRenderResumeDataCache( encryptedBoundArgs: new Map( Object.entries(json.store.encryptedBoundArgs) ), + decryptedBoundArgs: new Map(), } } } diff --git a/packages/next/src/server/stream-utils/node-web-streams-helper.ts b/packages/next/src/server/stream-utils/node-web-streams-helper.ts index 4e07dd231c6b06..98e389e2645816 100644 --- a/packages/next/src/server/stream-utils/node-web-streams-helper.ts +++ b/packages/next/src/server/stream-utils/node-web-streams-helper.ts @@ -101,12 +101,17 @@ export async function streamToBuffer( } export async function streamToString( - stream: ReadableStream + stream: ReadableStream, + signal?: AbortSignal ): Promise { const decoder = new TextDecoder('utf-8', { fatal: true }) let string = '' for await (const chunk of stream) { + if (signal?.aborted) { + return string + } + string += decoder.decode(chunk, { stream: true }) } diff --git a/packages/next/src/server/use-cache/use-cache-wrapper.ts b/packages/next/src/server/use-cache/use-cache-wrapper.ts index 7b84de7102c33c..84015b7bef743d 100644 --- a/packages/next/src/server/use-cache/use-cache-wrapper.ts +++ b/packages/next/src/server/use-cache/use-cache-wrapper.ts @@ -41,6 +41,7 @@ import { InvariantError } from '../../shared/lib/invariant-error' import { getDigestForWellKnownError } from '../app-render/create-error-handler' import { cacheHandlerGlobal, DYNAMIC_EXPIRE } from './constants' import { UseCacheTimeoutError } from './use-cache-errors' +import { createHangingInputAbortSignal } from '../app-render/dynamic-rendering' const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge' @@ -312,7 +313,7 @@ async function generateCacheEntryImpl( { temporaryReferences } ) - // Track the timestamp when we started copmuting the result. + // Track the timestamp when we started computing the result. const startTime = performance.timeOrigin + performance.now() // Invoke the inner function to load a new result. const result = fn.apply(null, args) @@ -506,29 +507,10 @@ export function cache( // the implementation. const buildId = workStore.buildId - let abortHangingInputSignal: undefined | AbortSignal - if (workUnitStore && workUnitStore.type === 'prerender') { - // In a prerender, we may end up with hanging Promises as inputs due them stalling - // on connection() or because they're loading dynamic data. In that case we need to - // abort the encoding of the arguments since they'll never complete. - const controller = new AbortController() - abortHangingInputSignal = controller.signal - if (workUnitStore.cacheSignal) { - // If we have a cacheSignal it means we're in a prospective render. If the input - // we're waiting on is coming from another cache, we do want to wait for it so that - // we can resolve this cache entry too. - workUnitStore.cacheSignal.inputReady().then(() => { - controller.abort() - }) - } else { - // Otherwise we're in the final render and we should already have all our caches - // filled. We might still be waiting on some microtasks so we wait one tick before - // giving up. When we give up, we still want to render the content of this cache - // as deeply as we can so that we can suspend as deeply as possible in the tree - // or not at all if we don't end up waiting for the input. - process.nextTick(() => controller.abort()) - } - } + const hangingInputAbortSignal = + workUnitStore?.type === 'prerender' + ? createHangingInputAbortSignal(workUnitStore) + : undefined if (boundArgsLength > 0) { if (args.length === 0) { @@ -558,7 +540,7 @@ export function cache( const temporaryReferences = createClientTemporaryReferenceSet() const encodedArguments: FormData | string = await encodeReply( [buildId, id, args], - { temporaryReferences, signal: abortHangingInputSignal } + { temporaryReferences, signal: hangingInputAbortSignal } ) const serializedCacheKey = diff --git a/test/e2e/app-dir/dynamic-io/dynamic-io.server-action.test.ts b/test/e2e/app-dir/dynamic-io/dynamic-io.server-action.test.ts index 690eedc2b1e341..724c7ed28bb6a3 100644 --- a/test/e2e/app-dir/dynamic-io/dynamic-io.server-action.test.ts +++ b/test/e2e/app-dir/dynamic-io/dynamic-io.server-action.test.ts @@ -26,9 +26,10 @@ describe('dynamic-io', () => { }) if (process.env.__NEXT_EXPERIMENTAL_PPR && isNextDev) { - // TODO: Remove this branch for PPR in dev mode when the issue is resolved - // where the inclusion of server timings in the RSC payload makes the - // serialized bound args not suitable to be used as a cache key. + // TODO(react-time-info): Remove this branch for PPR in dev mode when the + // issue is resolved where the inclusion of server timings in the RSC + // payload makes the serialized bound args not suitable to be used as a + // cache key. expect(next.cliOutput).toMatch('Error: Route "/server-action-inline"') } else { expect(next.cliOutput).not.toMatch('Error: Route "/server-action-inline"') diff --git a/test/e2e/app-dir/use-cache-hanging-inputs/app/bound-args/page.tsx b/test/e2e/app-dir/use-cache-hanging-inputs/app/bound-args/page.tsx new file mode 100644 index 00000000000000..8e87c1578bd44e --- /dev/null +++ b/test/e2e/app-dir/use-cache-hanging-inputs/app/bound-args/page.tsx @@ -0,0 +1,25 @@ +import { connection } from 'next/server' +import React from 'react' + +async function fetchUncachedData() { + await connection() + + return Math.random() +} + +export default async function Page() { + const uncachedDataPromise = fetchUncachedData() + + const Foo = async () => { + 'use cache' + + return ( + <> +

{await uncachedDataPromise}

+

{Math.random()}

+ + ) + } + + return +} diff --git a/test/e2e/app-dir/use-cache-hanging-inputs/next.config.js b/test/e2e/app-dir/use-cache-hanging-inputs/next.config.js index 3dac20d4703cf4..df2126c94163a1 100644 --- a/test/e2e/app-dir/use-cache-hanging-inputs/next.config.js +++ b/test/e2e/app-dir/use-cache-hanging-inputs/next.config.js @@ -5,6 +5,7 @@ const nextConfig = { experimental: { dynamicIO: true, prerenderEarlyExit: false, + ppr: process.env.__NEXT_EXPERIMENTAL_PPR === 'true', }, } diff --git a/test/e2e/app-dir/use-cache-hanging-inputs/use-cache-hanging-inputs.test.ts b/test/e2e/app-dir/use-cache-hanging-inputs/use-cache-hanging-inputs.test.ts index b81ad36324ec38..fa586fe958a284 100644 --- a/test/e2e/app-dir/use-cache-hanging-inputs/use-cache-hanging-inputs.test.ts +++ b/test/e2e/app-dir/use-cache-hanging-inputs/use-cache-hanging-inputs.test.ts @@ -134,6 +134,57 @@ describe('use-cache-hanging-inputs', () => { }, 180_000) }) + describe('when a "use cache" function is closing over an uncached promise', () => { + it('should show an error toast after a timeout', async () => { + const outputIndex = next.cliOutput.length + const browser = await next.browser('/bound-args') + + // The request is pending while we stall on the hanging inputs, and + // playwright will wait for the load even before continuing. So we don't + // need to wait for the "use cache" timeout of 50 seconds here. + + await openRedbox(browser) + + const errorDescription = await getRedboxDescription(browser) + const errorSource = await getRedboxSource(browser) + + if (process.env.__NEXT_EXPERIMENTAL_PPR) { + // TODO(react-time-info): Remove this branch for PPR when the issue is + // resolved where the inclusion of server timings in the RSC payload + // makes the serialized bound args not suitable to be used as a cache + // key. + + const expectedErrorMessagePpr = + 'Error: Route "/bound-args": A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. We don\'t have the exact line number added to error messages yet but you can see which component in the stack below. See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense' + + expect(errorDescription).toBe(`[ Server ] ${expectedErrorMessagePpr}`) + + const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex)) + + expect(cliOutput).toContain( + `${expectedErrorMessagePpr} + at Page [Server] ()` + ) + } else { + expect(errorDescription).toBe(`[ Cache ] ${expectedErrorMessage}`) + + // TODO(veil): This should have an error source if the source mapping works. + expect(errorSource).toBe(null) + + const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex)) + + // TODO(veil): Should include properly source mapped stack frames. + expect(cliOutput).toContain( + isTurbopack + ? `${expectedErrorMessage} + at [project]/app/bound-args/page.tsx [app-rsc] (ecmascript)` + : `${expectedErrorMessage} + at eval (webpack-internal:///(rsc)/./app/bound-args/page.tsx:25:97)` + ) + } + }, 180_000) + }) + describe('when an error is thrown', () => { it('should show an error overlay with only one error', async () => { const browser = await next.browser('/error') @@ -157,6 +208,10 @@ describe('use-cache-hanging-inputs', () => { expect(cliOutput).toInclude(expectedErrorMessage) + expect(cliOutput).toInclude( + 'Error occurred prerendering page "/bound-args"' + ) + expect(cliOutput).toInclude( 'Error occurred prerendering page "/search-params"' ) From 5f97eb9a8011055a92f61948147c54c9e48ddf6d Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Thu, 23 Jan 2025 01:08:57 +0100 Subject: [PATCH 03/61] Fix source mapping of generated cache wrapper calls (#74987) In the SWC transform we're now using the span of the original `"use cache"` function for the generated cache wrapper call, e.g. so that the captured timeout error stack can be source mapped correctly. This also fixes the wrongly placed `/*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/` marker for the generated cache wrapper statements. **Before:** 404215955-c354d1f5-4aeb-4f35-9385-a9531219dc4c **After:** 404215977-a0157fad-a1f8-409a-b9b9-9709e07f7c05 Closes NDX-675 --- .../src/transforms/server_actions.rs | 4 +- .../use-cache/output.js | 2 +- .../server-actions/server/33/output.js | 2 +- .../server-actions/server/34/output.js | 12 +- .../server-actions/server/35/output.js | 4 +- .../server-actions/server/36/output.js | 8 +- .../server-actions/server/37/output.js | 2 +- .../server-actions/server/38/output.js | 2 +- .../server-actions/server/39/output.js | 2 +- .../server-actions/server/40/output.js | 2 +- .../server-actions/server/41/output.js | 2 +- .../server-actions/server/42/output.js | 4 +- .../server-actions/server/43/output.js | 2 +- .../server-actions/server/45/output.js | 2 +- .../server-actions/server/46/output.js | 8 +- .../server-actions/server/48/output.js | 2 +- .../server-actions/server/49/output.js | 4 +- .../server-actions/server/50/output.js | 2 +- .../server-actions/server/52/output.js | 6 +- .../server-actions/server/53/output.js | 2 +- .../server-actions/server/54/output.js | 2 +- .../server-actions/server/55/output.js | 2 +- .../server-actions/server/57/output.js | 2 +- .../server-actions/server/58/output.js | 4 +- .../fixture/source-maps/use-cache/1/output.js | 6 +- .../source-maps/use-cache/1/output.map | 2 +- .../use-cache-hanging-inputs.test.ts | 150 ++++++++++++------ 27 files changed, 148 insertions(+), 94 deletions(-) diff --git a/crates/next-custom-transforms/src/transforms/server_actions.rs b/crates/next-custom-transforms/src/transforms/server_actions.rs index 0e0d396c286db0..175360c24e53fd 100644 --- a/crates/next-custom-transforms/src/transforms/server_actions.rs +++ b/crates/next-custom-transforms/src/transforms/server_actions.rs @@ -716,7 +716,7 @@ impl ServerActions { span: DUMMY_SP, kind: VarDeclKind::Var, decls: vec![VarDeclarator { - span: DUMMY_SP, + span: arrow.span, name: Pat::Ident(cache_ident.clone().into()), init: Some(wrap_cache_expr( Box::new(Expr::Fn(FnExpr { @@ -854,7 +854,7 @@ impl ServerActions { span: DUMMY_SP, kind: VarDeclKind::Var, decls: vec![VarDeclarator { - span: DUMMY_SP, + span: function.span, name: Pat::Ident(cache_ident.clone().into()), init: Some(wrap_cache_expr( Box::new(Expr::Fn(FnExpr { diff --git a/crates/next-custom-transforms/tests/fixture/next-font-with-directive/use-cache/output.js b/crates/next-custom-transforms/tests/fixture/next-font-with-directive/use-cache/output.js index 0cc8d775cee3f5..22b239b804e1c9 100644 --- a/crates/next-custom-transforms/tests/fixture/next-font-with-directive/use-cache/output.js +++ b/crates/next-custom-transforms/tests/fixture/next-font-with-directive/use-cache/output.js @@ -3,7 +3,7 @@ import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; import React from 'react'; import inter from '@next/font/google/target.css?{"path":"app/test.tsx","import":"Inter","arguments":[],"variableName":"inter"}'; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "c0dd5bb6fef67f5ab84327f5164ac2c3111a159337", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function Cached({ children }) { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "c0dd5bb6fef67f5ab84327f5164ac2c3111a159337", 0, async function Cached({ children }) { return
{children}
; }); Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/33/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/33/output.js index 606a46c54eb03d..2b5d1d572b9a78 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/33/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/33/output.js @@ -2,7 +2,7 @@ import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; const v = 'world'; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function fn() { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function fn() { return 'hello, ' + v; }); Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/34/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/34/output.js index a01b456ac9ec32..00c7466818f774 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/34/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/34/output.js @@ -1,16 +1,16 @@ /* __next_internal_action_entry_do_not_use__ {"8012a8d21b6362b4cc8f5b15560525095bc48dba80":"$$RSC_SERVER_CACHE_3","803128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0","8069348c79fce073bae2f70f139565a2fda1c74c74":"$$RSC_SERVER_CACHE_2","80951c375b4a6a6e89d67b743ec5808127cfde405d":"$$RSC_SERVER_CACHE_1"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function() { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function() { return 'foo'; }); Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { "value": "foo", "writable": false }); -const foo = /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ registerServerReference($$RSC_SERVER_CACHE_0, "803128060c414d59f8552e4788b846c0d2b7f74743", null); +const foo = registerServerReference($$RSC_SERVER_CACHE_0, "803128060c414d59f8552e4788b846c0d2b7f74743", null); export { bar }; -export var $$RSC_SERVER_CACHE_1 = $$cache__("default", "80951c375b4a6a6e89d67b743ec5808127cfde405d", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function bar() { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_1 = $$cache__("default", "80951c375b4a6a6e89d67b743ec5808127cfde405d", 0, async function bar() { return 'bar'; }); Object.defineProperty($$RSC_SERVER_CACHE_1, "name", { @@ -22,7 +22,7 @@ var bar = registerServerReference($$RSC_SERVER_CACHE_1, "80951c375b4a6a6e89d67b7 const qux = async function qux() { return 'qux'; }; -export var $$RSC_SERVER_CACHE_2 = $$cache__("default", "8069348c79fce073bae2f70f139565a2fda1c74c74", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function baz() { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_2 = $$cache__("default", "8069348c79fce073bae2f70f139565a2fda1c74c74", 0, async function baz() { return qux() + 'baz'; }); Object.defineProperty($$RSC_SERVER_CACHE_2, "name", { @@ -30,13 +30,13 @@ Object.defineProperty($$RSC_SERVER_CACHE_2, "name", { "writable": false }); const baz = registerServerReference($$RSC_SERVER_CACHE_2, "8069348c79fce073bae2f70f139565a2fda1c74c74", null); -export var $$RSC_SERVER_CACHE_3 = $$cache__("default", "8012a8d21b6362b4cc8f5b15560525095bc48dba80", 0, async function() { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_3 = $$cache__("default", "8012a8d21b6362b4cc8f5b15560525095bc48dba80", 0, async function() { return 'quux'; }); Object.defineProperty($$RSC_SERVER_CACHE_3, "name", { "value": "quux", "writable": false }); -const quux = /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ registerServerReference($$RSC_SERVER_CACHE_3, "8012a8d21b6362b4cc8f5b15560525095bc48dba80", null); +const quux = registerServerReference($$RSC_SERVER_CACHE_3, "8012a8d21b6362b4cc8f5b15560525095bc48dba80", null); export { foo, baz }; export default quux; diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/35/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/35/output.js index d72d77959887dd..7ff1b8a509b3d6 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/35/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/35/output.js @@ -1,11 +1,11 @@ /* __next_internal_action_entry_do_not_use__ {"803128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function() { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function() { return 'data'; }); Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { "value": "my_fn", "writable": false }); -export const my_fn = /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ registerServerReference($$RSC_SERVER_CACHE_0, "803128060c414d59f8552e4788b846c0d2b7f74743", null); +export const my_fn = registerServerReference($$RSC_SERVER_CACHE_0, "803128060c414d59f8552e4788b846c0d2b7f74743", null); diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/36/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/36/output.js index 983c818a1b41a5..da03d7679d89a3 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/36/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/36/output.js @@ -1,7 +1,7 @@ /* __next_internal_action_entry_do_not_use__ {"8012a8d21b6362b4cc8f5b15560525095bc48dba80":"$$RSC_SERVER_CACHE_3","803128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0","80951c375b4a6a6e89d67b743ec5808127cfde405d":"$$RSC_SERVER_CACHE_1","c069348c79fce073bae2f70f139565a2fda1c74c74":"$$RSC_SERVER_CACHE_2"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function foo() { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function foo() { return 'data A'; }); Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { @@ -9,7 +9,7 @@ Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { "writable": false }); export var foo = registerServerReference($$RSC_SERVER_CACHE_0, "803128060c414d59f8552e4788b846c0d2b7f74743", null); -export var $$RSC_SERVER_CACHE_1 = $$cache__("default", "80951c375b4a6a6e89d67b743ec5808127cfde405d", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function bar() { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_1 = $$cache__("default", "80951c375b4a6a6e89d67b743ec5808127cfde405d", 0, async function bar() { return 'data B'; }); Object.defineProperty($$RSC_SERVER_CACHE_1, "name", { @@ -17,7 +17,7 @@ Object.defineProperty($$RSC_SERVER_CACHE_1, "name", { "writable": false }); export var bar = registerServerReference($$RSC_SERVER_CACHE_1, "80951c375b4a6a6e89d67b743ec5808127cfde405d", null); -export var $$RSC_SERVER_CACHE_2 = $$cache__("default", "c069348c79fce073bae2f70f139565a2fda1c74c74", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function Cached({ children }) { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_2 = $$cache__("default", "c069348c79fce073bae2f70f139565a2fda1c74c74", 0, async function Cached({ children }) { return children; }); Object.defineProperty($$RSC_SERVER_CACHE_2, "name", { @@ -25,7 +25,7 @@ Object.defineProperty($$RSC_SERVER_CACHE_2, "name", { "writable": false }); export default registerServerReference($$RSC_SERVER_CACHE_2, "c069348c79fce073bae2f70f139565a2fda1c74c74", null); -export var $$RSC_SERVER_CACHE_3 = $$cache__("default", "8012a8d21b6362b4cc8f5b15560525095bc48dba80", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function baz() { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_3 = $$cache__("default", "8012a8d21b6362b4cc8f5b15560525095bc48dba80", 0, async function baz() { return 'data C'; }); Object.defineProperty($$RSC_SERVER_CACHE_3, "name", { diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/37/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/37/output.js index 9fbcd09d98769e..f1831a8e2e8fda 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/37/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/37/output.js @@ -1,7 +1,7 @@ /* __next_internal_action_entry_do_not_use__ {"803128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function fn() { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function fn() { return 'foo'; }); Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/38/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/38/output.js index 643356d06296ae..291971eaa71e95 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/38/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/38/output.js @@ -1,7 +1,7 @@ /* __next_internal_action_entry_do_not_use__ {"803128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("x", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function foo() { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("x", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function foo() { return 'data'; }); Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/39/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/39/output.js index ce79a8d2a50e90..77c4e8113879b9 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/39/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/39/output.js @@ -1,7 +1,7 @@ /* __next_internal_action_entry_do_not_use__ {"c03128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "c03128060c414d59f8552e4788b846c0d2b7f74743", 2, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function fn([$$ACTION_ARG_0, $$ACTION_ARG_1]) { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "c03128060c414d59f8552e4788b846c0d2b7f74743", 2, async function fn([$$ACTION_ARG_0, $$ACTION_ARG_1]) { console.log($$ACTION_ARG_0); return { foo: $$ACTION_ARG_1 diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/40/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/40/output.js index eb8f0868f490c2..fc79c978b3a38d 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/40/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/40/output.js @@ -1,7 +1,7 @@ /* __next_internal_action_entry_do_not_use__ {"601c36b06e398c97abe5d5d7ae8c672bfddf4e1b91":"$$RSC_SERVER_ACTION_2","e03128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "e03128060c414d59f8552e4788b846c0d2b7f74743", 2, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function cache([$$ACTION_ARG_0, $$ACTION_ARG_1], e) { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "e03128060c414d59f8552e4788b846c0d2b7f74743", 2, async function cache([$$ACTION_ARG_0, $$ACTION_ARG_1], e) { const f = $$ACTION_ARG_0 + e; return [ f, diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/41/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/41/output.js index 2144765c5b128b..2ea72779b776fd 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/41/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/41/output.js @@ -8,7 +8,7 @@ export const /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_ACTION_0 = a foo: $$ACTION_ARG_1 }; }; -export var $$RSC_SERVER_CACHE_1 = $$cache__("default", "c0951c375b4a6a6e89d67b743ec5808127cfde405d", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function Component({ foo }) { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_1 = $$cache__("default", "c0951c375b4a6a6e89d67b743ec5808127cfde405d", 0, async function Component({ foo }) { const a = 123; var fn = registerServerReference($$RSC_SERVER_ACTION_0, "406a88810ecce4a4e8b59d53b8327d7e98bbf251d7", null).bind(null, encryptActionBoundArgs("406a88810ecce4a4e8b59d53b8327d7e98bbf251d7", [ a, diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/42/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/42/output.js index 5d7b1436fa6cd1..c4de72750b45d4 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/42/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/42/output.js @@ -1,7 +1,7 @@ /* __next_internal_action_entry_do_not_use__ {"c03128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "c03128060c414d59f8552e4788b846c0d2b7f74743", 2, async function([$$ACTION_ARG_0, $$ACTION_ARG_1]) { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "c03128060c414d59f8552e4788b846c0d2b7f74743", 2, async function([$$ACTION_ARG_0, $$ACTION_ARG_1]) { console.log($$ACTION_ARG_0); return { foo: $$ACTION_ARG_1 @@ -20,4 +20,4 @@ async function Component({ foo }) { const data = await fn(); return
{data}
; } -var $$RSC_SERVER_REF_1 = /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ registerServerReference($$RSC_SERVER_CACHE_0, "c03128060c414d59f8552e4788b846c0d2b7f74743", null); +var $$RSC_SERVER_REF_1 = registerServerReference($$RSC_SERVER_CACHE_0, "c03128060c414d59f8552e4788b846c0d2b7f74743", null); diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/43/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/43/output.js index 2aa3b34d213354..6b9ddeb5a45666 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/43/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/43/output.js @@ -7,7 +7,7 @@ export const /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_ACTION_0 = a var [$$ACTION_ARG_0] = await decryptActionBoundArgs("406a88810ecce4a4e8b59d53b8327d7e98bbf251d7", $$ACTION_CLOSURE_BOUND); console.log(secret, $$ACTION_ARG_0); }; -export var $$RSC_SERVER_CACHE_1 = $$cache__("default", "e0951c375b4a6a6e89d67b743ec5808127cfde405d", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function getCachedRandom(x, children) { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_1 = $$cache__("default", "e0951c375b4a6a6e89d67b743ec5808127cfde405d", 0, async function getCachedRandom(x, children) { return { x, y: Math.random(), diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/45/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/45/output.js index 320d58b78faed8..05b7d55f549514 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/45/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/45/output.js @@ -7,7 +7,7 @@ function Foo() { console.log(v); return v; } -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function bar() { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function bar() { return ; }); Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/46/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/46/output.js index 989c00fb45223e..238e1a79d4b680 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/46/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/46/output.js @@ -22,8 +22,8 @@ export const // Should be 0 110000 0, which is "60" in hex. ]; }; var f2 = registerServerReference($$RSC_SERVER_ACTION_1, "6090b5db271335765a4b0eab01f044b381b5ebd5cd", null); -export var $$RSC_SERVER_CACHE_2 = $$cache__("default", "ff69348c79fce073bae2f70f139565a2fda1c74c74", 0, // Should be 1 111111 1, which is "ff" in hex. -/*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function f3(a, b, ...rest) { +export var // Should be 1 111111 1, which is "ff" in hex. +/*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_2 = $$cache__("default", "ff69348c79fce073bae2f70f139565a2fda1c74c74", 0, async function f3(a, b, ...rest) { return [ a, b, @@ -58,8 +58,8 @@ export const // Should be 0 111111 0, which is "7e" in hex. ]; }; var f4 = registerServerReference($$RSC_SERVER_ACTION_4, "7ea9b2939c1f39073a6bed227fd20233064c8b7869", null); -export var $$RSC_SERVER_CACHE_5 = $$cache__("default", "ff471a5eb0be1c31686dd4ba938a80328b80b1615d", 0, // Should be 1 111111 1, which is "ff" in hex. -/*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function f5(a, b, c, d, e, f, g) { +export var // Should be 1 111111 1, which is "ff" in hex. +/*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_5 = $$cache__("default", "ff471a5eb0be1c31686dd4ba938a80328b80b1615d", 0, async function f5(a, b, c, d, e, f, g) { return [ a, b, diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/48/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/48/output.js index e7c682cee3b8c1..bef227bb7e1282 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/48/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/48/output.js @@ -24,7 +24,7 @@ export async function /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ action3(a, b) { {b} ; } -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "e03128060c414d59f8552e4788b846c0d2b7f74743", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function cache(a, b) { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "e03128060c414d59f8552e4788b846c0d2b7f74743", 0, async function cache(a, b) { return
{a} {b} diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/49/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/49/output.js index 313d0e632e637d..5cff2aeb08ee06 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/49/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/49/output.js @@ -1,11 +1,11 @@ /* __next_internal_action_entry_do_not_use__ {"f03128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "f03128060c414d59f8552e4788b846c0d2b7f74743", 0, async function(a, b, c) { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "f03128060c414d59f8552e4788b846c0d2b7f74743", 0, async function(a, b, c) { return
{a} {b} {c}
; }); -export default /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ registerServerReference($$RSC_SERVER_CACHE_0, "f03128060c414d59f8552e4788b846c0d2b7f74743", null); +export default registerServerReference($$RSC_SERVER_CACHE_0, "f03128060c414d59f8552e4788b846c0d2b7f74743", null); diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/50/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/50/output.js index d214f646720f9b..d61b44666d39f5 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/50/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/50/output.js @@ -1,7 +1,7 @@ /* __next_internal_action_entry_do_not_use__ {"f03128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "f03128060c414d59f8552e4788b846c0d2b7f74743", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function(a, b, c) { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "f03128060c414d59f8552e4788b846c0d2b7f74743", 0, async function(a, b, c) { return
{a} {b} diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/52/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/52/output.js index 7f32a0680c2aad..98a7ca57426bb9 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/52/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/52/output.js @@ -1,14 +1,14 @@ /* __next_internal_action_entry_do_not_use__ {"409651a98a9dccd7ffbe72ff5cf0f38546ca1252ab":"$$RSC_SERVER_ACTION_5","60a9b2939c1f39073a6bed227fd20233064c8b7869":"$$RSC_SERVER_ACTION_4","c069348c79fce073bae2f70f139565a2fda1c74c74":"$$RSC_SERVER_CACHE_2","e03128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "e03128060c414d59f8552e4788b846c0d2b7f74743", 2, async function([$$ACTION_ARG_0, $$ACTION_ARG_1], c) { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "e03128060c414d59f8552e4788b846c0d2b7f74743", 2, async function([$$ACTION_ARG_0, $$ACTION_ARG_1], c) { return $$ACTION_ARG_0 + $$ACTION_ARG_1 + c; }); Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { "value": "fn1", "writable": false }); -export var $$RSC_SERVER_CACHE_2 = $$cache__("default", "c069348c79fce073bae2f70f139565a2fda1c74c74", 2, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function // Should be 1 100000 0, which is "c0" in hex (counts as one param, +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_2 = $$cache__("default", "c069348c79fce073bae2f70f139565a2fda1c74c74", 2, async function // Should be 1 100000 0, which is "c0" in hex (counts as one param, // because of the encrypted bound args param) fn2([$$ACTION_ARG_0, $$ACTION_ARG_1]) { return $$ACTION_ARG_0 + $$ACTION_ARG_1; @@ -47,5 +47,5 @@ export async function Component(a) { b ]))}/>; } -var $$RSC_SERVER_REF_1 = /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ registerServerReference($$RSC_SERVER_CACHE_0, "e03128060c414d59f8552e4788b846c0d2b7f74743", null); +var $$RSC_SERVER_REF_1 = registerServerReference($$RSC_SERVER_CACHE_0, "e03128060c414d59f8552e4788b846c0d2b7f74743", null); var $$RSC_SERVER_REF_3 = registerServerReference($$RSC_SERVER_CACHE_2, "c069348c79fce073bae2f70f139565a2fda1c74c74", null); diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/53/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/53/output.js index 46fef7eaa3fefd..8816f4c5ff9e33 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/53/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/53/output.js @@ -1,7 +1,7 @@ /* __next_internal_action_entry_do_not_use__ {"0090b5db271335765a4b0eab01f044b381b5ebd5cd":"$$RSC_SERVER_ACTION_1","803128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function foo() {}); +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function foo() {}); Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { "value": "foo", "writable": false diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/54/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/54/output.js index 7e001d9f4d6382..29ce97e9217323 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/54/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/54/output.js @@ -1,7 +1,7 @@ /* __next_internal_action_entry_do_not_use__ {"401c36b06e398c97abe5d5d7ae8c672bfddf4e1b91":"$$RSC_SERVER_ACTION_2","c03128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "c03128060c414d59f8552e4788b846c0d2b7f74743", 2, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function foo([$$ACTION_ARG_0, $$ACTION_ARG_1]) { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "c03128060c414d59f8552e4788b846c0d2b7f74743", 2, async function foo([$$ACTION_ARG_0, $$ACTION_ARG_1]) { return $$ACTION_ARG_0 * $$ACTION_ARG_1; }); Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/55/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/55/output.js index 6baee209f32102..589d14a66231b9 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/55/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/55/output.js @@ -1,7 +1,7 @@ /* __next_internal_action_entry_do_not_use__ {"803128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function fetch1() { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function fetch1() { return fetch('https://example.com').then((res)=>res.json()); }); Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/57/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/57/output.js index 175231a3d2b04f..b74015ca63236f 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/57/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/57/output.js @@ -1,7 +1,7 @@ /* __next_internal_action_entry_do_not_use__ {"0090b5db271335765a4b0eab01f044b381b5ebd5cd":"$$RSC_SERVER_ACTION_1","803128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function foo() { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function foo() { return fetch('https://example.com').then((res)=>res.json()); }); Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { diff --git a/crates/next-custom-transforms/tests/fixture/server-actions/server/58/output.js b/crates/next-custom-transforms/tests/fixture/server-actions/server/58/output.js index 8d8f50843fc646..ce3085559eacdb 100644 --- a/crates/next-custom-transforms/tests/fixture/server-actions/server/58/output.js +++ b/crates/next-custom-transforms/tests/fixture/server-actions/server/58/output.js @@ -1,7 +1,7 @@ /* __next_internal_action_entry_do_not_use__ {"401c36b06e398c97abe5d5d7ae8c672bfddf4e1b91":"$$RSC_SERVER_ACTION_2","c03128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "c03128060c414d59f8552e4788b846c0d2b7f74743", 1, async function([$$ACTION_ARG_0]) { +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "c03128060c414d59f8552e4788b846c0d2b7f74743", 1, async function([$$ACTION_ARG_0]) { return $$ACTION_ARG_0(); }); function createCachedFn(start) { @@ -12,7 +12,7 @@ function createCachedFn(start) { fn ])); } -var $$RSC_SERVER_REF_1 = /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ registerServerReference($$RSC_SERVER_CACHE_0, "c03128060c414d59f8552e4788b846c0d2b7f74743", null); +var $$RSC_SERVER_REF_1 = registerServerReference($$RSC_SERVER_CACHE_0, "c03128060c414d59f8552e4788b846c0d2b7f74743", null); export const /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_ACTION_2 = async function($$ACTION_CLOSURE_BOUND) { var [$$ACTION_ARG_0] = await decryptActionBoundArgs("401c36b06e398c97abe5d5d7ae8c672bfddf4e1b91", $$ACTION_CLOSURE_BOUND); console.log($$ACTION_ARG_0()); diff --git a/crates/next-custom-transforms/tests/fixture/source-maps/use-cache/1/output.js b/crates/next-custom-transforms/tests/fixture/source-maps/use-cache/1/output.js index 87cc49a329dc4b..09c5e7415e71da 100644 --- a/crates/next-custom-transforms/tests/fixture/source-maps/use-cache/1/output.js +++ b/crates/next-custom-transforms/tests/fixture/source-maps/use-cache/1/output.js @@ -1,13 +1,13 @@ /* __next_internal_action_entry_do_not_use__ {"803128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0","80951c375b4a6a6e89d67b743ec5808127cfde405d":"$$RSC_SERVER_CACHE_1"} */ import { registerServerReference } from "private-next-rsc-server-reference"; import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption"; import { cache as $$cache__ } from "private-next-rsc-cache-wrapper"; -export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function() {}); +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, async function() {}); Object.defineProperty($$RSC_SERVER_CACHE_0, "name", { "value": "foo", "writable": false }); -const foo = /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ registerServerReference($$RSC_SERVER_CACHE_0, "803128060c414d59f8552e4788b846c0d2b7f74743", null); -export var $$RSC_SERVER_CACHE_1 = $$cache__("default", "80951c375b4a6a6e89d67b743ec5808127cfde405d", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function bar() { +const foo = registerServerReference($$RSC_SERVER_CACHE_0, "803128060c414d59f8552e4788b846c0d2b7f74743", null); +export var /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_CACHE_1 = $$cache__("default", "80951c375b4a6a6e89d67b743ec5808127cfde405d", 0, async function bar() { return foo(); }); Object.defineProperty($$RSC_SERVER_CACHE_1, "name", { diff --git a/crates/next-custom-transforms/tests/fixture/source-maps/use-cache/1/output.map b/crates/next-custom-transforms/tests/fixture/source-maps/use-cache/1/output.map index 01088c723f8d97..191a72d02e7787 100644 --- a/crates/next-custom-transforms/tests/fixture/source-maps/use-cache/1/output.map +++ b/crates/next-custom-transforms/tests/fixture/source-maps/use-cache/1/output.map @@ -1 +1 @@ -{"version":3,"sources":["input.js"],"sourcesContent":["'use cache'\n\nconst foo = async () => {\n 'use cache'\n}\n\nexport async function bar() {\n return foo()\n}\n"],"names":[],"mappings":";;;0HAIA;;;;;AAFA,MAAM,MAAM,uCACC,GADD;wGAIL,uCACO,GADP,eAAe;IACpB,OAAO;AACT;;;;;AAFA,WAAsB,MAAf"} +{"version":3,"sources":["input.js"],"sourcesContent":["'use cache'\n\nconst foo = async () => {\n 'use cache'\n}\n\nexport async function bar() {\n return foo()\n}\n"],"names":[],"mappings":";;;WAEY,uCACC,GADD,+GAEZ;;;;;AAFA,MAAM,MAAM;WAIL,uCACO,GADP,6FAAA,eAAe;IACpB,OAAO;AACT;;;;;AAFA,WAAsB,MAAf"} diff --git a/test/e2e/app-dir/use-cache-hanging-inputs/use-cache-hanging-inputs.test.ts b/test/e2e/app-dir/use-cache-hanging-inputs/use-cache-hanging-inputs.test.ts index fa586fe958a284..69856caf8903df 100644 --- a/test/e2e/app-dir/use-cache-hanging-inputs/use-cache-hanging-inputs.test.ts +++ b/test/e2e/app-dir/use-cache-hanging-inputs/use-cache-hanging-inputs.test.ts @@ -41,19 +41,33 @@ describe('use-cache-hanging-inputs', () => { expect(errorDescription).toBe(`[ Cache ] ${expectedErrorMessage}`) - // TODO(veil): This should have an error source if the source mapping works. - expect(errorSource).toBe(null) - const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex)) - // TODO(veil): Should include properly source mapped stack frames. - expect(cliOutput).toContain( - isTurbopack - ? `${expectedErrorMessage} - at [project]/app/search-params/page.tsx [app-rsc] (ecmascript)` - : `${expectedErrorMessage} - at eval (webpack-internal:///(rsc)/./app/search-params/page.tsx:16:97)` - ) + if (isTurbopack) { + // TODO(veil): For Turbopack, a fix in the React Flight Client, where + // sourceURL is encoded, is needed for the error source and stack + // frames to be source mapped. + + expect(errorSource).toBe(null) + + expect(cliOutput).toContain(`${expectedErrorMessage} + at [project]/app/search-params/page.tsx [app-rsc] (ecmascript)`) + } else { + expect(errorSource).toMatchInlineSnapshot(` + "app/search-params/page.tsx (3:16) @ eval + + 1 | 'use cache' + 2 | + > 3 | export default async function Page({ + | ^ + 4 | searchParams, + 5 | }: { + 6 | searchParams: Promise<{ n: string }>" + `) + + expect(cliOutput).toContain(`${expectedErrorMessage} + at eval (app/search-params/page.tsx:3:15)`) + } }, 180_000) }) @@ -86,19 +100,33 @@ describe('use-cache-hanging-inputs', () => { expect(errorDescription).toBe(`[ Cache ] ${expectedErrorMessage}`) - // TODO(veil): This should have an error source if the source mapping works. - expect(errorSource).toBe(null) - const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex)) - // TODO(veil): Should include properly source mapped stack frames. - expect(cliOutput).toContain( - isTurbopack - ? `${expectedErrorMessage} - at [project]/app/uncached-promise/page.tsx [app-rsc] (ecmascript)` - : `${expectedErrorMessage} - at eval (webpack-internal:///(rsc)/./app/uncached-promise/page.tsx:26:97)` - ) + if (isTurbopack) { + // TODO(veil): For Turbopack, a fix in the React Flight Client, where + // sourceURL is encoded, is needed for the error source and stack + // frames to be source mapped. + + expect(errorSource).toBe(null) + + expect(cliOutput).toContain(`${expectedErrorMessage} + at [project]/app/uncached-promise/page.tsx [app-rsc] (ecmascript)`) + } else { + expect(errorSource).toMatchInlineSnapshot(` + "app/uncached-promise/page.tsx (10:13) @ eval + + 8 | } + 9 | + > 10 | const Foo = async ({ promise }) => { + | ^ + 11 | 'use cache' + 12 | + 13 | return (" + `) + + expect(cliOutput).toContain(`${expectedErrorMessage} + at eval (app/uncached-promise/page.tsx:10:12)`) + } }, 180_000) }) @@ -118,19 +146,33 @@ describe('use-cache-hanging-inputs', () => { expect(errorDescription).toBe(`[ Cache ] ${expectedErrorMessage}`) - // TODO(veil): This should have an error source if the source mapping works. - expect(errorSource).toBe(null) - const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex)) - // TODO(veil): Should include properly source mapped stack frames. - expect(cliOutput).toContain( - isTurbopack - ? `${expectedErrorMessage} - at [project]/app/uncached-promise-nested/page.tsx [app-rsc] (ecmascript)` - : `${expectedErrorMessage} - at eval (webpack-internal:///(rsc)/./app/uncached-promise-nested/page.tsx:35:97)` - ) + if (isTurbopack) { + // TODO(veil): For Turbopack, a fix in the React Flight Client, where + // sourceURL is encoded, is needed for the error source and stack + // frames to be source mapped. + + expect(errorSource).toBe(null) + + expect(cliOutput).toContain(`${expectedErrorMessage} + at [project]/app/uncached-promise-nested/page.tsx [app-rsc] (ecmascript)`) + } else { + expect(errorSource).toMatchInlineSnapshot(` + "app/uncached-promise-nested/page.tsx (16:1) @ eval + + 14 | } + 15 | + > 16 | async function indirection(promise: Promise) { + | ^ + 17 | 'use cache' + 18 | + 19 | return getCachedData(promise)" + `) + + expect(cliOutput).toContain(`${expectedErrorMessage} + at eval (app/uncached-promise-nested/page.tsx:16:0)`) + } }, 180_000) }) @@ -148,6 +190,8 @@ describe('use-cache-hanging-inputs', () => { const errorDescription = await getRedboxDescription(browser) const errorSource = await getRedboxSource(browser) + const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex)) + if (process.env.__NEXT_EXPERIMENTAL_PPR) { // TODO(react-time-info): Remove this branch for PPR when the issue is // resolved where the inclusion of server timings in the RSC payload @@ -159,8 +203,6 @@ describe('use-cache-hanging-inputs', () => { expect(errorDescription).toBe(`[ Server ] ${expectedErrorMessagePpr}`) - const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex)) - expect(cliOutput).toContain( `${expectedErrorMessagePpr} at Page [Server] ()` @@ -168,19 +210,31 @@ describe('use-cache-hanging-inputs', () => { } else { expect(errorDescription).toBe(`[ Cache ] ${expectedErrorMessage}`) - // TODO(veil): This should have an error source if the source mapping works. - expect(errorSource).toBe(null) - - const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex)) - - // TODO(veil): Should include properly source mapped stack frames. - expect(cliOutput).toContain( - isTurbopack - ? `${expectedErrorMessage} - at [project]/app/bound-args/page.tsx [app-rsc] (ecmascript)` - : `${expectedErrorMessage} - at eval (webpack-internal:///(rsc)/./app/bound-args/page.tsx:25:97)` - ) + if (isTurbopack) { + // TODO(veil): For Turbopack, a fix in the React Flight Client, where + // sourceURL is encoded, is needed for the error source and stack + // frames to be source mapped. + + expect(errorSource).toBe(null) + + expect(cliOutput).toContain(`${expectedErrorMessage} + at [project]/app/bound-args/page.tsx [app-rsc] (ecmascript)`) + } else { + expect(errorSource).toMatchInlineSnapshot(` + "app/bound-args/page.tsx (13:15) @ eval + + 11 | const uncachedDataPromise = fetchUncachedData() + 12 | + > 13 | const Foo = async () => { + | ^ + 14 | 'use cache' + 15 | + 16 | return (" + `) + + expect(cliOutput).toContain(`${expectedErrorMessage} + at eval (app/bound-args/page.tsx:13:14)`) + } } }, 180_000) }) From c33eb2b6505f6717a1a1eac10e193da9d9fa1ece Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 23 Jan 2025 01:25:04 +0100 Subject: [PATCH 04/61] [Turbopack] use new backend by default (#72434) Closes PACK-3412 --- Cargo.lock | 2 +- crates/napi/src/next_api/utils.rs | 26 ++++++++++++------- .../crates/turbo-tasks-backend/benches/mod.rs | 4 +-- turbopack/crates/turbopack-tests/Cargo.toml | 2 +- .../crates/turbopack-tests/tests/execution.rs | 10 +++++-- .../crates/turbopack-tests/tests/snapshot.rs | 10 +++++-- 6 files changed, 36 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d5882969dbb2c..36fe15c6643d8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9904,11 +9904,11 @@ dependencies = [ "tokio", "turbo-rcstr", "turbo-tasks", + "turbo-tasks-backend", "turbo-tasks-build", "turbo-tasks-bytes", "turbo-tasks-env", "turbo-tasks-fs", - "turbo-tasks-memory", "turbopack", "turbopack-browser", "turbopack-cli-utils", diff --git a/crates/napi/src/next_api/utils.rs b/crates/napi/src/next_api/utils.rs index 7969477c91959c..0d19fcfda94d9c 100644 --- a/crates/napi/src/next_api/utils.rs +++ b/crates/napi/src/next_api/utils.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashMap, env, future::Future, ops::Deref, path::PathBuf, sync::Arc, time::Duration, + collections::HashMap, future::Future, ops::Deref, path::PathBuf, sync::Arc, time::Duration, }; use anyhow::{anyhow, Context, Result}; @@ -12,7 +12,9 @@ use serde::Serialize; use turbo_tasks::{ trace::TraceRawVcs, OperationVc, ReadRef, TaskId, TryJoinIterExt, TurboTasks, UpdateInfo, Vc, }; -use turbo_tasks_backend::{default_backing_storage, DefaultBackingStorage}; +use turbo_tasks_backend::{ + default_backing_storage, noop_backing_storage, DefaultBackingStorage, NoopBackingStorage, +}; use turbo_tasks_fs::FileContent; use turbopack_core::{ diagnostics::{Diagnostic, DiagnosticContextExt, PlainDiagnostic}, @@ -25,7 +27,7 @@ use crate::util::log_internal_error_and_inform; #[derive(Clone)] pub enum NextTurboTasks { - Memory(Arc>), + Memory(Arc>>), PersistentCaching( Arc>>, ), @@ -108,7 +110,7 @@ impl NextTurboTasks { pub fn memory_backend(&self) -> Option<&turbo_tasks_memory::MemoryBackend> { match self { - NextTurboTasks::Memory(turbo_tasks) => Some(turbo_tasks.backend()), + NextTurboTasks::Memory(_) => None, NextTurboTasks::PersistentCaching(_) => None, } } @@ -124,7 +126,7 @@ impl NextTurboTasks { pub fn create_turbo_tasks( output_path: PathBuf, persistent_caching: bool, - memory_limit: usize, + _memory_limit: usize, ) -> Result { Ok(if persistent_caching { let dirty_suffix = if crate::build::GIT_CLEAN @@ -162,11 +164,15 @@ pub fn create_turbo_tasks( ), )) } else { - let mut backend = turbo_tasks_memory::MemoryBackend::new(memory_limit); - if env::var_os("NEXT_TURBOPACK_PRINT_TASK_INVALIDATION").is_some() { - backend.print_task_invalidation(true); - } - NextTurboTasks::Memory(TurboTasks::new(backend)) + NextTurboTasks::Memory(TurboTasks::new( + turbo_tasks_backend::TurboTasksBackend::new( + turbo_tasks_backend::BackendOptions { + storage_mode: None, + ..Default::default() + }, + noop_backing_storage(), + ), + )) }) } diff --git a/turbopack/crates/turbo-tasks-backend/benches/mod.rs b/turbopack/crates/turbo-tasks-backend/benches/mod.rs index b11aaaf8116e12..7428b611e5db8b 100644 --- a/turbopack/crates/turbo-tasks-backend/benches/mod.rs +++ b/turbopack/crates/turbo-tasks-backend/benches/mod.rs @@ -7,11 +7,11 @@ pub(crate) mod scope_stress; pub(crate) mod stress; criterion_group!( - name = turbo_tasks_memory_stress; + name = turbo_tasks_backend_stress; config = Criterion::default(); targets = stress::fibonacci, scope_stress::scope_stress ); -criterion_main!(turbo_tasks_memory_stress); +criterion_main!(turbo_tasks_backend_stress); pub fn register() { turbo_tasks::register(); diff --git a/turbopack/crates/turbopack-tests/Cargo.toml b/turbopack/crates/turbopack-tests/Cargo.toml index a5345623dd36b3..8586807bbaf961 100644 --- a/turbopack/crates/turbopack-tests/Cargo.toml +++ b/turbopack/crates/turbopack-tests/Cargo.toml @@ -30,7 +30,7 @@ turbo-tasks = { workspace = true } turbo-tasks-bytes = { workspace = true } turbo-tasks-env = { workspace = true } turbo-tasks-fs = { workspace = true } -turbo-tasks-memory = { workspace = true } +turbo-tasks-backend = { workspace = true } turbopack-browser = { workspace = true, features = ["test"] } turbopack-cli-utils = { workspace = true } turbopack-core = { workspace = true, features = ["issue_path"] } diff --git a/turbopack/crates/turbopack-tests/tests/execution.rs b/turbopack/crates/turbopack-tests/tests/execution.rs index 5d867f6b47901b..5088076904911f 100644 --- a/turbopack/crates/turbopack-tests/tests/execution.rs +++ b/turbopack/crates/turbopack-tests/tests/execution.rs @@ -15,13 +15,13 @@ use turbo_tasks::{ apply_effects, debug::ValueDebugFormat, fxindexmap, trace::TraceRawVcs, Completion, NonLocalValue, OperationVc, ResolvedVc, TryJoinIterExt, TurboTasks, Value, Vc, }; +use turbo_tasks_backend::{noop_backing_storage, BackendOptions, TurboTasksBackend}; use turbo_tasks_bytes::stream::SingleValue; use turbo_tasks_env::CommandLineProcessEnv; use turbo_tasks_fs::{ json::parse_json_with_source_context, util::sys_to_unix, DiskFileSystem, FileContent, FileSystem, FileSystemEntryType, FileSystemPath, }; -use turbo_tasks_memory::MemoryBackend; use turbopack::{ ecmascript::TreeShakingMode, module_options::{EcmascriptOptionsContext, ModuleOptionsContext, TypescriptTransformOptions}, @@ -168,7 +168,13 @@ async fn run(resource: PathBuf, snapshot_mode: IssueSnapshotMode) -> Result Result<()> { register(); - let tt = TurboTasks::new(MemoryBackend::default()); + let tt = TurboTasks::new(TurboTasksBackend::new( + BackendOptions { + storage_mode: None, + ..Default::default() + }, + noop_backing_storage(), + )); let task = tt.spawn_once_task(async move { let emit_op = run_inner_operation(resource.to_str().unwrap().into()); emit_op.read_strongly_consistent().await?; From c783ef8b2c8855e48a53d44d3b1730fcb5cf0ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Thu, 23 Jan 2025 18:18:36 +0900 Subject: [PATCH 05/61] perf(turbopack): Use `Cow::into_owned` instead of `.to_string()` (#75216) ### What? `Cow::into_owned()` does not allocate in some cases, while `to_string()` always allocate. ### Why? In the name of the performance. ### How? --- crates/napi/src/next_api/project.rs | 4 ++-- crates/next-core/src/util.rs | 2 +- turbopack/crates/turbopack-ecmascript/src/minify.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/napi/src/next_api/project.rs b/crates/napi/src/next_api/project.rs index 3a433c1af25166..03b25122136c4c 100644 --- a/crates/napi/src/next_api/project.rs +++ b/crates/napi/src/next_api/project.rs @@ -1291,7 +1291,7 @@ pub async fn project_get_source_for_asset( bail!("Cannot find source for asset {}", file_path); }; - Ok(Some(source_content.content().to_str()?.to_string())) + Ok(Some(source_content.content().to_str()?.into_owned())) }) .await .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?; @@ -1313,7 +1313,7 @@ pub async fn project_get_source_map( return Ok(None); }; - Ok(Some(map.to_rope().await?.to_str()?.to_string())) + Ok(Some(map.to_rope().await?.to_str()?.into_owned())) }) .await .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?; diff --git a/crates/next-core/src/util.rs b/crates/next-core/src/util.rs index 3d944b8e4028ec..2ac072df938d44 100644 --- a/crates/next-core/src/util.rs +++ b/crates/next-core/src/util.rs @@ -672,7 +672,7 @@ pub async fn load_next_js_template( let path = virtual_next_js_template_path(project_path, path.to_string()); let content = &*file_content_rope(path.read()).await?; - let content = content.to_str()?.to_string(); + let content = content.to_str()?.into_owned(); let parent_path = path.parent(); let parent_path_value = &*parent_path.await?; diff --git a/turbopack/crates/turbopack-ecmascript/src/minify.rs b/turbopack/crates/turbopack-ecmascript/src/minify.rs index 8645164b1f4189..2fc402af510dc7 100644 --- a/turbopack/crates/turbopack-ecmascript/src/minify.rs +++ b/turbopack/crates/turbopack-ecmascript/src/minify.rs @@ -43,7 +43,7 @@ pub async fn minify( let compiler = Arc::new(Compiler::new(cm.clone())); let fm = compiler.cm.new_source_file( FileName::Custom(path.path.to_string()).into(), - code.source_code().to_str()?.to_string(), + code.source_code().to_str()?.into_owned(), ); let lexer = Lexer::new( From 9083d939dbbe3cf0863276700cc41375f3f4a473 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 23 Jan 2025 10:21:11 +0100 Subject: [PATCH 06/61] [Turbopack] Memory improvements (#75210) ### What? * add tracing for resolve plugins * convert BeforeResolvePluginCondition::matches to a turbo-tasks function * remove storage on update too * avoid overreserving storages * remove PersistentUpperCount --- .../backend/operation/aggregation_update.rs | 35 ++--------- .../src/backend/storage.rs | 58 +++++++------------ .../crates/turbo-tasks-backend/src/data.rs | 8 --- .../crates/turbopack-core/src/resolve/mod.rs | 4 +- .../turbopack-core/src/resolve/plugin.rs | 18 +++--- 5 files changed, 40 insertions(+), 83 deletions(-) diff --git a/turbopack/crates/turbo-tasks-backend/src/backend/operation/aggregation_update.rs b/turbopack/crates/turbo-tasks-backend/src/backend/operation/aggregation_update.rs index 3ca592e66a08ba..f891387ddf4c7b 100644 --- a/turbopack/crates/turbo-tasks-backend/src/backend/operation/aggregation_update.rs +++ b/turbopack/crates/turbo-tasks-backend/src/backend/operation/aggregation_update.rs @@ -22,9 +22,7 @@ use crate::{ backend::{ get_mut, get_mut_or_insert_with, operation::{invalidate::make_task_dirty, ExecuteContext, Operation, TaskGuard}, - storage::{ - count, get, get_many, iter_many, remove, update, update_count, update_ucount_and_get, - }, + storage::{count, get, get_many, iter_many, remove, update, update_count}, TaskDataCategory, }, data::{ @@ -940,10 +938,7 @@ impl AggregationUpdateQueue { // Add the same amount of upper edges if update_count!(task, Upper { task: upper_id }, count) { - if !upper_id.is_transient() - && update_ucount_and_get!(task, PersistentUpperCount, 1) - .is_power_of_two() - { + if count!(task, Upper).is_power_of_two() { self.push_optimize_task(task_id); } // When this is a new inner node, update aggregated data and @@ -1003,10 +998,6 @@ impl AggregationUpdateQueue { #[cfg(feature = "trace_aggregation_update")] let _span = trace_span!("make follower").entered(); - if !upper_id.is_transient() { - update_ucount_and_get!(task, PersistentUpperCount, -1); - } - let upper_ids: Vec<_> = get_uppers(&upper); // Add the same amount of follower edges @@ -1160,8 +1151,6 @@ impl AggregationUpdateQueue { keep_upper }); if !upper_ids.is_empty() { - update_ucount_and_get!(follower, PersistentUpperCount, -persistent_uppers); - let data = AggregatedDataUpdate::from_task(&mut follower).invert(); let followers: Vec<_> = get_followers(&follower); drop(follower); @@ -1258,10 +1247,6 @@ impl AggregationUpdateQueue { Some(old - 1) }); if remove_upper { - if !upper_id.is_transient() { - update_ucount_and_get!(follower, PersistentUpperCount, -1); - } - let data = AggregatedDataUpdate::from_task(&mut follower).invert(); let followers: Vec<_> = get_followers(&follower); drop(follower); @@ -1415,8 +1400,7 @@ impl AggregationUpdateQueue { #[cfg(feature = "trace_aggregation_update")] let _span = trace_span!("new inner").entered(); if !upper_ids.is_empty() { - let new_count = - update_ucount_and_get!(follower, PersistentUpperCount, persistent_uppers); + let new_count = count!(follower, Upper); if (new_count - persistent_uppers).next_power_of_two() != new_count.next_power_of_two() { @@ -1546,10 +1530,7 @@ impl AggregationUpdateQueue { for &(follower_id, _) in followers_with_aggregation_number.iter() { let mut follower = ctx.task(follower_id, TaskDataCategory::Meta); if update_count!(follower, Upper { task: upper_id }, 1) { - if !upper_id.is_transient() - && update_ucount_and_get!(follower, PersistentUpperCount, 1) - .is_power_of_two() - { + if !upper_id.is_transient() && count!(follower, Upper).is_power_of_two() { self.push_optimize_task(follower_id); } @@ -1715,9 +1696,7 @@ impl AggregationUpdateQueue { drop(upper); let mut follower = ctx.task(new_follower_id, TaskDataCategory::Meta); if update_count!(follower, Upper { task: upper_id }, 1) { - if !upper_id.is_transient() - && update_ucount_and_get!(follower, PersistentUpperCount, 1).is_power_of_two() - { + if !upper_id.is_transient() && count!(follower, Upper).is_power_of_two() { self.push_optimize_task(new_follower_id); } // It's a new upper @@ -1915,9 +1894,7 @@ impl AggregationUpdateQueue { } children_count }; - let upper_count = get!(task, PersistentUpperCount) - .copied() - .unwrap_or_default() as usize; + let upper_count = count!(task, Upper); if upper_count <= 1 || upper_count.saturating_sub(1) * follower_count <= max( diff --git a/turbopack/crates/turbo-tasks-backend/src/backend/storage.rs b/turbopack/crates/turbo-tasks-backend/src/backend/storage.rs index ec3b206927f1dd..8fd1df7c09b81f 100644 --- a/turbopack/crates/turbo-tasks-backend/src/backend/storage.rs +++ b/turbopack/crates/turbo-tasks-backend/src/backend/storage.rs @@ -147,6 +147,7 @@ impl InnerStorage { if let Some(i) = i { &mut self.map[i] } else { + self.map.reserve_exact(1); self.map.push(CachedDataItemStorage::new(ty)); self.map.last_mut().unwrap() } @@ -160,6 +161,18 @@ impl InnerStorage { self.map.iter_mut().position(|m| m.ty() == ty) } + fn get_or_create_map_index(&mut self, ty: CachedDataItemType) -> usize { + let i = self.map.iter().position(|m| m.ty() == ty); + if let Some(i) = i { + i + } else { + let i = self.map.len(); + self.map.reserve_exact(1); + self.map.push(CachedDataItemStorage::new(ty)); + i + } + } + fn get_map(&self, ty: CachedDataItemType) -> Option<&CachedDataItemStorage> { self.map.iter().find(|m| m.ty() == ty) } @@ -180,6 +193,7 @@ impl InnerStorage { let result = storage.remove(key); if result.is_some() && storage.is_empty() { self.map.swap_remove(i); + self.map.shrink_to_fit(); } result }) @@ -254,6 +268,7 @@ impl InnerStorage { .collect::>(); if self.map[i].is_empty() { self.map.swap_remove(i); + self.map.shrink_to_fit(); } Either::Right(items.into_iter()) } @@ -263,9 +278,13 @@ impl InnerStorage { key: CachedDataItemKey, update: impl FnOnce(Option) -> Option, ) { - let map = self.get_or_create_map_mut(key.ty()); + let i = self.get_or_create_map_index(key.ty()); + let map = &mut self.map[i]; if let Some(v) = update(map.remove(&key)) { map.insert(CachedDataItem::from_key_and_value(key, v)); + } else if map.is_empty() { + self.map.swap_remove(i); + self.map.shrink_to_fit(); } } @@ -458,42 +477,6 @@ macro_rules! update { }; } -macro_rules! update_ucount_and_get { - ($task:ident, $key:ident $input:tt, -$update:expr) => {{ - let update = $update; - let mut value = 0; - $crate::backend::storage::update!($task, $key $input, |old: Option<_>| { - if let Some(old) = old { - value = old - update; - (value != 0).then_some(value) - } else { - None - } - }); - value - }}; - ($task:ident, $key:ident $input:tt, $update:expr) => {{ - let update = $update; - let mut value = 0; - $crate::backend::storage::update!($task, $key $input, |old: Option<_>| { - if let Some(old) = old { - value = old + update; - (value != 0).then_some(value) - } else { - value = update; - (update != 0).then_some(update) - } - }); - value - }}; - ($task:ident, $key:ident, -$update:expr) => { - $crate::backend::storage::update_ucount_and_get!($task, $key {}, -$update) - }; - ($task:ident, $key:ident, $update:expr) => { - $crate::backend::storage::update_ucount_and_get!($task, $key {}, $update) - }; -} - macro_rules! update_count { ($task:ident, $key:ident $input:tt, -$update:expr) => {{ let update = $update; @@ -562,4 +545,3 @@ pub(crate) use iter_many; pub(crate) use remove; pub(crate) use update; pub(crate) use update_count; -pub(crate) use update_ucount_and_get; diff --git a/turbopack/crates/turbo-tasks-backend/src/data.rs b/turbopack/crates/turbo-tasks-backend/src/data.rs index ebed696d5e1ea6..594e7c03ae1d2e 100644 --- a/turbopack/crates/turbo-tasks-backend/src/data.rs +++ b/turbopack/crates/turbo-tasks-backend/src/data.rs @@ -418,10 +418,6 @@ pub enum CachedDataItem { task: TaskId, value: i32, }, - PersistentUpperCount { - // Only counting persistent tasks - value: u32, - }, // Aggregated Data AggregatedDirtyContainer { @@ -505,7 +501,6 @@ impl CachedDataItem { CachedDataItem::AggregationNumber { .. } => true, CachedDataItem::Follower { task, .. } => !task.is_transient(), CachedDataItem::Upper { task, .. } => !task.is_transient(), - CachedDataItem::PersistentUpperCount { .. } => true, CachedDataItem::AggregatedDirtyContainer { task, .. } => !task.is_transient(), CachedDataItem::AggregatedCollectible { collectible, .. } => { !collectible.cell.task.is_transient() @@ -563,7 +558,6 @@ impl CachedDataItem { | Self::Dirty { .. } | Self::Follower { .. } | Self::Upper { .. } - | Self::PersistentUpperCount { .. } | Self::AggregatedDirtyContainer { .. } | Self::AggregatedCollectible { .. } | Self::AggregatedDirtyContainerCount { .. } => TaskDataCategory::Meta, @@ -601,7 +595,6 @@ impl CachedDataItemKey { CachedDataItemKey::AggregationNumber { .. } => true, CachedDataItemKey::Follower { task, .. } => !task.is_transient(), CachedDataItemKey::Upper { task, .. } => !task.is_transient(), - CachedDataItemKey::PersistentUpperCount {} => true, CachedDataItemKey::AggregatedDirtyContainer { task, .. } => !task.is_transient(), CachedDataItemKey::AggregatedCollectible { collectible, .. } => { !collectible.cell.task.is_transient() @@ -647,7 +640,6 @@ impl CachedDataItemType { | Self::Dirty { .. } | Self::Follower { .. } | Self::Upper { .. } - | Self::PersistentUpperCount { .. } | Self::AggregatedDirtyContainer { .. } | Self::AggregatedCollectible { .. } | Self::AggregatedDirtyContainerCount { .. } => TaskDataCategory::Meta, diff --git a/turbopack/crates/turbopack-core/src/resolve/mod.rs b/turbopack/crates/turbopack-core/src/resolve/mod.rs index 5c47848380354b..39cc3d5be34b95 100644 --- a/turbopack/crates/turbopack-core/src/resolve/mod.rs +++ b/turbopack/crates/turbopack-core/src/resolve/mod.rs @@ -1583,6 +1583,7 @@ pub async fn url_resolve( .await } +#[tracing::instrument(level = "trace", skip_all)] async fn handle_before_resolve_plugins( lookup_path: Vc, reference_type: Value, @@ -1591,7 +1592,7 @@ async fn handle_before_resolve_plugins( ) -> Result>> { for plugin in &options.await?.before_resolve_plugins { let condition = plugin.before_resolve_condition().resolve().await?; - if !condition.await?.matches(request).await? { + if !*condition.matches(request).await? { continue; } @@ -1605,6 +1606,7 @@ async fn handle_before_resolve_plugins( Ok(None) } +#[tracing::instrument(level = "trace", skip_all)] async fn handle_after_resolve_plugins( lookup_path: Vc, reference_type: Value, diff --git a/turbopack/crates/turbopack-core/src/resolve/plugin.rs b/turbopack/crates/turbopack-core/src/resolve/plugin.rs index 5cc0095be4e46c..17ed289ce11873 100644 --- a/turbopack/crates/turbopack-core/src/resolve/plugin.rs +++ b/turbopack/crates/turbopack-core/src/resolve/plugin.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use anyhow::Result; use turbo_rcstr::RcStr; use turbo_tasks::{ResolvedVc, Value, Vc}; @@ -43,14 +45,14 @@ impl AfterResolvePluginCondition { #[turbo_tasks::value] pub enum BeforeResolvePluginCondition { Request(ResolvedVc), - Modules(ResolvedVc>), + Modules(HashSet), } #[turbo_tasks::value_impl] impl BeforeResolvePluginCondition { #[turbo_tasks::function] - pub fn from_modules(modules: ResolvedVc>) -> Vc { - BeforeResolvePluginCondition::Modules(modules).cell() + pub async fn from_modules(modules: ResolvedVc>) -> Result> { + Ok(BeforeResolvePluginCondition::Modules(modules.await?.iter().cloned().collect()).cell()) } #[turbo_tasks::function] @@ -59,21 +61,23 @@ impl BeforeResolvePluginCondition { } } +#[turbo_tasks::value_impl] impl BeforeResolvePluginCondition { - pub async fn matches(&self, request: Vc) -> Result { - Ok(match self { + #[turbo_tasks::function] + pub async fn matches(&self, request: Vc) -> Result> { + Ok(Vc::cell(match self { BeforeResolvePluginCondition::Request(glob) => match request.await?.request() { Some(request) => glob.await?.execute(request.as_str()), None => false, }, BeforeResolvePluginCondition::Modules(modules) => { if let Request::Module { module, .. } = &*request.await? { - modules.await?.contains(module) + modules.contains(module) } else { false } } - }) + })) } } From f10073be5943c04e8e12b2908b9c61c028b28df0 Mon Sep 17 00:00:00 2001 From: Jiwon Choi Date: Thu, 23 Jan 2025 19:33:52 +0900 Subject: [PATCH 07/61] eslint: allow ts extensions for config (#75222) ### Why? `eslint.config.*ts` config files were not included to be calculated before as they are experimental but shouldn't be a blocker from us, so uncommented them from the supported config files. Will follow up with eslint config test cases when we enable the flat config on `eslint-config-next`, but confirmed on local that `.(m|c)ts` extensions work. Fixes #74900 --- packages/next/src/lib/eslint/runLintCheck.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/next/src/lib/eslint/runLintCheck.ts b/packages/next/src/lib/eslint/runLintCheck.ts index 9d78b59c08b591..1851a7f0bb3a74 100644 --- a/packages/next/src/lib/eslint/runLintCheck.ts +++ b/packages/next/src/lib/eslint/runLintCheck.ts @@ -360,12 +360,11 @@ export async function runLintCheck( 'eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs', - // TODO(jiwon): Support when it's stable. - // TS extensions are experimental and requires to install another package `jiti`. + // TS extensions require to install a separate package `jiti`. // https://eslint.org/docs/latest/use/configure/configuration-files#typescript-configuration-files - // 'eslint.config.ts', - // 'eslint.config.mts', - // 'eslint.config.cts', + 'eslint.config.ts', + 'eslint.config.mts', + 'eslint.config.cts', // eslint <= v8 '.eslintrc.js', '.eslintrc.cjs', From f4177614a0d034c32d73fd85f739a8dce45b750b Mon Sep 17 00:00:00 2001 From: Will Binns-Smith Date: Thu, 23 Jan 2025 05:25:27 -0800 Subject: [PATCH 08/61] Split entrypoint/route handling into separate dev and prod versions (#75169) Currently, for `handleEntrypoints`, `handlePagesErrorRoute`, `handleRouteType`, etc, both dev and prod use cases are combined into the same functions. This leads to a lot of branching, use-case-specific arguments, etc. for minimal opportunity for shared code. In a following PR, entrypoint writing for prod will be done through a single napi call to rust, further branching the two versions. This splits them formally, at the cost of duplicating a handful of lines in each case. Perhaps in the future we can develop a better system for sharing code, but this makes things far clearer and easier to maintain in this moment. Test Plan: CI --- packages/next/src/build/handle-entrypoints.ts | 334 ++++++++++++++ packages/next/src/build/index.ts | 36 +- packages/next/src/build/swc/index.ts | 2 +- packages/next/src/build/swc/types.ts | 46 +- .../src/server/dev/hot-reloader-turbopack.ts | 26 +- .../next/src/server/dev/turbopack-utils.ts | 431 ++++-------------- .../next/src/server/dev/turbopack/types.ts | 49 -- .../lib/router-utils/setup-dev-bundler.ts | 8 +- .../dev => shared/lib}/turbopack/entry-key.ts | 0 .../lib}/turbopack/manifest-loader.ts | 18 +- .../next/src/shared/lib/turbopack/utils.ts | 241 ++++++++++ test/development/basic/next-rs-api.test.ts | 8 +- 12 files changed, 754 insertions(+), 445 deletions(-) create mode 100644 packages/next/src/build/handle-entrypoints.ts delete mode 100644 packages/next/src/server/dev/turbopack/types.ts rename packages/next/src/{server/dev => shared/lib}/turbopack/entry-key.ts (100%) rename packages/next/src/{server/dev => shared/lib}/turbopack/manifest-loader.ts (97%) create mode 100644 packages/next/src/shared/lib/turbopack/utils.ts diff --git a/packages/next/src/build/handle-entrypoints.ts b/packages/next/src/build/handle-entrypoints.ts new file mode 100644 index 00000000000000..adcac7d45049f3 --- /dev/null +++ b/packages/next/src/build/handle-entrypoints.ts @@ -0,0 +1,334 @@ +import type { CustomRoutes } from '../lib/load-custom-routes' +import type { TurbopackManifestLoader } from '../shared/lib/turbopack/manifest-loader' +import type { + TurbopackResult, + RawEntrypoints, + Entrypoints, + PageRoute, + AppRoute, +} from './swc/types' +import * as Log from './output/log' +import { getEntryKey } from '../shared/lib/turbopack/entry-key' +import { + processIssues, + type EntryIssuesMap, +} from '../shared/lib/turbopack/utils' + +export async function handleEntrypoints({ + entrypoints, + currentEntrypoints, + currentEntryIssues, + manifestLoader, + productionRewrites, + logErrors, +}: { + entrypoints: TurbopackResult + currentEntrypoints: Entrypoints + currentEntryIssues: EntryIssuesMap + manifestLoader: TurbopackManifestLoader + productionRewrites: CustomRoutes['rewrites'] | undefined + logErrors: boolean +}) { + currentEntrypoints.global.app = entrypoints.pagesAppEndpoint + currentEntrypoints.global.document = entrypoints.pagesDocumentEndpoint + currentEntrypoints.global.error = entrypoints.pagesErrorEndpoint + + currentEntrypoints.global.instrumentation = entrypoints.instrumentation + + currentEntrypoints.page.clear() + currentEntrypoints.app.clear() + + for (const [pathname, route] of entrypoints.routes) { + switch (route.type) { + case 'page': + case 'page-api': + currentEntrypoints.page.set(pathname, route) + break + case 'app-page': { + route.pages.forEach((page) => { + currentEntrypoints.app.set(page.originalName, { + type: 'app-page', + ...page, + }) + }) + break + } + case 'app-route': { + currentEntrypoints.app.set(route.originalName, route) + break + } + default: + Log.info(`skipping ${pathname} (${route.type})`) + break + } + } + + const { middleware, instrumentation } = entrypoints + + // We check for explicit true/false, since it's initialized to + // undefined during the first loop (middlewareChanges event is + // unnecessary during the first serve) + if (currentEntrypoints.global.middleware && !middleware) { + const key = getEntryKey('root', 'server', 'middleware') + // Went from middleware to no middleware + currentEntryIssues.delete(key) + } + + currentEntrypoints.global.middleware = middleware + + if (instrumentation) { + const processInstrumentation = async ( + name: string, + prop: 'nodeJs' | 'edge' + ) => { + const key = getEntryKey('root', 'server', name) + + const writtenEndpoint = await instrumentation[prop].writeToDisk() + processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) + } + await processInstrumentation('instrumentation.nodeJs', 'nodeJs') + await processInstrumentation('instrumentation.edge', 'edge') + await manifestLoader.loadMiddlewareManifest( + 'instrumentation', + 'instrumentation' + ) + await manifestLoader.writeManifests({ + devRewrites: undefined, + productionRewrites, + entrypoints: currentEntrypoints, + }) + } + + if (middleware) { + const key = getEntryKey('root', 'server', 'middleware') + + const endpoint = middleware.endpoint + + async function processMiddleware() { + const writtenEndpoint = await endpoint.writeToDisk() + processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) + await manifestLoader.loadMiddlewareManifest('middleware', 'middleware') + } + await processMiddleware() + } else { + manifestLoader.deleteMiddlewareManifest( + getEntryKey('root', 'server', 'middleware') + ) + } +} + +export async function handlePagesErrorRoute({ + currentEntryIssues, + entrypoints, + manifestLoader, + productionRewrites, + logErrors, +}: { + currentEntryIssues: EntryIssuesMap + entrypoints: Entrypoints + manifestLoader: TurbopackManifestLoader + productionRewrites: CustomRoutes['rewrites'] | undefined + logErrors: boolean +}) { + if (entrypoints.global.app) { + const key = getEntryKey('pages', 'server', '_app') + const writtenEndpoint = await entrypoints.global.app.writeToDisk() + processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) + } + await manifestLoader.loadBuildManifest('_app') + await manifestLoader.loadPagesManifest('_app') + await manifestLoader.loadFontManifest('_app') + + if (entrypoints.global.document) { + const key = getEntryKey('pages', 'server', '_document') + const writtenEndpoint = await entrypoints.global.document.writeToDisk() + processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) + } + await manifestLoader.loadPagesManifest('_document') + + if (entrypoints.global.error) { + const key = getEntryKey('pages', 'server', '_error') + const writtenEndpoint = await entrypoints.global.error.writeToDisk() + processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) + } + + await manifestLoader.loadBuildManifest('_error') + await manifestLoader.loadPagesManifest('_error') + await manifestLoader.loadFontManifest('_error') + + await manifestLoader.writeManifests({ + devRewrites: undefined, + productionRewrites, + entrypoints, + }) +} + +export async function handleRouteType({ + page, + route, + currentEntryIssues, + entrypoints, + manifestLoader, + productionRewrites, + logErrors, +}: { + page: string + route: PageRoute | AppRoute + + currentEntryIssues: EntryIssuesMap + entrypoints: Entrypoints + manifestLoader: TurbopackManifestLoader + productionRewrites: CustomRoutes['rewrites'] | undefined + logErrors: boolean +}) { + const shouldCreateWebpackStats = process.env.TURBOPACK_STATS != null + + switch (route.type) { + case 'page': { + const serverKey = getEntryKey('pages', 'server', page) + + if (entrypoints.global.app) { + const key = getEntryKey('pages', 'server', '_app') + + const writtenEndpoint = await entrypoints.global.app.writeToDisk() + processIssues( + currentEntryIssues, + key, + writtenEndpoint, + false, + logErrors + ) + } + await manifestLoader.loadBuildManifest('_app') + await manifestLoader.loadPagesManifest('_app') + + if (entrypoints.global.document) { + const key = getEntryKey('pages', 'server', '_document') + + const writtenEndpoint = await entrypoints.global.document.writeToDisk() + processIssues( + currentEntryIssues, + key, + writtenEndpoint, + false, + logErrors + ) + } + await manifestLoader.loadPagesManifest('_document') + + const writtenEndpoint = await route.htmlEndpoint.writeToDisk() + + const type = writtenEndpoint?.type + + await manifestLoader.loadBuildManifest(page) + await manifestLoader.loadPagesManifest(page) + if (type === 'edge') { + await manifestLoader.loadMiddlewareManifest(page, 'pages') + } else { + manifestLoader.deleteMiddlewareManifest(serverKey) + } + await manifestLoader.loadFontManifest('/_app', 'pages') + await manifestLoader.loadFontManifest(page, 'pages') + + if (shouldCreateWebpackStats) { + await manifestLoader.loadWebpackStats(page, 'pages') + } + + await manifestLoader.writeManifests({ + devRewrites: undefined, + productionRewrites, + entrypoints, + }) + + processIssues( + currentEntryIssues, + serverKey, + writtenEndpoint, + false, + logErrors + ) + + break + } + case 'page-api': { + const key = getEntryKey('pages', 'server', page) + + const writtenEndpoint = await route.endpoint.writeToDisk() + + const type = writtenEndpoint.type + + await manifestLoader.loadPagesManifest(page) + if (type === 'edge') { + await manifestLoader.loadMiddlewareManifest(page, 'pages') + } else { + manifestLoader.deleteMiddlewareManifest(key) + } + + await manifestLoader.writeManifests({ + devRewrites: undefined, + productionRewrites, + entrypoints, + }) + + processIssues(currentEntryIssues, key, writtenEndpoint, true, logErrors) + + break + } + case 'app-page': { + const key = getEntryKey('app', 'server', page) + const writtenEndpoint = await route.htmlEndpoint.writeToDisk() + const type = writtenEndpoint.type + + if (type === 'edge') { + await manifestLoader.loadMiddlewareManifest(page, 'app') + } else { + manifestLoader.deleteMiddlewareManifest(key) + } + + await manifestLoader.loadAppBuildManifest(page) + await manifestLoader.loadBuildManifest(page, 'app') + await manifestLoader.loadAppPathsManifest(page) + await manifestLoader.loadActionManifest(page) + await manifestLoader.loadFontManifest(page, 'app') + + if (shouldCreateWebpackStats) { + await manifestLoader.loadWebpackStats(page, 'app') + } + + await manifestLoader.writeManifests({ + devRewrites: undefined, + productionRewrites, + entrypoints, + }) + + processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) + + break + } + case 'app-route': { + const key = getEntryKey('app', 'server', page) + const writtenEndpoint = await route.endpoint.writeToDisk() + const type = writtenEndpoint.type + + await manifestLoader.loadAppPathsManifest(page) + + if (type === 'edge') { + await manifestLoader.loadMiddlewareManifest(page, 'app') + } else { + manifestLoader.deleteMiddlewareManifest(key) + } + + await manifestLoader.writeManifests({ + devRewrites: undefined, + productionRewrites, + entrypoints, + }) + processIssues(currentEntryIssues, key, writtenEndpoint, true, logErrors) + + break + } + default: { + throw new Error(`unknown route type ${(route as any).type} for ${page}`) + } + } +} diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 47798a405cd215..e4e055f2ac40e4 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -186,18 +186,7 @@ import { import { getStartServerInfo, logStartInfo } from '../server/lib/app-info-log' import type { NextEnabledDirectories } from '../server/base-server' import { hasCustomExportOutput } from '../export/utils' -import { - getTurbopackJsConfig, - handleEntrypoints, - type EntryIssuesMap, - handleRouteType, - handlePagesErrorRoute, - formatIssue, - isRelevantWarning, - isPersistentCachingEnabled, -} from '../server/dev/turbopack-utils' -import { TurbopackManifestLoader } from '../server/dev/turbopack/manifest-loader' -import type { Entrypoints } from '../server/dev/turbopack/types' +import { TurbopackManifestLoader } from '../shared/lib/turbopack/manifest-loader' import { buildCustomRoute } from '../lib/build-custom-route' import { createProgress } from './progress' import { traceMemoryUsage } from '../lib/memory/trace' @@ -218,6 +207,19 @@ import { import { InvariantError } from '../shared/lib/invariant-error' import { HTML_LIMITED_BOT_UA_RE_STRING } from '../shared/lib/router/utils/is-bot' import type { UseCacheTrackerKey } from './webpack/plugins/telemetry-plugin/use-cache-tracker-utils' +import { + handleEntrypoints, + handlePagesErrorRoute, + handleRouteType, +} from './handle-entrypoints' +import type { Entrypoints } from './swc/types' +import { + formatIssue, + getTurbopackJsConfig, + isPersistentCachingEnabled, + isRelevantWarning, + type EntryIssuesMap, +} from '../shared/lib/turbopack/utils' type Fallback = null | boolean | string @@ -1489,7 +1491,6 @@ export default async function build( currentEntrypoints, currentEntryIssues, manifestLoader, - devRewrites: undefined, productionRewrites: customRoutes.rewrites, logErrors: false, }) @@ -1530,15 +1531,11 @@ export default async function build( for (const [page, route] of currentEntrypoints.page) { enqueue(() => handleRouteType({ - dev, page, - pathname: page, route, - currentEntryIssues, entrypoints: currentEntrypoints, manifestLoader, - devRewrites: undefined, productionRewrites: customRoutes.rewrites, logErrors: false, }) @@ -1550,13 +1547,10 @@ export default async function build( enqueue(() => handleRouteType({ page, - dev: false, - pathname: normalizeAppPath(page), route, currentEntryIssues, entrypoints: currentEntrypoints, manifestLoader, - devRewrites: undefined, productionRewrites: customRoutes.rewrites, logErrors: false, }) @@ -1565,11 +1559,9 @@ export default async function build( enqueue(() => handlePagesErrorRoute({ - dev: false, currentEntryIssues, entrypoints: currentEntrypoints, manifestLoader, - devRewrites: undefined, productionRewrites: customRoutes.rewrites, logErrors: false, }) diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index 1640442bd2f4af..58e17490b974c8 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -21,7 +21,6 @@ import { getDefineEnv, } from '../webpack/plugins/define-env-plugin' import { getReactCompilerLoader } from '../get-babel-loader-config' -import { TurbopackInternalError } from '../../server/dev/turbopack-utils' import type { NapiPartialProjectOptions, NapiProjectOptions, @@ -41,6 +40,7 @@ import type { UpdateMessage, WrittenEndpoint, } from './types' +import { TurbopackInternalError } from '../../shared/lib/turbopack/utils' type RawBindings = typeof import('./generated-native') type RawWasmBindings = typeof import('./generated-wasm') & { diff --git a/packages/next/src/build/swc/types.ts b/packages/next/src/build/swc/types.ts index 67af229ca489b8..8327e8bb5bb86f 100644 --- a/packages/next/src/build/swc/types.ts +++ b/packages/next/src/build/swc/types.ts @@ -120,7 +120,7 @@ export interface Instrumentation { edge: Endpoint } -export interface Entrypoints { +export interface RawEntrypoints { routes: Map middleware?: Middleware instrumentation?: Instrumentation @@ -192,7 +192,7 @@ export interface UpdateInfo { export interface Project { update(options: Partial): Promise - entrypointsSubscribe(): AsyncIterableIterator> + entrypointsSubscribe(): AsyncIterableIterator> hmrEvents(identifier: string): AsyncIterableIterator> @@ -395,3 +395,45 @@ export interface DefineEnv { } export type RustifiedEnv = { name: string; value: string }[] + +export interface GlobalEntrypoints { + app: Endpoint | undefined + document: Endpoint | undefined + error: Endpoint | undefined + middleware: Middleware | undefined + instrumentation: Instrumentation | undefined +} + +export type PageRoute = + | { + type: 'page' + htmlEndpoint: Endpoint + dataEndpoint: Endpoint + } + | { + type: 'page-api' + endpoint: Endpoint + } + +export type AppRoute = + | { + type: 'app-page' + htmlEndpoint: Endpoint + rscEndpoint: Endpoint + } + | { + type: 'app-route' + endpoint: Endpoint + } + +// pathname -> route +export type PageEntrypoints = Map + +// originalName / page -> route +export type AppEntrypoints = Map + +export type Entrypoints = { + global: GlobalEntrypoints + page: PageEntrypoints + app: AppEntrypoints +} diff --git a/packages/next/src/server/dev/hot-reloader-turbopack.ts b/packages/next/src/server/dev/hot-reloader-turbopack.ts index 47af297218d72e..2b224932a9ae4c 100644 --- a/packages/next/src/server/dev/hot-reloader-turbopack.ts +++ b/packages/next/src/server/dev/hot-reloader-turbopack.ts @@ -22,6 +22,7 @@ import type { WrittenEndpoint, TurbopackResult, Project, + Entrypoints, } from '../../build/swc/types' import { createDefineEnv } from '../../build/swc' import * as Log from '../../build/output/log' @@ -47,40 +48,31 @@ import { AssetMapper, type ChangeSubscriptions, type ClientState, - type EntryIssuesMap, - formatIssue, - getTurbopackJsConfig, handleEntrypoints, handlePagesErrorRoute, handleRouteType, hasEntrypointForKey, msToNs, - processIssues, type ReadyIds, - renderStyledStringToErrorAnsi, type SendHmr, type StartBuilding, processTopLevelIssues, - type TopLevelIssuesMap, - isWellKnownError, printNonFatalIssue, normalizedPageToTurbopackStructureRoute, - isPersistentCachingEnabled, } from './turbopack-utils' import { propagateServerField, type ServerFields, type SetupOpts, } from '../lib/router-utils/setup-dev-bundler' -import { TurbopackManifestLoader } from './turbopack/manifest-loader' -import type { Entrypoints } from './turbopack/types' +import { TurbopackManifestLoader } from '../../shared/lib/turbopack/manifest-loader' import { findPagePathData } from './on-demand-entry-handler' import type { RouteDefinition } from '../route-definitions/route-definition' import { type EntryKey, getEntryKey, splitEntryKey, -} from './turbopack/entry-key' +} from '../../shared/lib/turbopack/entry-key' import { FAST_REFRESH_RUNTIME_RELOAD } from './messages' import { generateEncryptionKeyBase64 } from '../app-render/encryption-utils-server' import { isAppPageRouteDefinition } from '../route-definitions/app-page-route-definition' @@ -92,6 +84,16 @@ import { type ModernSourceMapPayload, } from '../patch-error-inspect' import { getNextErrorFeedbackMiddleware } from '../../client/components/react-dev-overlay/server/get-next-error-feedback-middleware' +import { + formatIssue, + getTurbopackJsConfig, + isPersistentCachingEnabled, + isWellKnownError, + processIssues, + renderStyledStringToErrorAnsi, + type EntryIssuesMap, + type TopLevelIssuesMap, +} from '../../shared/lib/turbopack/utils' // import { getSupportedBrowsers } from '../../build/utils' const wsServer = new ws.Server({ noServer: true }) @@ -973,14 +975,12 @@ export async function createHotReloaderTurbopack( let finishBuilding = startBuilding(pathname, requestUrl, false) try { await handlePagesErrorRoute({ - dev: true, currentEntryIssues, entrypoints: currentEntrypoints, manifestLoader, devRewrites: opts.fsChecker.rewrites, productionRewrites: undefined, logErrors: true, - hooks: { subscribeToChanges, handleWrittenEndpoint: (id, result) => { diff --git a/packages/next/src/server/dev/turbopack-utils.ts b/packages/next/src/server/dev/turbopack-utils.ts index e8b0ce1407a008..39ff45754e7f79 100644 --- a/packages/next/src/server/dev/turbopack-utils.ts +++ b/packages/next/src/server/dev/turbopack-utils.ts @@ -1,83 +1,40 @@ -import type { NextConfigComplete } from '../config-shared' -import loadJsConfig from '../../build/load-jsconfig' import type { ServerFields, SetupOpts, } from '../lib/router-utils/setup-dev-bundler' import type { Issue, - StyledString, TurbopackResult, Endpoint, - Entrypoints as RawEntrypoints, + RawEntrypoints, Update as TurbopackUpdate, WrittenEndpoint, } from '../../build/swc/types' -import { - decodeMagicIdentifier, - MAGIC_IDENTIFIER_REGEX, -} from '../../shared/lib/magic-identifier' -import { bold, green, magenta, red } from '../../lib/picocolors' import { type HMR_ACTION_TYPES, HMR_ACTIONS_SENT_TO_BROWSER, } from './hot-reloader-types' import * as Log from '../../build/output/log' import type { PropagateToWorkersField } from '../lib/router-utils/types' -import type { TurbopackManifestLoader } from './turbopack/manifest-loader' -import type { AppRoute, Entrypoints, PageRoute } from './turbopack/types' +import type { TurbopackManifestLoader } from '../../shared/lib/turbopack/manifest-loader' +import type { AppRoute, Entrypoints, PageRoute } from '../../build/swc/types' import { type EntryKey, getEntryKey, splitEntryKey, -} from './turbopack/entry-key' +} from '../../shared/lib/turbopack/entry-key' import type ws from 'next/dist/compiled/ws' -import isInternal from '../../shared/lib/is-internal' import { isMetadataRoute } from '../../lib/metadata/is-metadata-route' import type { CustomRoutes } from '../../lib/load-custom-routes' - -export async function getTurbopackJsConfig( - dir: string, - nextConfig: NextConfigComplete -) { - const { jsConfig } = await loadJsConfig(dir, nextConfig) - return jsConfig ?? { compilerOptions: {} } -} - -// An error generated from emitted Turbopack issues. This can include build -// errors caused by issues with user code. -export class ModuleBuildError extends Error { - name = 'ModuleBuildError' -} - -// An error caused by an internal issue in Turbopack. These should be written -// to a log file and details should not be shown to the user. -export class TurbopackInternalError extends Error { - name = 'TurbopackInternalError' - - constructor(cause: Error) { - super(cause.message) - this.stack = cause.stack - } -} - -/** - * Thin stopgap workaround layer to mimic existing wellknown-errors-plugin in webpack's build - * to emit certain type of errors into cli. - */ -export function isWellKnownError(issue: Issue): boolean { - const { title } = issue - const formattedTitle = renderStyledStringToErrorAnsi(title) - // TODO: add more well known errors - if ( - formattedTitle.includes('Module not found') || - formattedTitle.includes('Unknown module type') - ) { - return true - } - - return false -} +import { + formatIssue, + getIssueKey, + isRelevantWarning, + processIssues, + renderStyledStringToErrorAnsi, + type EntryIssuesMap, + type TopLevelIssuesMap, +} from '../../shared/lib/turbopack/utils' const onceErrorSet = new Set() /** @@ -116,121 +73,6 @@ export function printNonFatalIssue(issue: Issue) { } } -function isNodeModulesIssue(issue: Issue): boolean { - if (issue.severity === 'warning' && issue.stage === 'config') { - // Override for the externalize issue - // `Package foo (serverExternalPackages or default list) can't be external` - if ( - renderStyledStringToErrorAnsi(issue.title).includes("can't be external") - ) { - return false - } - } - - return ( - issue.severity === 'warning' && - (issue.filePath.match(/^(?:.*[\\/])?node_modules(?:[\\/].*)?$/) !== null || - // Ignore Next.js itself when running next directly in the monorepo where it is not inside - // node_modules anyway. - // TODO(mischnic) prevent matches when this is published to npm - issue.filePath.startsWith('[project]/packages/next/')) - ) -} - -export function isRelevantWarning(issue: Issue): boolean { - return issue.severity === 'warning' && !isNodeModulesIssue(issue) -} - -export function formatIssue(issue: Issue) { - const { filePath, title, description, source } = issue - let { documentationLink } = issue - let formattedTitle = renderStyledStringToErrorAnsi(title).replace( - /\n/g, - '\n ' - ) - - // TODO: Use error codes to identify these - // TODO: Generalize adapting Turbopack errors to Next.js errors - if (formattedTitle.includes('Module not found')) { - // For compatiblity with webpack - // TODO: include columns in webpack errors. - documentationLink = 'https://nextjs.org/docs/messages/module-not-found' - } - - let formattedFilePath = filePath - .replace('[project]/', './') - .replaceAll('/./', '/') - .replace('\\\\?\\', '') - - let message = '' - - if (source && source.range) { - const { start } = source.range - message = `${formattedFilePath}:${start.line + 1}:${ - start.column + 1 - }\n${formattedTitle}` - } else if (formattedFilePath) { - message = `${formattedFilePath}\n${formattedTitle}` - } else { - message = formattedTitle - } - message += '\n' - - if ( - source?.range && - source.source.content && - // ignore Next.js/React internals, as these can often be huge bundled files. - !isInternal(filePath) - ) { - const { start, end } = source.range - const { codeFrameColumns } = require('next/dist/compiled/babel/code-frame') - - message += - codeFrameColumns( - source.source.content, - { - start: { - line: start.line + 1, - column: start.column + 1, - }, - end: { - line: end.line + 1, - column: end.column + 1, - }, - }, - { forceColor: true } - ).trim() + '\n\n' - } - - if (description) { - message += renderStyledStringToErrorAnsi(description) + '\n\n' - } - - // TODO: make it possible to enable this for debugging, but not in tests. - // if (detail) { - // message += renderStyledStringToErrorAnsi(detail) + '\n\n' - // } - - // TODO: Include a trace from the issue. - - if (documentationLink) { - message += documentationLink + '\n\n' - } - - return message -} - -type IssueKey = `${Issue['severity']}-${Issue['filePath']}-${string}-${string}` -export type IssuesMap = Map -export type EntryIssuesMap = Map -export type TopLevelIssuesMap = IssuesMap - -function getIssueKey(issue: Issue): IssueKey { - return `${issue.severity}-${issue.filePath}-${JSON.stringify( - issue.title - )}-${JSON.stringify(issue.description)}` -} - export function processTopLevelIssues( currentTopLevelIssues: TopLevelIssuesMap, result: TurbopackResult @@ -243,74 +85,6 @@ export function processTopLevelIssues( } } -export function processIssues( - currentEntryIssues: EntryIssuesMap, - key: EntryKey, - result: TurbopackResult, - throwIssue: boolean, - logErrors: boolean -) { - const newIssues = new Map() - currentEntryIssues.set(key, newIssues) - - const relevantIssues = new Set() - - for (const issue of result.issues) { - if ( - issue.severity !== 'error' && - issue.severity !== 'fatal' && - issue.severity !== 'warning' - ) - continue - - const issueKey = getIssueKey(issue) - newIssues.set(issueKey, issue) - - if (issue.severity !== 'warning') { - if (throwIssue) { - const formatted = formatIssue(issue) - relevantIssues.add(formatted) - } - // if we throw the issue it will most likely get handed and logged elsewhere - else if (logErrors && isWellKnownError(issue)) { - const formatted = formatIssue(issue) - Log.error(formatted) - } - } - } - - if (relevantIssues.size && throwIssue) { - throw new ModuleBuildError([...relevantIssues].join('\n\n')) - } -} - -export function renderStyledStringToErrorAnsi(string: StyledString): string { - function decodeMagicIdentifiers(str: string): string { - return str.replaceAll(MAGIC_IDENTIFIER_REGEX, (ident) => { - try { - return magenta(`{${decodeMagicIdentifier(ident)}}`) - } catch (e) { - return magenta(`{${ident} (decoding failed: ${e})}`) - } - }) - } - - switch (string.type) { - case 'text': - return decodeMagicIdentifiers(string.value) - case 'strong': - return bold(red(decodeMagicIdentifiers(string.value))) - case 'code': - return green(decodeMagicIdentifiers(string.value)) - case 'line': - return string.value.map(renderStyledStringToErrorAnsi).join('') - case 'stack': - return string.value.map(renderStyledStringToErrorAnsi).join('\n') - default: - throw new Error('Unknown StyledString type', string) - } -} - const MILLISECONDS_IN_NANOSECOND = BigInt(1_000_000) export function msToNs(ms: number): bigint { @@ -789,7 +563,6 @@ export async function handleEntrypoints({ currentEntryIssues, manifestLoader, devRewrites, - productionRewrites, logErrors, dev, }: { @@ -803,7 +576,7 @@ export async function handleEntrypoints({ productionRewrites: CustomRoutes['rewrites'] | undefined logErrors: boolean - dev?: HandleEntrypointsDevOpts + dev: HandleEntrypointsDevOpts }) { currentEntrypoints.global.app = entrypoints.pagesAppEndpoint currentEntrypoints.global.document = entrypoints.pagesDocumentEndpoint @@ -858,12 +631,12 @@ export async function handleEntrypoints({ // Went from middleware to no middleware await dev?.hooks.unsubscribeFromChanges(key) currentEntryIssues.delete(key) - dev?.hooks.sendHmr('middleware', { + dev.hooks.sendHmr('middleware', { event: HMR_ACTIONS_SENT_TO_BROWSER.MIDDLEWARE_CHANGES, }) } else if (!currentEntrypoints.global.middleware && middleware) { // Went from no middleware to middleware - dev?.hooks.sendHmr('middleware', { + dev.hooks.sendHmr('middleware', { event: HMR_ACTIONS_SENT_TO_BROWSER.MIDDLEWARE_CHANGES, }) } @@ -878,7 +651,7 @@ export async function handleEntrypoints({ const key = getEntryKey('root', 'server', name) const writtenEndpoint = await instrumentation[prop].writeToDisk() - dev?.hooks.handleWrittenEndpoint(key, writtenEndpoint) + dev.hooks.handleWrittenEndpoint(key, writtenEndpoint) processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) } await processInstrumentation('instrumentation.nodeJs', 'nodeJs') @@ -889,25 +662,21 @@ export async function handleEntrypoints({ ) await manifestLoader.writeManifests({ devRewrites, - productionRewrites, + productionRewrites: undefined, entrypoints: currentEntrypoints, }) - if (dev) { - dev.serverFields.actualInstrumentationHookFile = '/instrumentation' - await dev.hooks.propagateServerField( - 'actualInstrumentationHookFile', - dev.serverFields.actualInstrumentationHookFile - ) - } + dev.serverFields.actualInstrumentationHookFile = '/instrumentation' + await dev.hooks.propagateServerField( + 'actualInstrumentationHookFile', + dev.serverFields.actualInstrumentationHookFile + ) } else { - if (dev) { - dev.serverFields.actualInstrumentationHookFile = undefined - await dev.hooks.propagateServerField( - 'actualInstrumentationHookFile', - dev.serverFields.actualInstrumentationHookFile - ) - } + dev.serverFields.actualInstrumentationHookFile = undefined + await dev.hooks.propagateServerField( + 'actualInstrumentationHookFile', + dev.serverFields.actualInstrumentationHookFile + ) } if (middleware) { @@ -917,7 +686,7 @@ export async function handleEntrypoints({ async function processMiddleware() { const writtenEndpoint = await endpoint.writeToDisk() - dev?.hooks.handleWrittenEndpoint(key, writtenEndpoint) + dev.hooks.handleWrittenEndpoint(key, writtenEndpoint) processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) await manifestLoader.loadMiddlewareManifest('middleware', 'middleware') if (dev) { @@ -953,7 +722,7 @@ export async function handleEntrypoints({ ) await manifestLoader.writeManifests({ devRewrites, - productionRewrites, + productionRewrites: undefined, entrypoints: currentEntrypoints, }) @@ -971,22 +740,18 @@ export async function handleEntrypoints({ manifestLoader.deleteMiddlewareManifest( getEntryKey('root', 'server', 'middleware') ) - if (dev) { - dev.serverFields.actualMiddlewareFile = undefined - dev.serverFields.middleware = undefined - } + dev.serverFields.actualMiddlewareFile = undefined + dev.serverFields.middleware = undefined } - if (dev) { - await dev.hooks.propagateServerField( - 'actualMiddlewareFile', - dev.serverFields.actualMiddlewareFile - ) - await dev.hooks.propagateServerField( - 'middleware', - dev.serverFields.middleware - ) - } + await dev.hooks.propagateServerField( + 'actualMiddlewareFile', + dev.serverFields.actualMiddlewareFile + ) + await dev.hooks.propagateServerField( + 'middleware', + dev.serverFields.middleware + ) } async function handleEntrypointsDevCleanup({ @@ -1050,49 +815,43 @@ async function handleEntrypointsDevCleanup({ } export async function handlePagesErrorRoute({ - dev, currentEntryIssues, entrypoints, manifestLoader, devRewrites, productionRewrites, logErrors, - hooks, }: { - dev: boolean currentEntryIssues: EntryIssuesMap entrypoints: Entrypoints manifestLoader: TurbopackManifestLoader devRewrites: SetupOpts['fsChecker']['rewrites'] | undefined productionRewrites: CustomRoutes['rewrites'] | undefined logErrors: boolean - - hooks?: HandleRouteTypeHooks // dev + hooks: HandleRouteTypeHooks }) { if (entrypoints.global.app) { const key = getEntryKey('pages', 'server', '_app') const writtenEndpoint = await entrypoints.global.app.writeToDisk() - hooks?.handleWrittenEndpoint(key, writtenEndpoint) - if (dev) { - hooks?.subscribeToChanges( - key, - false, - entrypoints.global.app, - () => { - // There's a special case for this in `../client/page-bootstrap.ts`. - // https://github.com/vercel/next.js/blob/08d7a7e5189a835f5dcb82af026174e587575c0e/packages/next/src/client/page-bootstrap.ts#L69-L71 - return { event: HMR_ACTIONS_SENT_TO_BROWSER.CLIENT_CHANGES } - }, - () => { - return { - action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, - data: '_app has changed (error route)', - } + hooks.handleWrittenEndpoint(key, writtenEndpoint) + hooks.subscribeToChanges( + key, + false, + entrypoints.global.app, + () => { + // There's a special case for this in `../client/page-bootstrap.ts`. + // https://github.com/vercel/next.js/blob/08d7a7e5189a835f5dcb82af026174e587575c0e/packages/next/src/client/page-bootstrap.ts#L69-L71 + return { event: HMR_ACTIONS_SENT_TO_BROWSER.CLIENT_CHANGES } + }, + () => { + return { + action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, + data: '_app has changed (error route)', } - ) - } + } + ) processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) } await manifestLoader.loadBuildManifest('_app') @@ -1103,26 +862,24 @@ export async function handlePagesErrorRoute({ const key = getEntryKey('pages', 'server', '_document') const writtenEndpoint = await entrypoints.global.document.writeToDisk() - hooks?.handleWrittenEndpoint(key, writtenEndpoint) - if (dev) { - hooks?.subscribeToChanges( - key, - false, - entrypoints.global.document, - () => { - return { - action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, - data: '_document has changed (error route)', - } - }, - (e) => { - return { - action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, - data: `error in _document subscription (error route): ${e}`, - } + hooks.handleWrittenEndpoint(key, writtenEndpoint) + hooks.subscribeToChanges( + key, + false, + entrypoints.global.document, + () => { + return { + action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, + data: '_document has changed (error route)', } - ) - } + }, + (e) => { + return { + action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, + data: `error in _document subscription (error route): ${e}`, + } + } + ) processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) } await manifestLoader.loadPagesManifest('_document') @@ -1131,25 +888,23 @@ export async function handlePagesErrorRoute({ const key = getEntryKey('pages', 'server', '_error') const writtenEndpoint = await entrypoints.global.error.writeToDisk() - hooks?.handleWrittenEndpoint(key, writtenEndpoint) - if (dev) { - hooks?.subscribeToChanges( - key, - false, - entrypoints.global.error, - () => { - // There's a special case for this in `../client/page-bootstrap.ts`. - // https://github.com/vercel/next.js/blob/08d7a7e5189a835f5dcb82af026174e587575c0e/packages/next/src/client/page-bootstrap.ts#L69-L71 - return { event: HMR_ACTIONS_SENT_TO_BROWSER.CLIENT_CHANGES } - }, - (e) => { - return { - action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, - data: `error in _error subscription: ${e}`, - } + hooks.handleWrittenEndpoint(key, writtenEndpoint) + hooks.subscribeToChanges( + key, + false, + entrypoints.global.error, + () => { + // There's a special case for this in `../client/page-bootstrap.ts`. + // https://github.com/vercel/next.js/blob/08d7a7e5189a835f5dcb82af026174e587575c0e/packages/next/src/client/page-bootstrap.ts#L69-L71 + return { event: HMR_ACTIONS_SENT_TO_BROWSER.CLIENT_CHANGES } + }, + (e) => { + return { + action: HMR_ACTIONS_SENT_TO_BROWSER.RELOAD_PAGE, + data: `error in _error subscription: ${e}`, } - ) - } + } + ) processIssues(currentEntryIssues, key, writtenEndpoint, false, logErrors) } await manifestLoader.loadBuildManifest('_error') @@ -1202,9 +957,3 @@ export function normalizedPageToTurbopackStructureRoute( } return entrypointKey } - -export function isPersistentCachingEnabled( - config: NextConfigComplete -): boolean { - return config.experimental.turbo?.unstablePersistentCaching || false -} diff --git a/packages/next/src/server/dev/turbopack/types.ts b/packages/next/src/server/dev/turbopack/types.ts deleted file mode 100644 index 31b26fa383157f..00000000000000 --- a/packages/next/src/server/dev/turbopack/types.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { - Endpoint, - Instrumentation, - Middleware, -} from '../../../build/swc/types' - -export interface GlobalEntrypoints { - app: Endpoint | undefined - document: Endpoint | undefined - error: Endpoint | undefined - - middleware: Middleware | undefined - instrumentation: Instrumentation | undefined -} - -export type PageRoute = - | { - type: 'page' - htmlEndpoint: Endpoint - dataEndpoint: Endpoint - } - | { - type: 'page-api' - endpoint: Endpoint - } - -export type AppRoute = - | { - type: 'app-page' - htmlEndpoint: Endpoint - rscEndpoint: Endpoint - } - | { - type: 'app-route' - endpoint: Endpoint - } - -// pathname -> route -export type PageEntrypoints = Map - -// originalName / page -> route -export type AppEntrypoints = Map - -export type Entrypoints = { - global: GlobalEntrypoints - - page: PageEntrypoints - app: AppEntrypoints -} diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index cba059010c8fd4..9ad83a12dba388 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -66,15 +66,15 @@ import { HMR_ACTIONS_SENT_TO_BROWSER } from '../../dev/hot-reloader-types' import { PAGE_TYPES } from '../../../lib/page-types' import { createHotReloaderTurbopack } from '../../dev/hot-reloader-turbopack' import { generateEncryptionKeyBase64 } from '../../app-render/encryption-utils-server' -import { - ModuleBuildError, - TurbopackInternalError, -} from '../../dev/turbopack-utils' import { isMetadataRouteFile } from '../../../lib/metadata/is-metadata-route' import { normalizeMetadataPageToRoute } from '../../../lib/metadata/get-metadata-route' import { createEnvDefinitions } from '../experimental/create-env-definitions' import { JsConfigPathsPlugin } from '../../../build/webpack/plugins/jsconfig-paths-plugin' import { store as consoleStore } from '../../../build/output/store' +import { + ModuleBuildError, + TurbopackInternalError, +} from '../../../shared/lib/turbopack/utils' export type SetupOpts = { renderServer: LazyRenderServerInstance diff --git a/packages/next/src/server/dev/turbopack/entry-key.ts b/packages/next/src/shared/lib/turbopack/entry-key.ts similarity index 100% rename from packages/next/src/server/dev/turbopack/entry-key.ts rename to packages/next/src/shared/lib/turbopack/entry-key.ts diff --git a/packages/next/src/server/dev/turbopack/manifest-loader.ts b/packages/next/src/shared/lib/turbopack/manifest-loader.ts similarity index 97% rename from packages/next/src/server/dev/turbopack/manifest-loader.ts rename to packages/next/src/shared/lib/turbopack/manifest-loader.ts index 294f66139adcde..cd62e5b53f8848 100644 --- a/packages/next/src/server/dev/turbopack/manifest-loader.ts +++ b/packages/next/src/shared/lib/turbopack/manifest-loader.ts @@ -3,14 +3,14 @@ import type { MiddlewareManifest, } from '../../../build/webpack/plugins/middleware-plugin' import type { StatsAsset, StatsChunk, StatsChunkGroup, StatsModule, StatsCompilation as WebpackStats } from 'webpack' -import type { BuildManifest } from '../../get-page-files' +import type { BuildManifest } from '../../../server/get-page-files' import type { AppBuildManifest } from '../../../build/webpack/plugins/app-build-manifest-plugin' import type { PagesManifest } from '../../../build/webpack/plugins/pages-manifest-plugin' import { pathToRegexp } from 'next/dist/compiled/path-to-regexp' import type { ActionManifest } from '../../../build/webpack/plugins/flight-client-entry-plugin' import type { NextFontManifest } from '../../../build/webpack/plugins/next-font-manifest-plugin' import type { - REACT_LOADABLE_MANIFEST} from '../../../shared/lib/constants'; + REACT_LOADABLE_MANIFEST} from '../constants'; import { APP_BUILD_MANIFEST, APP_PATHS_MANIFEST, @@ -23,11 +23,11 @@ import { SERVER_REFERENCE_MANIFEST, TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST, WEBPACK_STATS, -} from '../../../shared/lib/constants' +} from '../constants' import { join, posix } from 'path' import { readFile } from 'fs/promises' -import type { SetupOpts } from '../../lib/router-utils/setup-dev-bundler' -import { deleteCache } from '../require-cache' +import type { SetupOpts } from '../../../server/lib/router-utils/setup-dev-bundler' +import { deleteCache } from '../../../server/dev/require-cache' import { writeFileAtomic } from '../../../lib/fs/write-atomic' import { isInterceptionRouteRewrite } from '../../../lib/generate-interception-routes-rewrites' import { @@ -36,14 +36,14 @@ import { srcEmptySsgManifest, processRoute, } from '../../../build/webpack/plugins/build-manifest-plugin' -import type { Entrypoints } from './types' -import getAssetPathFromRoute from '../../../shared/lib/router/utils/get-asset-path-from-route' +import getAssetPathFromRoute from '../router/utils/get-asset-path-from-route' import { getEntryKey, type EntryKey } from './entry-key' import type { CustomRoutes } from '../../../lib/load-custom-routes' -import { getSortedRoutes } from '../../../shared/lib/router/utils' +import { getSortedRoutes } from '../router/utils' import { existsSync } from 'fs' -import { addMetadataIdToRoute, addRouteSuffix, removeRouteSuffix } from '../turbopack-utils' +import { addMetadataIdToRoute, addRouteSuffix, removeRouteSuffix } from '../../../server/dev/turbopack-utils' import { tryToParsePath } from '../../../lib/try-to-parse-path' +import type { Entrypoints } from '../../../build/swc/types' interface InstrumentationDefinition { files: string[] diff --git a/packages/next/src/shared/lib/turbopack/utils.ts b/packages/next/src/shared/lib/turbopack/utils.ts new file mode 100644 index 00000000000000..e200f4a5d4d556 --- /dev/null +++ b/packages/next/src/shared/lib/turbopack/utils.ts @@ -0,0 +1,241 @@ +import type { Issue, StyledString, TurbopackResult } from '../../../build/swc/types' +import { bold, green, magenta, red } from '../../../lib/picocolors' +import isInternal from '../is-internal' +import { decodeMagicIdentifier, MAGIC_IDENTIFIER_REGEX } from '../magic-identifier' +import type { EntryKey } from './entry-key' +import * as Log from '../../../build/output/log' +import type { NextConfigComplete } from '../../../server/config-shared' +import loadJsConfig from '../../../build/load-jsconfig' + +type IssueKey = `${Issue['severity']}-${Issue['filePath']}-${string}-${string}` +export type IssuesMap = Map +export type EntryIssuesMap = Map +export type TopLevelIssuesMap = IssuesMap + +// An error generated from emitted Turbopack issues. This can include build +// errors caused by issues with user code. +export class ModuleBuildError extends Error { + name = 'ModuleBuildError' +} + +// An error caused by an internal issue in Turbopack. These should be written +// to a log file and details should not be shown to the user. +export class TurbopackInternalError extends Error { + name = 'TurbopackInternalError' + + constructor(cause: Error) { + super(cause.message) + this.stack = cause.stack + } +} + +/** + * Thin stopgap workaround layer to mimic existing wellknown-errors-plugin in webpack's build + * to emit certain type of errors into cli. + */ +export function isWellKnownError(issue: Issue): boolean { + const { title } = issue + const formattedTitle = renderStyledStringToErrorAnsi(title) + // TODO: add more well known errors + if ( + formattedTitle.includes('Module not found') || + formattedTitle.includes('Unknown module type') + ) { + return true + } + + return false +} + +export function getIssueKey(issue: Issue): IssueKey { + return `${issue.severity}-${issue.filePath}-${JSON.stringify( + issue.title + )}-${JSON.stringify(issue.description)}` +} + +export async function getTurbopackJsConfig( + dir: string, + nextConfig: NextConfigComplete +) { + const { jsConfig } = await loadJsConfig(dir, nextConfig) + return jsConfig ?? { compilerOptions: {} } +} + + +export function processIssues( + currentEntryIssues: EntryIssuesMap, + key: EntryKey, + result: TurbopackResult, + throwIssue: boolean, + logErrors: boolean +) { + const newIssues = new Map() + currentEntryIssues.set(key, newIssues) + + const relevantIssues = new Set() + + for (const issue of result.issues) { + if ( + issue.severity !== 'error' && + issue.severity !== 'fatal' && + issue.severity !== 'warning' + ) + continue + + const issueKey = getIssueKey(issue) + newIssues.set(issueKey, issue) + + if (issue.severity !== 'warning') { + if (throwIssue) { + const formatted = formatIssue(issue) + relevantIssues.add(formatted) + } + // if we throw the issue it will most likely get handed and logged elsewhere + else if (logErrors && isWellKnownError(issue)) { + const formatted = formatIssue(issue) + Log.error(formatted) + } + } + } + + if (relevantIssues.size && throwIssue) { + throw new ModuleBuildError([...relevantIssues].join('\n\n')) + } +} + +export function formatIssue(issue: Issue) { + const { filePath, title, description, source } = issue + let { documentationLink } = issue + let formattedTitle = renderStyledStringToErrorAnsi(title).replace( + /\n/g, + '\n ' + ) + + // TODO: Use error codes to identify these + // TODO: Generalize adapting Turbopack errors to Next.js errors + if (formattedTitle.includes('Module not found')) { + // For compatiblity with webpack + // TODO: include columns in webpack errors. + documentationLink = 'https://nextjs.org/docs/messages/module-not-found' + } + + let formattedFilePath = filePath + .replace('[project]/', './') + .replaceAll('/./', '/') + .replace('\\\\?\\', '') + + let message = '' + + if (source && source.range) { + const { start } = source.range + message = `${formattedFilePath}:${start.line + 1}:${ + start.column + 1 + }\n${formattedTitle}` + } else if (formattedFilePath) { + message = `${formattedFilePath}\n${formattedTitle}` + } else { + message = formattedTitle + } + message += '\n' + + if ( + source?.range && + source.source.content && + // ignore Next.js/React internals, as these can often be huge bundled files. + !isInternal(filePath) + ) { + const { start, end } = source.range + const { codeFrameColumns } = require('next/dist/compiled/babel/code-frame') + + message += + codeFrameColumns( + source.source.content, + { + start: { + line: start.line + 1, + column: start.column + 1, + }, + end: { + line: end.line + 1, + column: end.column + 1, + }, + }, + { forceColor: true } + ).trim() + '\n\n' + } + + if (description) { + message += renderStyledStringToErrorAnsi(description) + '\n\n' + } + + // TODO: make it possible to enable this for debugging, but not in tests. + // if (detail) { + // message += renderStyledStringToErrorAnsi(detail) + '\n\n' + // } + + // TODO: Include a trace from the issue. + + if (documentationLink) { + message += documentationLink + '\n\n' + } + + return message +} + +export function isRelevantWarning(issue: Issue): boolean { + return issue.severity === 'warning' && !isNodeModulesIssue(issue) +} + +function isNodeModulesIssue(issue: Issue): boolean { + if (issue.severity === 'warning' && issue.stage === 'config') { + // Override for the externalize issue + // `Package foo (serverExternalPackages or default list) can't be external` + if ( + renderStyledStringToErrorAnsi(issue.title).includes("can't be external") + ) { + return false + } + } + + return ( + issue.severity === 'warning' && + (issue.filePath.match(/^(?:.*[\\/])?node_modules(?:[\\/].*)?$/) !== null || + // Ignore Next.js itself when running next directly in the monorepo where it is not inside + // node_modules anyway. + // TODO(mischnic) prevent matches when this is published to npm + issue.filePath.startsWith('[project]/packages/next/')) + ) +} + +export function renderStyledStringToErrorAnsi(string: StyledString): string { + function decodeMagicIdentifiers(str: string): string { + return str.replaceAll(MAGIC_IDENTIFIER_REGEX, (ident) => { + try { + return magenta(`{${decodeMagicIdentifier(ident)}}`) + } catch (e) { + return magenta(`{${ident} (decoding failed: ${e})}`) + } + }) + } + + switch (string.type) { + case 'text': + return decodeMagicIdentifiers(string.value) + case 'strong': + return bold(red(decodeMagicIdentifiers(string.value))) + case 'code': + return green(decodeMagicIdentifiers(string.value)) + case 'line': + return string.value.map(renderStyledStringToErrorAnsi).join('') + case 'stack': + return string.value.map(renderStyledStringToErrorAnsi).join('\n') + default: + throw new Error('Unknown StyledString type', string) + } +} + +export function isPersistentCachingEnabled( + config: NextConfigComplete +): boolean { + return config.experimental.turbo?.unstablePersistentCaching || false +} diff --git a/test/development/basic/next-rs-api.test.ts b/test/development/basic/next-rs-api.test.ts index f97ab5866bbfcc..e295487210668e 100644 --- a/test/development/basic/next-rs-api.test.ts +++ b/test/development/basic/next-rs-api.test.ts @@ -4,9 +4,9 @@ import { PHASE_DEVELOPMENT_SERVER } from 'next/constants' import { createDefineEnv, loadBindings } from 'next/dist/build/swc' import type { Diagnostics, - Entrypoints, Issue, Project, + RawEntrypoints, StyledString, TurbopackResult, UpdateInfo, @@ -329,7 +329,7 @@ describe('next.rs api', () => { // eslint-disable-next-line no-loop-func it(`should allow to write ${name} to disk`, async () => { const entrypointsSubscribtion = project.entrypointsSubscribe() - const entrypoints: TurbopackResult = ( + const entrypoints: TurbopackResult = ( await entrypointsSubscribtion.next() ).value const route = entrypoints.routes.get(path) @@ -465,7 +465,7 @@ describe('next.rs api', () => { console.log('start') await new Promise((r) => setTimeout(r, 1000)) const entrypointsSubscribtion = project.entrypointsSubscribe() - const entrypoints: TurbopackResult = ( + const entrypoints: TurbopackResult = ( await entrypointsSubscribtion.next() ).value const route = entrypoints.routes.get(path) @@ -608,7 +608,7 @@ describe('next.rs api', () => { console.log('start') await new Promise((r) => setTimeout(r, 1000)) const entrypointsSubscribtion = project.entrypointsSubscribe() - const entrypoints: TurbopackResult = ( + const entrypoints: TurbopackResult = ( await entrypointsSubscribtion.next() ).value const route = entrypoints.routes.get('/') From 4f523905f05b8fc1b8c34cbe517854c346beef33 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 23 Jan 2025 09:33:49 -0500 Subject: [PATCH 09/61] [Segment Cache] Prioritize route trees over segments (#75213) When processing the prefetch queue, we should prioritize fetching the route trees for all the links before we fetch any of the segments. The reason the route tree is more important is because it tells us the structure of the target page; from this alone, we can determine which segments are already cached and skip them during the navigation. Whereas if the route tree is missing at the time of the navigation, we cannot instruct the server to skip over prefetched segments, because we don't know which ones to skip. To implement this, I added a `phase` property to the PrefetchTask type. This is essentially a secondary priority field that changes as the task progresses. There are two phases: `RouteTree` and `Segments`. Any task in the `RouteTree` phase has higher priority than tasks in the `Segments` phase. Another way to implement this would be to add a secondary queue for segment prefetches. This would require reference counting to clean up segments when there are no more tasks that depend on them. The ref count could also be used as an additional prioritization heuristic: shared layouts with many dependent tasks could be prioritized over segments with fewer. However, I'm not sure the benefits are worth the extra code/complexity, so I'm starting with this simpler approach. --- .../components/segment-cache/scheduler.ts | 41 ++++++++++++++++-- .../app/cancellation/[pageNumber]/page.tsx | 15 ++++++- .../prefetch-scheduling.test.ts | 42 +++++++++++++++++++ 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/packages/next/src/client/components/segment-cache/scheduler.ts b/packages/next/src/client/components/segment-cache/scheduler.ts index 1cbaf83844875c..aca66538c4722d 100644 --- a/packages/next/src/client/components/segment-cache/scheduler.ts +++ b/packages/next/src/client/components/segment-cache/scheduler.ts @@ -83,6 +83,14 @@ export type PrefetchTask = { */ priority: PrefetchPriority + /** + * The phase of the task. Tasks are split into multiple phases so that their + * priority can be adjusted based on what kind of work they're doing. + * Concretely, prefetching the route tree is higher priority than prefetching + * segment data. + */ + phase: PrefetchPhase + /** * Temporary state for tracking the currently running task. This is currently * used to track whether a task deferred some work to run background at @@ -149,6 +157,16 @@ export const enum PrefetchPriority { Background = 0, } +/** + * Prefetch tasks are processed in two phases: first the route tree is fetched, + * then the segments. We use this to priortize tasks that have not yet fetched + * the route tree. + */ +const enum PrefetchPhase { + RouteTree = 1, + Segments = 0, +} + export type PrefetchSubtaskResult = { /** * A promise that resolves when the network connection is closed. @@ -190,6 +208,7 @@ export function schedulePrefetchTask( key, treeAtTimeOfPrefetch, priority, + phase: PrefetchPhase.RouteTree, hasBackgroundWork: false, includeDynamicData, sortId: sortIdCounter++, @@ -359,7 +378,12 @@ function processQueueInMicrotask() { task = heapPeek(taskHeap) continue case PrefetchTaskExitStatus.Done: - if (hasBackgroundWork) { + if (task.phase === PrefetchPhase.RouteTree) { + // Finished prefetching the route tree. Proceed to prefetching + // the segments. + task.phase = PrefetchPhase.Segments + heapResift(taskHeap, task) + } else if (hasBackgroundWork) { // The task spawned additional background work. Reschedule the task // at background priority. task.priority = PrefetchPriority.Background @@ -446,6 +470,10 @@ function pingRootRouteTree( return PrefetchTaskExitStatus.Done } case EntryStatus.Fulfilled: { + if (task.phase !== PrefetchPhase.Segments) { + // Do not prefetch segment data until we've entered the segment phase. + return PrefetchTaskExitStatus.Done + } // Recursively fill in the segment tree. if (!hasNetworkBandwidth()) { // Stop prefetching segments until there's more bandwidth. @@ -1058,8 +1086,15 @@ function compareQueuePriority(a: PrefetchTask, b: PrefetchTask) { return priorityDiff } - // sortId is an incrementing counter assigned to prefetches. We want to - // process the newest prefetches first. + // If the priority is the same, check which phase the prefetch is in — is it + // prefetching the route tree, or the segments? Route trees are prioritized. + const phaseDiff = b.phase - a.phase + if (phaseDiff !== 0) { + return phaseDiff + } + + // Finally, check the insertion order. `sortId` is an incrementing counter + // assigned to prefetches. We want to process the newest prefetches first. return b.sortId - a.sortId } diff --git a/test/e2e/app-dir/segment-cache/prefetch-scheduling/app/cancellation/[pageNumber]/page.tsx b/test/e2e/app-dir/segment-cache/prefetch-scheduling/app/cancellation/[pageNumber]/page.tsx index 32653a296aec4e..bba8062e1dc1a7 100644 --- a/test/e2e/app-dir/segment-cache/prefetch-scheduling/app/cancellation/[pageNumber]/page.tsx +++ b/test/e2e/app-dir/segment-cache/prefetch-scheduling/app/cancellation/[pageNumber]/page.tsx @@ -4,6 +4,19 @@ type Params = { pageNumber: string } +export async function generateViewport({ + params, +}: { + params: Promise +}) { + const { pageNumber } = await params + return { + // Put the page number into the media query. This is just a trick to allow + // the test to detect when the viewport for this page has been prefetched. + themeColor: [{ media: `(min-width: ${pageNumber}px)`, color: 'light' }], + } +} + async function Content({ params }: { params: Promise }) { const { pageNumber } = await params return 'Content of page ' + pageNumber @@ -23,7 +36,7 @@ export default async function LinkCancellationTargetPage({ export async function generateStaticParams(): Promise> { const result: Array = [] - for (let n = 1; n <= 1; n++) { + for (let n = 1; n <= 10; n++) { result.push({ pageNumber: n.toString() }) } return result diff --git a/test/e2e/app-dir/segment-cache/prefetch-scheduling/prefetch-scheduling.test.ts b/test/e2e/app-dir/segment-cache/prefetch-scheduling/prefetch-scheduling.test.ts index 7fbf4dfa7698d5..ed1b07ac11b70e 100644 --- a/test/e2e/app-dir/segment-cache/prefetch-scheduling/prefetch-scheduling.test.ts +++ b/test/e2e/app-dir/segment-cache/prefetch-scheduling/prefetch-scheduling.test.ts @@ -52,6 +52,48 @@ describe('segment cache prefetch scheduling', () => { ) }) + it('prioritizes prefetching the route trees before the segments', async () => { + let act: ReturnType + const browser = await next.browser('/cancellation', { + beforePageLoad(p: Playwright.Page) { + act = createRouterAct(p) + }, + }) + + const checkbox = await browser.elementByCss('input[type="checkbox"]') + + await act(async () => { + // Reveal the links to start prefetching + await checkbox.click() + }, [ + // Assert on the order that the prefetches requests are + // initiated. We don't need to assert on every single prefetch response; + // this will only check the order of the ones that we've listed. + // + // To detect when the route tree is prefetched, we check for a string + // that is known to be present in the target page's viewport config + // (which is included in the route tree response). In this test app, the + // page number is used in the media query of the theme color. E.g. for + // page 1, the viewport includes: + // + // + + // First we should prefetch all the route trees: + { includes: '(min-width: 7px)' }, + { includes: '(min-width: 6px)' }, + { includes: '(min-width: 5px)' }, + { includes: '(min-width: 4px)' }, + { includes: '(min-width: 3px)' }, + + // Then we should prefetch the segments: + { includes: 'Content of page 7' }, + { includes: 'Content of page 6' }, + { includes: 'Content of page 5' }, + { includes: 'Content of page 4' }, + { includes: 'Content of page 3' }, + ]) + }) + it( 'even on mouseexit, any link that was previously hovered is prioritized ' + 'over links that were never hovered at all', From 9cc6e96ed561ad52c45602bbb5a84ab4b2db6a5f Mon Sep 17 00:00:00 2001 From: Lee Robinson Date: Thu, 23 Jan 2025 09:06:59 -0600 Subject: [PATCH 10/61] docs: Update `robots` for `generateMetadata` spec (#75229) Typically you would want things to be indexed, so flipping the defaults shown. --- .../04-functions/generate-metadata.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/01-app/04-api-reference/04-functions/generate-metadata.mdx b/docs/01-app/04-api-reference/04-functions/generate-metadata.mdx index f3f52baee15ef5..289f4893b329ee 100644 --- a/docs/01-app/04-api-reference/04-functions/generate-metadata.mdx +++ b/docs/01-app/04-api-reference/04-functions/generate-metadata.mdx @@ -566,13 +566,13 @@ import type { Metadata } from 'next' export const metadata: Metadata = { robots: { - index: false, + index: true, follow: true, - nocache: true, + nocache: false, googleBot: { index: true, - follow: false, - noimageindex: true, + follow: true, + noimageindex: false, 'max-video-preview': -1, 'max-image-preview': 'large', 'max-snippet': -1, @@ -582,10 +582,10 @@ export const metadata: Metadata = { ``` ```html filename=" output" hideLineNumbers - + ``` From b88a45969c7fc4dcf97097c48ba285f667397cd0 Mon Sep 17 00:00:00 2001 From: vercel-release-bot Date: Thu, 23 Jan 2025 15:42:09 +0000 Subject: [PATCH 11/61] v15.2.0-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-refresh-utils/package.json | 2 +- packages/third-parties/package.json | 4 ++-- pnpm-lock.yaml | 16 ++++++++-------- 17 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lerna.json b/lerna.json index 2aa5efee6e3b13..8d8d3b73d31615 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "15.2.0-canary.21" + "version": "15.2.0-canary.22" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 53013047ed5ec7..8b25f22d72e50c 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "15.2.0-canary.21", + "version": "15.2.0-canary.22", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 8c55583a59d3d4..dc069deee9b164 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "15.2.0-canary.21", + "version": "15.2.0-canary.22", "description": "ESLint configuration used by Next.js.", "main": "index.js", "license": "MIT", @@ -10,7 +10,7 @@ }, "homepage": "https://nextjs.org/docs/app/api-reference/config/eslint", "dependencies": { - "@next/eslint-plugin-next": "15.2.0-canary.21", + "@next/eslint-plugin-next": "15.2.0-canary.22", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 76e137b9b7aa30..fd2345ac911d6e 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": "15.2.0-canary.21", + "version": "15.2.0-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 833edeec0a78aa..dee0d9b9d0200c 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "15.2.0-canary.21", + "version": "15.2.0-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 40fe0269cf190f..9977a7a2c521f3 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "15.2.0-canary.21", + "version": "15.2.0-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 0c5b23ee6d1040..b1e727d60cc32b 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "15.2.0-canary.21", + "version": "15.2.0-canary.22", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 7b389f3704650e..2dcc98f563bee4 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "15.2.0-canary.21", + "version": "15.2.0-canary.22", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 4589df43d61a49..0cbe52ff74153d 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "15.2.0-canary.21", + "version": "15.2.0-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 f07107796fd4c1..0172320cc455cb 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "15.2.0-canary.21", + "version": "15.2.0-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 00adef22a39690..2b1cb72c1d603f 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "15.2.0-canary.21", + "version": "15.2.0-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 30c427e0510300..25d25d2278b918 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "15.2.0-canary.21", + "version": "15.2.0-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 f440e5e77204a1..3dbd74fd54e6b3 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "15.2.0-canary.21", + "version": "15.2.0-canary.22", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", diff --git a/packages/next/package.json b/packages/next/package.json index ce410f58204ccc..54e9142f7db01d 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "15.2.0-canary.21", + "version": "15.2.0-canary.22", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -99,7 +99,7 @@ ] }, "dependencies": { - "@next/env": "15.2.0-canary.21", + "@next/env": "15.2.0-canary.22", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -164,11 +164,11 @@ "@jest/types": "29.5.0", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/font": "15.2.0-canary.21", - "@next/polyfill-module": "15.2.0-canary.21", - "@next/polyfill-nomodule": "15.2.0-canary.21", - "@next/react-refresh-utils": "15.2.0-canary.21", - "@next/swc": "15.2.0-canary.21", + "@next/font": "15.2.0-canary.22", + "@next/polyfill-module": "15.2.0-canary.22", + "@next/polyfill-nomodule": "15.2.0-canary.22", + "@next/react-refresh-utils": "15.2.0-canary.22", + "@next/swc": "15.2.0-canary.22", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.41.2", "@storybook/addon-essentials": "^8.4.7", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 0f90faec63f3ca..6ac76533fb4d6a 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": "15.2.0-canary.21", + "version": "15.2.0-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 98c78e8f24400a..49d1a7f993b3ff 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "15.2.0-canary.21", + "version": "15.2.0-canary.22", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "15.2.0-canary.21", + "next": "15.2.0-canary.22", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "5.7.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7522189e22a6e6..6a7acce6cc6962 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -793,7 +793,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 15.2.0-canary.21 + specifier: 15.2.0-canary.22 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.10.3 @@ -857,7 +857,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 15.2.0-canary.21 + specifier: 15.2.0-canary.22 version: link:../next-env '@swc/counter': specifier: 0.1.3 @@ -985,19 +985,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 15.2.0-canary.21 + specifier: 15.2.0-canary.22 version: link:../font '@next/polyfill-module': - specifier: 15.2.0-canary.21 + specifier: 15.2.0-canary.22 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 15.2.0-canary.21 + specifier: 15.2.0-canary.22 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 15.2.0-canary.21 + specifier: 15.2.0-canary.22 version: link:../react-refresh-utils '@next/swc': - specifier: 15.2.0-canary.21 + specifier: 15.2.0-canary.22 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1661,7 +1661,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 15.2.0-canary.21 + specifier: 15.2.0-canary.22 version: link:../next outdent: specifier: 0.8.0 From dae2f036e028aa83679edfc5158aaffef5f9f737 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Thu, 23 Jan 2025 08:38:21 -0800 Subject: [PATCH 12/61] refactor(turbo-tasks): Use Return Position Impl Traits (RPIT) to reimplement GraphTraversal (#74896) Reimplements `GraphTraversal` without the manual `Future` implementation. This was [part of an experiment to try to use RPIT in the `Visit` trait](https://vercel.slack.com/archives/C02MK5YRKNZ/p1736864833846679), but I didn't quite get that far. This code likely didn't use RPIT when it was written because RPIT on traits wasn't stable (or even fully implemented in rustc?) at that time. This removes some unsafe code and a use of the unstable nightly-only `impl_trait_in_assoc_type` feature. --- .../turbo-tasks/src/graph/adjacency_map.rs | 2 +- .../turbo-tasks/src/graph/graph_store.rs | 6 +- .../turbo-tasks/src/graph/graph_traversal.rs | 217 +++++------------- .../src/graph/non_deterministic.rs | 5 +- .../crates/turbo-tasks/src/graph/visit.rs | 15 +- turbopack/crates/turbo-tasks/src/lib.rs | 2 - 6 files changed, 81 insertions(+), 166 deletions(-) diff --git a/turbopack/crates/turbo-tasks/src/graph/adjacency_map.rs b/turbopack/crates/turbo-tasks/src/graph/adjacency_map.rs index 1e3d8bb4d54506..5acd4358e903da 100644 --- a/turbopack/crates/turbo-tasks/src/graph/adjacency_map.rs +++ b/turbopack/crates/turbo-tasks/src/graph/adjacency_map.rs @@ -68,7 +68,7 @@ where impl GraphStore for AdjacencyMap where - T: Eq + Hash + Clone, + T: Eq + Hash + Clone + Send, { type Node = T; type Handle = T; diff --git a/turbopack/crates/turbo-tasks/src/graph/graph_store.rs b/turbopack/crates/turbo-tasks/src/graph/graph_store.rs index 8ae17b76996ac0..abdec7122a165b 100644 --- a/turbopack/crates/turbo-tasks/src/graph/graph_store.rs +++ b/turbopack/crates/turbo-tasks/src/graph/graph_store.rs @@ -4,9 +4,9 @@ use super::VisitedNodes; /// A graph store is a data structure that will be built up during a graph /// traversal. It is used to store the results of the traversal. -pub trait GraphStore { - type Node; - type Handle: Clone; +pub trait GraphStore: Send { + type Node: Send; + type Handle: Clone + Send; // TODO(alexkirsz) An `entry(from_handle) -> Entry` API would be more // efficient, as right now we're getting the same key multiple times. diff --git a/turbopack/crates/turbo-tasks/src/graph/graph_traversal.rs b/turbopack/crates/turbo-tasks/src/graph/graph_traversal.rs index 3fabff021eb9af..42b560f455c959 100644 --- a/turbopack/crates/turbo-tasks/src/graph/graph_traversal.rs +++ b/turbopack/crates/turbo-tasks/src/graph/graph_traversal.rs @@ -1,7 +1,7 @@ -use std::{collections::HashSet, future::Future, pin::Pin}; +use std::{collections::HashSet, future::Future}; use anyhow::Result; -use futures::{stream::FuturesUnordered, Stream}; +use futures::{stream::FuturesUnordered, StreamExt}; use super::{ graph_store::{GraphNode, GraphStore}, @@ -19,14 +19,15 @@ pub struct VisitedNodes(pub HashSet); /// The traversal is done in parallel, and the order of the nodes in the traversal /// result is determined by the [`GraphStore`] parameter. pub trait GraphTraversal: GraphStore + Sized { - fn visit( + fn visit( self, - root_edges: RootEdgesIt, + root_edges: impl IntoIterator, visit: VisitImpl, - ) -> GraphTraversalFuture + ) -> impl Future, Abort>> + Send where - VisitImpl: Visit, - RootEdgesIt: IntoIterator; + VisitImpl: Visit + Send, + Abort: Send, + Impl: Send; fn skip_duplicates(self) -> SkipDuplicates; fn skip_duplicates_with_visited_nodes( @@ -41,16 +42,22 @@ where { /// Visits the graph starting from the given `roots`, and returns a future /// that will resolve to the traversal result. - fn visit( + fn visit( mut self, - root_edges: RootEdgesIt, + root_edges: impl IntoIterator, mut visit: VisitImpl, - ) -> GraphTraversalFuture + ) -> impl Future, Abort>> + Send where - VisitImpl: Visit, - RootEdgesIt: IntoIterator, + VisitImpl: Visit + Send, + Abort: Send, + Impl: Send, { - let futures = FuturesUnordered::new(); + let mut futures = FuturesUnordered::new(); + let mut root_abort = None; + + // Populate `futures` with all the roots, `root_edges` isn't required to be `Send`, so this + // has to happen outside of the future. We could require `root_edges` to be `Send` in the + // future. for edge in root_edges { match visit.visit(edge) { VisitControlFlow::Continue(node) => { @@ -63,19 +70,52 @@ where self.insert(None, GraphNode(node)); } VisitControlFlow::Abort(abort) => { - return GraphTraversalFuture { - state: GraphTraversalState::Aborted { abort }, - }; + // this must be returned inside the `async` block below so that it's part of the + // returned future + root_abort = Some(abort) } } } - GraphTraversalFuture { - state: GraphTraversalState::Running(GraphTraversalRunningState { - store: self, - futures, - visit, - _phantom: std::marker::PhantomData, - }), + + async move { + if let Some(abort) = root_abort { + return GraphTraversalResult::Aborted(abort); + } + loop { + match futures.next().await { + Some((parent_handle, span, Ok(edges))) => { + let _guard = span.enter(); + for edge in edges { + match visit.visit(edge) { + VisitControlFlow::Continue(node) => { + if let Some((node_handle, node_ref)) = + self.insert(Some(parent_handle.clone()), GraphNode(node)) + { + let span = visit.span(node_ref); + futures.push(With::new( + visit.edges(node_ref), + span, + node_handle, + )); + } + } + VisitControlFlow::Skip(node) => { + self.insert(Some(parent_handle.clone()), GraphNode(node)); + } + VisitControlFlow::Abort(abort) => { + return GraphTraversalResult::Aborted(abort) + } + } + } + } + Some((_, _, Err(err))) => { + return GraphTraversalResult::Completed(Err(err)); + } + None => { + return GraphTraversalResult::Completed(Ok(self)); + } + } + } } } @@ -91,47 +131,6 @@ where } } -/// A future that resolves to a [`GraphStore`] containing the result of a graph -/// traversal. -pub struct GraphTraversalFuture -where - Store: GraphStore, - VisitImpl: Visit, - EdgesFuture: Future, -{ - state: GraphTraversalState, -} - -#[derive(Default)] -enum GraphTraversalState -where - Store: GraphStore, - VisitImpl: Visit, - EdgesFuture: Future, -{ - #[default] - Completed, - Aborted { - abort: Abort, - }, - Running(GraphTraversalRunningState), -} - -struct GraphTraversalRunningState -where - Store: GraphStore, - VisitImpl: Visit, - EdgesFuture: Future, -{ - store: Store, - // This should be VisitImpl::EdgesFuture, but this causes a bug in the Rust - // compiler (see https://github.com/rust-lang/rust/issues/102211). - // Instead, we pass the associated type as an additional generic parameter. - futures: FuturesUnordered>, - visit: VisitImpl, - _phantom: std::marker::PhantomData<(Abort, Impl)>, -} - pub enum GraphTraversalResult { Completed(Completed), Aborted(Aborted), @@ -145,93 +144,3 @@ impl GraphTraversalResult { } } } - -impl Future - for GraphTraversalFuture -where - Store: GraphStore, - // The EdgesFuture bound is necessary to avoid the compiler bug mentioned - // above. - VisitImpl: Visit, - EdgesFuture: Future>, -{ - type Output = GraphTraversalResult, Abort>; - - fn poll( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll { - let this = unsafe { self.get_unchecked_mut() }; - - let result; - (this.state, result) = match std::mem::take(&mut this.state) { - GraphTraversalState::Completed => { - panic!("polled after completion") - } - GraphTraversalState::Aborted { abort } => ( - GraphTraversalState::Completed, - std::task::Poll::Ready(GraphTraversalResult::Aborted(abort)), - ), - GraphTraversalState::Running(mut running) => 'outer: loop { - let futures_pin = unsafe { Pin::new_unchecked(&mut running.futures) }; - match futures_pin.poll_next(cx) { - std::task::Poll::Ready(Some((parent_handle, span, Ok(edges)))) => { - let _guard = span.enter(); - for edge in edges { - match running.visit.visit(edge) { - VisitControlFlow::Continue(node) => { - if let Some((node_handle, node_ref)) = running - .store - .insert(Some(parent_handle.clone()), GraphNode(node)) - { - let span = running.visit.span(node_ref); - running.futures.push(With::new( - running.visit.edges(node_ref), - span, - node_handle, - )); - } - } - VisitControlFlow::Skip(node) => { - running - .store - .insert(Some(parent_handle.clone()), GraphNode(node)); - } - VisitControlFlow::Abort(abort) => { - break 'outer ( - GraphTraversalState::Completed, - std::task::Poll::Ready(GraphTraversalResult::Aborted( - abort, - )), - ); - } - } - } - } - std::task::Poll::Ready(Some((_, _, Err(err)))) => { - break ( - GraphTraversalState::Completed, - std::task::Poll::Ready(GraphTraversalResult::Completed(Err(err))), - ); - } - std::task::Poll::Ready(None) => { - break ( - GraphTraversalState::Completed, - std::task::Poll::Ready(GraphTraversalResult::Completed(Ok( - running.store - ))), - ); - } - std::task::Poll::Pending => { - break ( - GraphTraversalState::Running(running), - std::task::Poll::Pending, - ); - } - } - }, - }; - - result - } -} diff --git a/turbopack/crates/turbo-tasks/src/graph/non_deterministic.rs b/turbopack/crates/turbo-tasks/src/graph/non_deterministic.rs index ef64f514176f42..4f51f3ccac3c12 100644 --- a/turbopack/crates/turbo-tasks/src/graph/non_deterministic.rs +++ b/turbopack/crates/turbo-tasks/src/graph/non_deterministic.rs @@ -18,7 +18,10 @@ impl NonDeterministic { } } -impl GraphStore for NonDeterministic { +impl GraphStore for NonDeterministic +where + T: Send, +{ type Node = T; type Handle = (); diff --git a/turbopack/crates/turbo-tasks/src/graph/visit.rs b/turbopack/crates/turbo-tasks/src/graph/visit.rs index d0d0003a709be4..6fcc73c3559c7f 100644 --- a/turbopack/crates/turbo-tasks/src/graph/visit.rs +++ b/turbopack/crates/turbo-tasks/src/graph/visit.rs @@ -10,7 +10,7 @@ use super::VisitControlFlow; pub trait Visit { type Edge; type EdgesIntoIter: IntoIterator; - type EdgesFuture: Future>; + type EdgesFuture: Future> + Send; /// Visits an edge to get to the neighbor node. Should return a /// [`VisitControlFlow`] that indicates whether to: @@ -19,8 +19,13 @@ pub trait Visit { /// * abort the traversal entirely. fn visit(&mut self, edge: Self::Edge) -> VisitControlFlow; - /// Returns a future that resolves to the outgoing edges of the given - /// `node`. + /// Returns a future that resolves to the outgoing edges of the given `node`. + /// + /// Lifetimes: + /// - The returned future's lifetime cannot depend on the reference to self because there are + /// multiple `edges` futures created and awaited concurrently. + /// - The returned future's lifetime cannot depend on `node` because `GraphStore::insert` + /// returns a node reference that's only valid for the lifetime of its `&mut self` reference. fn edges(&mut self, node: &Node) -> Self::EdgesFuture; /// Returns a [Span] for the given `node`, under which all edges are @@ -41,7 +46,7 @@ pub struct ImplRef; impl Visit for VisitFn where VisitFn: FnMut(&Node) -> NeighFut, - NeighFut: Future>, + NeighFut: Future> + Send, NeighIt: IntoIterator, { type Edge = Node; @@ -63,7 +68,7 @@ impl Visit for VisitFn where Node: Clone, VisitFn: FnMut(Node) -> NeighFut, - NeighFut: Future>, + NeighFut: Future> + Send, NeighIt: IntoIterator, { type Edge = Node; diff --git a/turbopack/crates/turbo-tasks/src/lib.rs b/turbopack/crates/turbo-tasks/src/lib.rs index 35952853a7c6ec..ccad66dc437cdc 100644 --- a/turbopack/crates/turbo-tasks/src/lib.rs +++ b/turbopack/crates/turbo-tasks/src/lib.rs @@ -36,9 +36,7 @@ #![feature(arbitrary_self_types)] #![feature(arbitrary_self_types_pointers)] #![feature(new_zeroed_alloc)] -#![feature(type_alias_impl_trait)] #![feature(never_type)] -#![feature(impl_trait_in_assoc_type)] pub mod backend; mod capture_future; From a5d24dc78fbf3d1ce0db90568370cfa6b9718e16 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Thu, 23 Jan 2025 08:38:46 -0800 Subject: [PATCH 13/61] chore(turbo-tasks): Delete `non_local_return` support from `#[turbo_tasks::function]` (#75209) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This code was left over from when we thought that we could perform the `ResolvedVc` migration on a per-function basis by inspecting the return value of the function. *Oh, to be so naïve! The innocence we've lost!* Every `VcValueType` and `VcValueTrait` is a `NonLocalValue` now, so this macro option is no longer useful. --- .../fail_attribute_invalid_args.stderr | 2 +- ...ttribute_invalid_args_inherent_impl.stderr | 2 +- .../function/pass_non_local_inherent_impl.rs | 21 ----------- .../tests/function/pass_non_local_static.rs | 20 ----------- .../function/pass_non_local_trait_impl.rs | 26 -------------- .../crates/turbo-tasks-macros/src/func.rs | 35 ++++--------------- 6 files changed, 8 insertions(+), 98 deletions(-) delete mode 100644 turbopack/crates/turbo-tasks-macros-tests/tests/function/pass_non_local_inherent_impl.rs delete mode 100644 turbopack/crates/turbo-tasks-macros-tests/tests/function/pass_non_local_static.rs delete mode 100644 turbopack/crates/turbo-tasks-macros-tests/tests/function/pass_non_local_trait_impl.rs diff --git a/turbopack/crates/turbo-tasks-macros-tests/tests/function/fail_attribute_invalid_args.stderr b/turbopack/crates/turbo-tasks-macros-tests/tests/function/fail_attribute_invalid_args.stderr index 611cb7bafe374b..32bcaa2595082d 100644 --- a/turbopack/crates/turbo-tasks-macros-tests/tests/function/fail_attribute_invalid_args.stderr +++ b/turbopack/crates/turbo-tasks-macros-tests/tests/function/fail_attribute_invalid_args.stderr @@ -1,4 +1,4 @@ -error: unexpected token, expected one of: "fs", "network", "non_local_return", "operation", or "local_cells" +error: unexpected token, expected one of: "fs", "network", "operation", or "local_cells" --> tests/function/fail_attribute_invalid_args.rs:9:25 | 9 | #[turbo_tasks::function(invalid_argument)] diff --git a/turbopack/crates/turbo-tasks-macros-tests/tests/function/fail_attribute_invalid_args_inherent_impl.stderr b/turbopack/crates/turbo-tasks-macros-tests/tests/function/fail_attribute_invalid_args_inherent_impl.stderr index 30500cc07d4dae..46acba8b7bc688 100644 --- a/turbopack/crates/turbo-tasks-macros-tests/tests/function/fail_attribute_invalid_args_inherent_impl.stderr +++ b/turbopack/crates/turbo-tasks-macros-tests/tests/function/fail_attribute_invalid_args_inherent_impl.stderr @@ -1,4 +1,4 @@ -error: unexpected token, expected one of: "fs", "network", "non_local_return", "operation", or "local_cells" +error: unexpected token, expected one of: "fs", "network", "operation", or "local_cells" --> tests/function/fail_attribute_invalid_args_inherent_impl.rs:14:29 | 14 | #[turbo_tasks::function(invalid_argument)] diff --git a/turbopack/crates/turbo-tasks-macros-tests/tests/function/pass_non_local_inherent_impl.rs b/turbopack/crates/turbo-tasks-macros-tests/tests/function/pass_non_local_inherent_impl.rs deleted file mode 100644 index 99b582171210c7..00000000000000 --- a/turbopack/crates/turbo-tasks-macros-tests/tests/function/pass_non_local_inherent_impl.rs +++ /dev/null @@ -1,21 +0,0 @@ -#![feature(arbitrary_self_types)] -#![feature(arbitrary_self_types_pointers)] -#![allow(dead_code)] - -use turbo_tasks::{ResolvedVc, Vc}; - -#[turbo_tasks::value] -struct ExampleStruct; - -#[turbo_tasks::value(transparent)] -struct IntegersVec(Vec>); - -#[turbo_tasks::value_impl] -impl ExampleStruct { - #[turbo_tasks::function(non_local_return)] - fn return_contains_resolved_vc(self: Vc) -> Vc { - Vc::cell(Vec::new()) - } -} - -fn main() {} diff --git a/turbopack/crates/turbo-tasks-macros-tests/tests/function/pass_non_local_static.rs b/turbopack/crates/turbo-tasks-macros-tests/tests/function/pass_non_local_static.rs deleted file mode 100644 index 01c71fb484788a..00000000000000 --- a/turbopack/crates/turbo-tasks-macros-tests/tests/function/pass_non_local_static.rs +++ /dev/null @@ -1,20 +0,0 @@ -#![feature(arbitrary_self_types)] -#![feature(arbitrary_self_types_pointers)] -#![allow(dead_code)] - -use turbo_tasks::{ResolvedVc, Vc}; - -#[turbo_tasks::value(transparent)] -struct IntegersVec(Vec>); - -#[turbo_tasks::function(non_local_return)] -fn return_contains_resolved_vc() -> Vc { - Vc::cell(Vec::new()) -} - -#[turbo_tasks::function(non_local_return)] -fn return_contains_resolved_vc_result() -> anyhow::Result> { - Ok(Vc::cell(Vec::new())) -} - -fn main() {} diff --git a/turbopack/crates/turbo-tasks-macros-tests/tests/function/pass_non_local_trait_impl.rs b/turbopack/crates/turbo-tasks-macros-tests/tests/function/pass_non_local_trait_impl.rs deleted file mode 100644 index de688467e0a147..00000000000000 --- a/turbopack/crates/turbo-tasks-macros-tests/tests/function/pass_non_local_trait_impl.rs +++ /dev/null @@ -1,26 +0,0 @@ -#![feature(arbitrary_self_types)] -#![feature(arbitrary_self_types_pointers)] -#![allow(dead_code)] - -use turbo_tasks::{ResolvedVc, Vc}; - -#[turbo_tasks::value] -struct ExampleStruct; - -#[turbo_tasks::value(transparent)] -struct IntegersVec(Vec>); - -#[turbo_tasks::value_trait] -trait ExampleTrait { - fn return_contains_resolved_vc(self: Vc) -> Vc; -} - -#[turbo_tasks::value_impl] -impl ExampleTrait for ExampleStruct { - #[turbo_tasks::function(non_local_return)] - fn return_contains_resolved_vc(self: Vc) -> Vc { - Vc::cell(Vec::new()) - } -} - -fn main() {} diff --git a/turbopack/crates/turbo-tasks-macros/src/func.rs b/turbopack/crates/turbo-tasks-macros/src/func.rs index c9d684c11aa675..3578749092d3c9 100644 --- a/turbopack/crates/turbo-tasks-macros/src/func.rs +++ b/turbopack/crates/turbo-tasks-macros/src/func.rs @@ -28,8 +28,6 @@ pub struct TurboFn<'a> { output: Type, this: Option, inputs: Vec, - /// Should we check that the return type contains a `NonLocalValue`? - non_local: Option, /// Should we return `OperationVc` and require that all arguments are `NonLocalValue`s? operation: bool, /// Should this function use `TaskPersistence::LocalCells`? @@ -275,7 +273,6 @@ impl TurboFn<'_> { output, this, inputs, - non_local: args.non_local_return, operation: args.operation.is_some(), local_cells: args.local_cells.is_some(), inline_ident, @@ -514,13 +511,7 @@ impl TurboFn<'_> { } fn get_assertions(&self) -> TokenStream { - if let Some(span) = self.non_local { - let return_type = &self.output; - quote_spanned! { - span => - turbo_tasks::macro_helpers::assert_returns_non_local_value::<#return_type, _>(); - } - } else if self.operation { + if self.operation { let mut assertions = Vec::new(); // theoretically we could support methods by rewriting the exposed self argument, but // it's not worth it, given the rarity of operations. @@ -706,15 +697,8 @@ pub struct FunctionArguments { /// This should only be used by the task that directly performs the IO. Tasks that transitively /// perform IO should not be manually annotated. io_markers: HashSet, - /// Should we check that the return type contains a `NonLocalValue`? - /// - /// If there is an error due to this option being set, it should be reported to this span. - /// - /// If [`Self::local_cells`] is set, this will also be set to the same span. - non_local_return: Option, /// Should the function return an `OperationVc` instead of a `Vc`? Also ensures that all - /// arguments are `OperationValue`s. Mutually exclusive with the `non_local_return` and - /// `local_cells` flags. + /// arguments are `OperationValue`s. Mutually exclusive with the `local_cells` flag. /// /// If there is an error due to this option being set, it should be reported to this span. operation: Option, @@ -722,8 +706,6 @@ pub struct FunctionArguments { /// executions. Cells can be converted to their non-local versions by calling `Vc::resolve`. /// /// If there is an error due to this option being set, it should be reported to this span. - /// - /// Setting this option will also set [`Self::non_local`] to the same span. pub local_cells: Option, } @@ -746,31 +728,26 @@ impl Parse for FunctionArguments { ("network", Meta::Path(_)) => { parsed_args.io_markers.insert(IoMarker::Network); } - ("non_local_return", Meta::Path(_)) => { - parsed_args.non_local_return = Some(meta.span()); - } ("operation", Meta::Path(_)) => { parsed_args.operation = Some(meta.span()); } ("local_cells", Meta::Path(_)) => { let span = Some(meta.span()); parsed_args.local_cells = span; - parsed_args.non_local_return = span; } (_, meta) => { return Err(syn::Error::new_spanned( meta, - "unexpected token, expected one of: \"fs\", \"network\", \ - \"non_local_return\", \"operation\", or \"local_cells\"", + "unexpected token, expected one of: \"fs\", \"network\", \"operation\", \ + or \"local_cells\"", )) } } } - if let (Some(_), Some(span)) = (parsed_args.non_local_return, parsed_args.operation) { + if let (Some(_), Some(span)) = (parsed_args.local_cells, parsed_args.operation) { return Err(syn::Error::new( span, - "\"operation\" is mutually exclusive with \"non_local_return\" and \ - \"local_cells\" options", + "\"operation\" is mutually exclusive with the \"local_cells\" option", )); } Ok(parsed_args) From 55f25fb2bb9498c5fe4aa1be972df364a6ee6e22 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Thu, 23 Jan 2025 18:57:57 +0100 Subject: [PATCH 14/61] Remove `internal_disableSyncDynamicAPIWarnings` flag (#75231) --- .../webpack/plugins/define-env-plugin.ts | 4 -- packages/next/src/server/config-schema.ts | 1 - packages/next/src/server/request/cookies.ts | 8 +-- .../next/src/server/request/draft-mode.ts | 8 +-- packages/next/src/server/request/headers.ts | 8 +-- .../next/src/server/request/params.browser.ts | 58 +++++++------------ packages/next/src/server/request/params.ts | 16 ++--- .../server/request/search-params.browser.ts | 42 +++++--------- .../next/src/server/request/search-params.ts | 18 ++---- 9 files changed, 56 insertions(+), 107 deletions(-) diff --git a/packages/next/src/build/webpack/plugins/define-env-plugin.ts b/packages/next/src/build/webpack/plugins/define-env-plugin.ts index 08348354a1898b..417df966c57a02 100644 --- a/packages/next/src/build/webpack/plugins/define-env-plugin.ts +++ b/packages/next/src/build/webpack/plugins/define-env-plugin.ts @@ -269,10 +269,6 @@ export function getDefineEnv({ 'process.env.__NEXT_LINK_NO_TOUCH_START': config.experimental.linkNoTouchStart ?? false, 'process.env.__NEXT_ASSET_PREFIX': config.assetPrefix, - 'process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS': - // Internal only so untyped to avoid discovery - (config.experimental as any).internal_disableSyncDynamicAPIWarnings ?? - false, 'process.env.__NEXT_EXPERIMENTAL_AUTH_INTERRUPTS': !!config.experimental.authInterrupts, ...(isNodeOrEdgeCompilation diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index 285be37dbc9943..d42df5ab738435 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -314,7 +314,6 @@ export const configSchema: zod.ZodType = z.lazy(() => imgOptTimeoutInSeconds: z.number().int().optional(), imgOptMaxInputPixels: z.number().int().optional(), imgOptSequentialRead: z.boolean().optional().nullable(), - internal_disableSyncDynamicAPIWarnings: z.boolean().optional(), isrFlushToDisk: z.boolean().optional(), largePageDataBytes: z.number().optional(), linkNoTouchStart: z.boolean().optional(), diff --git a/packages/next/src/server/request/cookies.ts b/packages/next/src/server/request/cookies.ts index 492bc7855df39c..524703b9f4d3da 100644 --- a/packages/next/src/server/request/cookies.ts +++ b/packages/next/src/server/request/cookies.ts @@ -558,11 +558,9 @@ function syncIODev(route: string | undefined, expression: string) { warnForSyncAccess(route, expression) } -const noop = () => {} - -const warnForSyncAccess = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS - ? noop - : createDedupedByCallsiteServerErrorLoggerDev(createCookiesAccessError) +const warnForSyncAccess = createDedupedByCallsiteServerErrorLoggerDev( + createCookiesAccessError +) function createCookiesAccessError( route: string | undefined, diff --git a/packages/next/src/server/request/draft-mode.ts b/packages/next/src/server/request/draft-mode.ts index 37d9902bf1d23f..b7b91f3ce91973 100644 --- a/packages/next/src/server/request/draft-mode.ts +++ b/packages/next/src/server/request/draft-mode.ts @@ -202,11 +202,9 @@ function syncIODev(route: string | undefined, expression: string) { warnForSyncAccess(route, expression) } -const noop = () => {} - -const warnForSyncAccess = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS - ? noop - : createDedupedByCallsiteServerErrorLoggerDev(createDraftModeAccessError) +const warnForSyncAccess = createDedupedByCallsiteServerErrorLoggerDev( + createDraftModeAccessError +) function createDraftModeAccessError( route: string | undefined, diff --git a/packages/next/src/server/request/headers.ts b/packages/next/src/server/request/headers.ts index 10b0ccda878aec..63dc0cee0e30e2 100644 --- a/packages/next/src/server/request/headers.ts +++ b/packages/next/src/server/request/headers.ts @@ -480,11 +480,9 @@ function syncIODev(route: string | undefined, expression: string) { warnForSyncAccess(route, expression) } -const noop = () => {} - -const warnForSyncAccess = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS - ? noop - : createDedupedByCallsiteServerErrorLoggerDev(createHeadersAccessError) +const warnForSyncAccess = createDedupedByCallsiteServerErrorLoggerDev( + createHeadersAccessError +) function createHeadersAccessError( route: string | undefined, diff --git a/packages/next/src/server/request/params.browser.ts b/packages/next/src/server/request/params.browser.ts index a81806cbabced9..bb4804d3f7e78e 100644 --- a/packages/next/src/server/request/params.browser.ts +++ b/packages/next/src/server/request/params.browser.ts @@ -91,43 +91,29 @@ function makeDynamicallyTrackedExoticParamsWithDevWarnings( return proxiedPromise } -const noop = () => {} - -const warnForSyncAccess = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS - ? noop - : function warnForSyncAccess(expression: string) { - if (process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS) { - return - } - - console.error( - `A param property was accessed directly with ${expression}. \`params\` is now a Promise and should be unwrapped with \`React.use()\` before accessing properties of the underlying params object. In this version of Next.js direct access to param properties is still supported to facilitate migration but in a future version you will be required to unwrap \`params\` with \`React.use()\`.` - ) - } - -const warnForEnumeration = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS - ? noop - : function warnForEnumeration(missingProperties: Array) { - if (process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS) { - return - } +function warnForSyncAccess(expression: string) { + console.error( + `A param property was accessed directly with ${expression}. \`params\` is now a Promise and should be unwrapped with \`React.use()\` before accessing properties of the underlying params object. In this version of Next.js direct access to param properties is still supported to facilitate migration but in a future version you will be required to unwrap \`params\` with \`React.use()\`.` + ) +} - if (missingProperties.length) { - const describedMissingProperties = - describeListOfPropertyNames(missingProperties) - console.error( - `params are being enumerated incompletely missing these properties: ${describedMissingProperties}. ` + - `\`params\` should be unwrapped with \`React.use()\` before using its value. ` + - `Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis` - ) - } else { - console.error( - `params are being enumerated. ` + - `\`params\` should be unwrapped with \`React.use()\` before using its value. ` + - `Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis` - ) - } - } +function warnForEnumeration(missingProperties: Array) { + if (missingProperties.length) { + const describedMissingProperties = + describeListOfPropertyNames(missingProperties) + console.error( + `params are being enumerated incompletely missing these properties: ${describedMissingProperties}. ` + + `\`params\` should be unwrapped with \`React.use()\` before using its value. ` + + `Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis` + ) + } else { + console.error( + `params are being enumerated. ` + + `\`params\` should be unwrapped with \`React.use()\` before using its value. ` + + `Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis` + ) + } +} function describeListOfPropertyNames(properties: Array) { switch (properties.length) { diff --git a/packages/next/src/server/request/params.ts b/packages/next/src/server/request/params.ts index 1e516a62029721..fec994c5208b30 100644 --- a/packages/next/src/server/request/params.ts +++ b/packages/next/src/server/request/params.ts @@ -447,18 +447,12 @@ function syncIODev( } } -const noop = () => {} +const warnForSyncAccess = createDedupedByCallsiteServerErrorLoggerDev( + createParamsAccessError +) -const warnForSyncAccess = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS - ? noop - : createDedupedByCallsiteServerErrorLoggerDev(createParamsAccessError) - -const warnForIncompleteEnumeration = process.env - .__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS - ? noop - : createDedupedByCallsiteServerErrorLoggerDev( - createIncompleteEnumerationError - ) +const warnForIncompleteEnumeration = + createDedupedByCallsiteServerErrorLoggerDev(createIncompleteEnumerationError) function createParamsAccessError( route: string | undefined, diff --git a/packages/next/src/server/request/search-params.browser.ts b/packages/next/src/server/request/search-params.browser.ts index 19f53febbb3d24..068640798abe9a 100644 --- a/packages/next/src/server/request/search-params.browser.ts +++ b/packages/next/src/server/request/search-params.browser.ts @@ -122,32 +122,18 @@ function makeUntrackedExoticSearchParams( return promise } -const noop = () => {} - -const warnForSyncAccess = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS - ? noop - : function warnForSyncAccess(expression: string) { - if (process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS) { - return - } - - console.error( - `A searchParam property was accessed directly with ${expression}. ` + - `\`searchParams\` should be unwrapped with \`React.use()\` before accessing its properties. ` + - `Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis` - ) - } - -const warnForSyncSpread = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS - ? noop - : function warnForSyncSpread() { - if (process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS) { - return - } +function warnForSyncAccess(expression: string) { + console.error( + `A searchParam property was accessed directly with ${expression}. ` + + `\`searchParams\` should be unwrapped with \`React.use()\` before accessing its properties. ` + + `Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis` + ) +} - console.error( - `The keys of \`searchParams\` were accessed directly. ` + - `\`searchParams\` should be unwrapped with \`React.use()\` before accessing its properties. ` + - `Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis` - ) - } +function warnForSyncSpread() { + console.error( + `The keys of \`searchParams\` were accessed directly. ` + + `\`searchParams\` should be unwrapped with \`React.use()\` before accessing its properties. ` + + `Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis` + ) +} diff --git a/packages/next/src/server/request/search-params.ts b/packages/next/src/server/request/search-params.ts index 578e95549a42ed..af4eb518ea463b 100644 --- a/packages/next/src/server/request/search-params.ts +++ b/packages/next/src/server/request/search-params.ts @@ -716,18 +716,12 @@ function syncIODev( } } -const noop = () => {} - -const warnForSyncAccess = process.env.__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS - ? noop - : createDedupedByCallsiteServerErrorLoggerDev(createSearchAccessError) - -const warnForIncompleteEnumeration = process.env - .__NEXT_DISABLE_SYNC_DYNAMIC_API_WARNINGS - ? noop - : createDedupedByCallsiteServerErrorLoggerDev( - createIncompleteEnumerationError - ) +const warnForSyncAccess = createDedupedByCallsiteServerErrorLoggerDev( + createSearchAccessError +) + +const warnForIncompleteEnumeration = + createDedupedByCallsiteServerErrorLoggerDev(createIncompleteEnumerationError) function createSearchAccessError( route: string | undefined, From f641c39d9ac07866e3082322bfa41d7978bccf0a Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 23 Jan 2025 14:03:01 -0500 Subject: [PATCH 15/61] router.prefetch should not trigger a React update (#75238) The prefetch reducer doesn't actually modify any React state. It just writes to a mutable cache. So we shouldn't bother calling setState/dispatch; we can just re-run the reducer directly using the current state. --- .../next/src/client/components/app-router.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/next/src/client/components/app-router.tsx b/packages/next/src/client/components/app-router.tsx index c46f2441d995d7..d1dde4bcfd13d5 100644 --- a/packages/next/src/client/components/app-router.tsx +++ b/packages/next/src/client/components/app-router.tsx @@ -61,6 +61,7 @@ import type { AppRouterActionQueue } from '../../shared/lib/router/action-queue' import { prefetch as prefetchWithSegmentCache } from '../components/segment-cache/prefetch' import { getRedirectTypeFromError, getURLFromRedirectError } from './redirect' import { isRedirectError, RedirectType } from './redirect-error' +import { prefetchReducer } from './router-reducer/reducers/prefetch-reducer' const globalMutable: { pendingMpaPath?: string @@ -296,12 +297,16 @@ function Router({ // Use the old prefetch implementation. const url = createPrefetchURL(href) if (url !== null) { - startTransition(() => { - dispatch({ - type: ACTION_PREFETCH, - url, - kind: options?.kind ?? PrefetchKind.FULL, - }) + // The prefetch reducer doesn't actually update any state or + // trigger a rerender. It just writes to a mutable cache. So we + // shouldn't bother calling setState/dispatch; we can just re-run + // the reducer directly using the current state. + // TODO: Refactor this away from a "reducer" so it's + // less confusing. + prefetchReducer(actionQueue.state, { + type: ACTION_PREFETCH, + url, + kind: options?.kind ?? PrefetchKind.FULL, }) } }, From 525b9687a37873f0c7d7fade623f15ad752f6381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Fri, 24 Jan 2025 04:36:06 +0900 Subject: [PATCH 16/61] perf(turbopack): Update SWC plugins (`styled-jsx`, `emotion`) (#75236) ### What? Apply https://github.com/swc-project/plugins/pull/391 ### Why? Those passes allocates too much even when they are not used. --- Cargo.lock | 19 +++++++++++-------- Cargo.toml | 4 ++-- .../src/transform/emotion.rs | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 36fe15c6643d8a..ca79941c6b7fd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4956,7 +4956,7 @@ dependencies = [ "phf", "phf_codegen", "precomputed-hash", - "rustc-hash 2.0.0", + "rustc-hash 2.1.0", "serde", "smallvec", "static-self", @@ -6080,9 +6080,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustc_version" @@ -6905,15 +6905,16 @@ dependencies = [ [[package]] name = "styled_jsx" -version = "0.75.1" +version = "0.75.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "768f794e5cbf87d9ff5eb768f99247c40981c2e666cc5e22fd1eb8dfd6f6cea8" +checksum = "c1f6e318858ba16a2ad6b70a60c3c81266096602faf1839c20c6c060bd707bf2" dependencies = [ "anyhow", "lightningcss", "parcel_selectors", "preset_env_base", "serde", + "swc_atoms", "swc_common", "swc_css_ast", "swc_css_codegen", @@ -6925,6 +6926,7 @@ dependencies = [ "swc_ecma_ast", "swc_ecma_minifier", "swc_ecma_parser", + "swc_ecma_transforms_base", "swc_ecma_utils", "swc_ecma_visit", "swc_plugin_macro", @@ -8058,22 +8060,23 @@ dependencies = [ [[package]] name = "swc_emotion" -version = "0.74.1" +version = "0.74.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e73f5a11086b7d7e70878510e4e4be79d40b8b977f5b39cd224696ed711ddab2" +checksum = "177c1403bd72b2e08ffbf1c13d4567fc789d69564f2e62677a434645b34bc328" dependencies = [ "base64 0.22.1", "byteorder", - "fxhash", "once_cell", "radix_fmt", "regex", + "rustc-hash 2.1.0", "serde", "sourcemap", "swc_atoms", "swc_common", "swc_ecma_ast", "swc_ecma_codegen", + "swc_ecma_transforms", "swc_ecma_utils", "swc_ecma_visit", "swc_trace_macro", diff --git a/Cargo.toml b/Cargo.toml index 56366ba4794764..971518dc70fffd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,8 +103,8 @@ miette = { version = "5.10.0", features = ["fancy"] } mdxjs = "0.2.15" modularize_imports = { version = "0.70.1" } styled_components = { version = "0.98.1" } -styled_jsx = { version = "0.75.1" } -swc_emotion = { version = "0.74.1" } +styled_jsx = { version = "0.75.2" } +swc_emotion = { version = "0.74.2" } swc_relay = { version = "0.46.1" } # General Deps diff --git a/turbopack/crates/turbopack-ecmascript-plugins/src/transform/emotion.rs b/turbopack/crates/turbopack-ecmascript-plugins/src/transform/emotion.rs index 38c190697a7930..3426d0e27413f7 100644 --- a/turbopack/crates/turbopack-ecmascript-plugins/src/transform/emotion.rs +++ b/turbopack/crates/turbopack-ecmascript-plugins/src/transform/emotion.rs @@ -67,7 +67,7 @@ impl EmotionTransformer { // emotion transform. enabled: Some(true), sourcemap: config.sourcemap, - label_format: config.label_format.clone(), + label_format: config.label_format.as_deref().map(From::from), auto_label: if let Some(auto_label) = config.auto_label.as_ref() { match auto_label { EmotionLabelKind::Always => Some(true), From 73e6250fad1358eae3dbf6f439d2f060b3eb8592 Mon Sep 17 00:00:00 2001 From: vercel-release-bot Date: Thu, 23 Jan 2025 20:10:34 +0000 Subject: [PATCH 17/61] v15.2.0-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-refresh-utils/package.json | 2 +- packages/third-parties/package.json | 4 ++-- pnpm-lock.yaml | 16 ++++++++-------- 17 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lerna.json b/lerna.json index 8d8d3b73d31615..52ecb2dd694655 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "15.2.0-canary.22" + "version": "15.2.0-canary.23" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 8b25f22d72e50c..35bc4eeea1b9e0 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "15.2.0-canary.22", + "version": "15.2.0-canary.23", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index dc069deee9b164..a4b4c74ffd0bf4 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "15.2.0-canary.22", + "version": "15.2.0-canary.23", "description": "ESLint configuration used by Next.js.", "main": "index.js", "license": "MIT", @@ -10,7 +10,7 @@ }, "homepage": "https://nextjs.org/docs/app/api-reference/config/eslint", "dependencies": { - "@next/eslint-plugin-next": "15.2.0-canary.22", + "@next/eslint-plugin-next": "15.2.0-canary.23", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index fd2345ac911d6e..541a62154823a5 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": "15.2.0-canary.22", + "version": "15.2.0-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 dee0d9b9d0200c..b5960bd114e8e5 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "15.2.0-canary.22", + "version": "15.2.0-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 9977a7a2c521f3..49ce6776fe7f82 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "15.2.0-canary.22", + "version": "15.2.0-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 b1e727d60cc32b..0af9d964bba3c9 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "15.2.0-canary.22", + "version": "15.2.0-canary.23", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 2dcc98f563bee4..744c9353c9abd4 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "15.2.0-canary.22", + "version": "15.2.0-canary.23", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 0cbe52ff74153d..d23d3a27cf4139 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "15.2.0-canary.22", + "version": "15.2.0-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 0172320cc455cb..953557bfcff129 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "15.2.0-canary.22", + "version": "15.2.0-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 2b1cb72c1d603f..ad3d110fa65ad9 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "15.2.0-canary.22", + "version": "15.2.0-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 25d25d2278b918..8e60b2917e95c3 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "15.2.0-canary.22", + "version": "15.2.0-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 3dbd74fd54e6b3..21cd41fbe0b965 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "15.2.0-canary.22", + "version": "15.2.0-canary.23", "private": true, "scripts": { "clean": "node ../../scripts/rm.mjs native", diff --git a/packages/next/package.json b/packages/next/package.json index 54e9142f7db01d..93d259291438a0 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "15.2.0-canary.22", + "version": "15.2.0-canary.23", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -99,7 +99,7 @@ ] }, "dependencies": { - "@next/env": "15.2.0-canary.22", + "@next/env": "15.2.0-canary.23", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -164,11 +164,11 @@ "@jest/types": "29.5.0", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/font": "15.2.0-canary.22", - "@next/polyfill-module": "15.2.0-canary.22", - "@next/polyfill-nomodule": "15.2.0-canary.22", - "@next/react-refresh-utils": "15.2.0-canary.22", - "@next/swc": "15.2.0-canary.22", + "@next/font": "15.2.0-canary.23", + "@next/polyfill-module": "15.2.0-canary.23", + "@next/polyfill-nomodule": "15.2.0-canary.23", + "@next/react-refresh-utils": "15.2.0-canary.23", + "@next/swc": "15.2.0-canary.23", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.41.2", "@storybook/addon-essentials": "^8.4.7", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 6ac76533fb4d6a..d3ebf5f0f9a5b9 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": "15.2.0-canary.22", + "version": "15.2.0-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 49d1a7f993b3ff..eb886b75b73052 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "15.2.0-canary.22", + "version": "15.2.0-canary.23", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "15.2.0-canary.22", + "next": "15.2.0-canary.23", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "5.7.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a7acce6cc6962..1a0f53c1c87a25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -793,7 +793,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 15.2.0-canary.22 + specifier: 15.2.0-canary.23 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.10.3 @@ -857,7 +857,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 15.2.0-canary.22 + specifier: 15.2.0-canary.23 version: link:../next-env '@swc/counter': specifier: 0.1.3 @@ -985,19 +985,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 15.2.0-canary.22 + specifier: 15.2.0-canary.23 version: link:../font '@next/polyfill-module': - specifier: 15.2.0-canary.22 + specifier: 15.2.0-canary.23 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 15.2.0-canary.22 + specifier: 15.2.0-canary.23 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 15.2.0-canary.22 + specifier: 15.2.0-canary.23 version: link:../react-refresh-utils '@next/swc': - specifier: 15.2.0-canary.22 + specifier: 15.2.0-canary.23 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1661,7 +1661,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 15.2.0-canary.22 + specifier: 15.2.0-canary.23 version: link:../next outdent: specifier: 0.8.0 From 2f0c80fb459957f60bb07b136d5a1f989d0c6c7f Mon Sep 17 00:00:00 2001 From: Jiwon Choi Date: Fri, 24 Jan 2025 06:07:56 +0900 Subject: [PATCH 18/61] [DevOverlay] Enable reactOwnerStack when newDevOverlay is enabled (#75199) Enable `reactOwnerStack` when `newDevOverlay` is enabled to have better call stack output in the new UI. Closes NDX-668 --------- Co-authored-by: Jiachi Liu --- packages/next/src/server/config.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index d45d7fd767cf6d..027962e1346db5 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -440,6 +440,13 @@ function assignDefaults( } } + // TODO(jiwon): remove once we've made new UI default + // Enable reactOwnerStack when newDevOverlay is enabled to have + // better call stack output in the new UI. + if (result.experimental?.newDevOverlay) { + result.experimental.reactOwnerStack = true + } + warnCustomizedOption( result, 'experimental.esmExternals', From 8952a10efdcbe0ecdfb58b26f50a1ab35924f109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Fri, 24 Jan 2025 06:19:52 +0900 Subject: [PATCH 19/61] refactor(turbopack): Remove `ReadRawVcFuture.turbo_task` (#75217) ### Why? `core::ptr::drop_in_place` of `ReadRawVcFuture.turbo_task` is consuming too much time. --- turbopack/crates/turbo-tasks/src/manager.rs | 13 +- turbopack/crates/turbo-tasks/src/raw_vc.rs | 181 +++++++++----------- 2 files changed, 90 insertions(+), 104 deletions(-) diff --git a/turbopack/crates/turbo-tasks/src/manager.rs b/turbopack/crates/turbo-tasks/src/manager.rs index 0a6a76a7b0c495..9e035fdfbd66d2 100644 --- a/turbopack/crates/turbo-tasks/src/manager.rs +++ b/turbopack/crates/turbo-tasks/src/manager.rs @@ -594,8 +594,11 @@ impl TurboTasks { // track a dependency let raw_result = read_task_output_untracked(self, task_id, ReadConsistency::Eventual).await?; - ReadVcFuture::::from(raw_result.into_read_untracked_with_turbo_tasks(self)) - .await?; + turbo_tasks_future_scope( + self.pin(), + ReadVcFuture::::from(raw_result.into_read_untracked_with_turbo_tasks(self)), + ) + .await?; Ok(rx.await?) } @@ -1748,7 +1751,8 @@ pub async fn run_once( // INVALIDATION: A Once task will never invalidate, therefore we don't need to // track a dependency let raw_result = read_task_output_untracked(&*tt, task_id, ReadConsistency::Eventual).await?; - ReadVcFuture::::from(raw_result.into_read_untracked_with_turbo_tasks(&*tt)).await?; + let raw_future = raw_result.into_read_untracked_with_turbo_tasks(&*tt); + turbo_tasks_future_scope(tt, ReadVcFuture::::from(raw_future)).await?; Ok(rx.await?) } @@ -1773,7 +1777,8 @@ pub async fn run_once_with_reason( // INVALIDATION: A Once task will never invalidate, therefore we don't need to // track a dependency let raw_result = read_task_output_untracked(&*tt, task_id, ReadConsistency::Eventual).await?; - ReadVcFuture::::from(raw_result.into_read_untracked_with_turbo_tasks(&*tt)).await?; + let raw_future = raw_result.into_read_untracked_with_turbo_tasks(&*tt); + turbo_tasks_future_scope(tt, ReadVcFuture::::from(raw_future)).await?; Ok(rx.await?) } diff --git a/turbopack/crates/turbo-tasks/src/raw_vc.rs b/turbopack/crates/turbo-tasks/src/raw_vc.rs index 507e8a05513681..7e07d1a51f17f4 100644 --- a/turbopack/crates/turbo-tasks/src/raw_vc.rs +++ b/turbopack/crates/turbo-tasks/src/raw_vc.rs @@ -1,11 +1,4 @@ -use std::{ - fmt::{Debug, Display}, - future::Future, - hash::Hash, - pin::Pin, - sync::Arc, - task::Poll, -}; +use std::{fmt::Display, future::Future, pin::Pin, task::Poll}; use anyhow::Result; use auto_hash_map::AutoSet; @@ -18,7 +11,7 @@ use crate::{ id::{ExecutionId, LocalCellId, LocalTaskId}, manager::{ assert_execution_id, current_task, read_local_cell, read_local_output, read_task_cell, - read_task_output, TurboTasksApi, + read_task_output, with_turbo_tasks, TurboTasksApi, }, registry::{self, get_value_type}, turbo_tasks, CollectiblesSource, ReadConsistency, TaskId, TraitTypeId, ValueType, ValueTypeId, @@ -335,7 +328,6 @@ impl CollectiblesSource for RawVc { } pub struct ReadRawVcFuture { - turbo_tasks: Arc, consistency: ReadConsistency, current: RawVc, untracked: bool, @@ -344,9 +336,7 @@ pub struct ReadRawVcFuture { impl ReadRawVcFuture { pub(crate) fn new(vc: RawVc) -> Self { - let tt = turbo_tasks(); ReadRawVcFuture { - turbo_tasks: tt, consistency: ReadConsistency::Eventual, current: vc, untracked: false, @@ -354,10 +344,8 @@ impl ReadRawVcFuture { } } - fn new_untracked_with_turbo_tasks(vc: RawVc, turbo_tasks: &dyn TurboTasksApi) -> Self { - let tt = turbo_tasks.pin(); + fn new_untracked_with_turbo_tasks(vc: RawVc, _turbo_tasks: &dyn TurboTasksApi) -> Self { ReadRawVcFuture { - turbo_tasks: tt, consistency: ReadConsistency::Eventual, current: vc, untracked: true, @@ -366,9 +354,7 @@ impl ReadRawVcFuture { } fn new_untracked(vc: RawVc) -> Self { - let tt = turbo_tasks(); ReadRawVcFuture { - turbo_tasks: tt, consistency: ReadConsistency::Eventual, current: vc, untracked: true, @@ -377,9 +363,7 @@ impl ReadRawVcFuture { } fn new_strongly_consistent(vc: RawVc) -> Self { - let tt = turbo_tasks(); ReadRawVcFuture { - turbo_tasks: tt, consistency: ReadConsistency::Strong, current: vc, untracked: false, @@ -388,9 +372,7 @@ impl ReadRawVcFuture { } fn new_strongly_consistent_untracked(vc: RawVc) -> Self { - let tt = turbo_tasks(); ReadRawVcFuture { - turbo_tasks: tt, consistency: ReadConsistency::Strong, current: vc, untracked: true, @@ -403,92 +385,91 @@ impl Future for ReadRawVcFuture { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { - self.turbo_tasks.notify_scheduled_tasks(); - // SAFETY: we are not moving this - let this = unsafe { self.get_unchecked_mut() }; - 'outer: loop { - if let Some(listener) = &mut this.listener { - // SAFETY: listener is from previous pinned this - let listener = unsafe { Pin::new_unchecked(listener) }; - if listener.poll(cx).is_pending() { - return Poll::Pending; + with_turbo_tasks(|tt| { + tt.notify_scheduled_tasks(); + // SAFETY: we are not moving this + let this = unsafe { self.get_unchecked_mut() }; + 'outer: loop { + if let Some(listener) = &mut this.listener { + // SAFETY: listener is from previous pinned this + let listener = unsafe { Pin::new_unchecked(listener) }; + if listener.poll(cx).is_pending() { + return Poll::Pending; + } + this.listener = None; } - this.listener = None; - } - let mut listener = match this.current { - RawVc::TaskOutput(task) => { - let read_result = if this.untracked { - this.turbo_tasks - .try_read_task_output_untracked(task, this.consistency) - } else { - this.turbo_tasks - .try_read_task_output(task, this.consistency) - }; - match read_result { - Ok(Ok(vc)) => { - // We no longer need to read strongly consistent, as any Vc returned - // from the first task will be inside of the scope of the first task. So - // it's already strongly consistent. - this.consistency = ReadConsistency::Eventual; - this.current = vc; - continue 'outer; + let mut listener = match this.current { + RawVc::TaskOutput(task) => { + let read_result = if this.untracked { + tt.try_read_task_output_untracked(task, this.consistency) + } else { + tt.try_read_task_output(task, this.consistency) + }; + match read_result { + Ok(Ok(vc)) => { + // We no longer need to read strongly consistent, as any Vc returned + // from the first task will be inside of the scope of the first + // task. So it's already strongly + // consistent. + this.consistency = ReadConsistency::Eventual; + this.current = vc; + continue 'outer; + } + Ok(Err(listener)) => listener, + Err(err) => return Poll::Ready(Err(err)), } - Ok(Err(listener)) => listener, - Err(err) => return Poll::Ready(Err(err)), } - } - RawVc::TaskCell(task, index) => { - let read_result = if this.untracked { - this.turbo_tasks.try_read_task_cell_untracked(task, index) - } else { - this.turbo_tasks.try_read_task_cell(task, index) - }; - match read_result { - Ok(Ok(content)) => { - // SAFETY: Constructor ensures that T and U are binary identical - return Poll::Ready(Ok(content)); + RawVc::TaskCell(task, index) => { + let read_result = if this.untracked { + tt.try_read_task_cell_untracked(task, index) + } else { + tt.try_read_task_cell(task, index) + }; + match read_result { + Ok(Ok(content)) => { + // SAFETY: Constructor ensures that T and U are binary identical + return Poll::Ready(Ok(content)); + } + Ok(Err(listener)) => listener, + Err(err) => return Poll::Ready(Err(err)), } - Ok(Err(listener)) => listener, - Err(err) => return Poll::Ready(Err(err)), } - } - RawVc::LocalOutput(task_id, local_output_id) => { - let read_result = if this.untracked { - this.turbo_tasks.try_read_local_output_untracked( - task_id, - local_output_id, - this.consistency, - ) - } else { - this.turbo_tasks.try_read_local_output( - task_id, - local_output_id, - this.consistency, - ) - }; - match read_result { - Ok(Ok(vc)) => { - this.consistency = ReadConsistency::Eventual; - this.current = vc; - continue 'outer; + RawVc::LocalOutput(task_id, local_output_id) => { + let read_result = if this.untracked { + tt.try_read_local_output_untracked( + task_id, + local_output_id, + this.consistency, + ) + } else { + tt.try_read_local_output(task_id, local_output_id, this.consistency) + }; + match read_result { + Ok(Ok(vc)) => { + this.consistency = ReadConsistency::Eventual; + this.current = vc; + continue 'outer; + } + Ok(Err(listener)) => listener, + Err(err) => return Poll::Ready(Err(err)), } - Ok(Err(listener)) => listener, - Err(err) => return Poll::Ready(Err(err)), } - } - RawVc::LocalCell(execution_id, local_cell_id) => { - return Poll::Ready(Ok(read_local_cell(execution_id, local_cell_id).into())); - } - }; - // SAFETY: listener is from previous pinned this - match unsafe { Pin::new_unchecked(&mut listener) }.poll(cx) { - Poll::Ready(_) => continue, - Poll::Pending => { - this.listener = Some(listener); - return Poll::Pending; - } - }; - } + RawVc::LocalCell(execution_id, local_cell_id) => { + return Poll::Ready( + Ok(read_local_cell(execution_id, local_cell_id).into()), + ); + } + }; + // SAFETY: listener is from previous pinned this + match unsafe { Pin::new_unchecked(&mut listener) }.poll(cx) { + Poll::Ready(_) => continue, + Poll::Pending => { + this.listener = Some(listener); + return Poll::Pending; + } + }; + } + }) } } From 4ebc32bcf674a6fef85005654b0a30cbe476e679 Mon Sep 17 00:00:00 2001 From: HQidea Date: Fri, 24 Jan 2025 05:37:46 +0800 Subject: [PATCH 20/61] fix: typeof msg should compare to object (#75221) ### Fixing a bug The code is located at https://github.com/vercel/next.js/blob/c33eb2b6505f6717a1a1eac10e193da9d9fa1ece/packages/next/src/server/lib/start-server.ts#L429 `typeof msg` should be compared to `'object'`. Otherwise, it makes no sense. --- packages/next/src/server/lib/start-server.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/next/src/server/lib/start-server.ts b/packages/next/src/server/lib/start-server.ts index 43f0d572ac51b0..87cd0440fa252b 100644 --- a/packages/next/src/server/lib/start-server.ts +++ b/packages/next/src/server/lib/start-server.ts @@ -426,7 +426,12 @@ export async function startServer( if (process.env.NEXT_PRIVATE_WORKER && process.send) { process.addListener('message', async (msg: any) => { - if (msg && typeof msg && msg.nextWorkerOptions && process.send) { + if ( + msg && + typeof msg === 'object' && + msg.nextWorkerOptions && + process.send + ) { startServerSpan = trace('start-dev-server', undefined, { cpus: String(os.cpus().length), platform: os.platform(), From b29b6aa720dde6cb4841702c21c7d245082dc548 Mon Sep 17 00:00:00 2001 From: Jiwon Choi Date: Fri, 24 Jan 2025 06:39:31 +0900 Subject: [PATCH 21/61] test: Move `should not cause error when removing loading.js` to flaky manifest (#75239) Moved `should not cause error when removing loading.js` test to flaky manifest. x-ref: [Flakiness Metrics](https://app.datadoghq.com/ci/test-runs?query=test_level%3Atest%20env%3Aci%20%40git.repository.id%3Agithub.com%2Fvercel%2Fnext.js%20%40test.service%3Anextjs%20%40test.status%3Afail%20%40test.name%3A%22app%20dir%20HMR%20should%20not%20cause%20error%20when%20removing%20loading.js%22&agg_m=count&agg_m_source=base&agg_t=count¤tTab=overview&eventStack=&fromUser=false&index=citest&start=1720817553405&end=1721422353405&paused=false) --- test/turbopack-dev-tests-manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/turbopack-dev-tests-manifest.json b/test/turbopack-dev-tests-manifest.json index f94167f0d54ffb..f9f8f17dc633c4 100644 --- a/test/turbopack-dev-tests-manifest.json +++ b/test/turbopack-dev-tests-manifest.json @@ -3770,10 +3770,10 @@ "runtimeError": false }, "test/e2e/app-dir/app-compilation/index.test.ts": { - "passed": ["app dir HMR should not cause error when removing loading.js"], + "passed": [], "failed": [], "pending": [], - "flakey": [], + "flakey": ["app dir HMR should not cause error when removing loading.js"], "runtimeError": false }, "test/e2e/app-dir/app-config-crossorigin/index.test.ts": { From baa4f787d20b36a14ca968895bbb1bf00e15a1f8 Mon Sep 17 00:00:00 2001 From: Jiwon Choi Date: Fri, 24 Jan 2025 06:53:32 +0900 Subject: [PATCH 22/61] [DevOverlay] Align old and new overlay (#74935) Enable the new UI for the CI testings of existing redbox tests. There are several changes made to let the test pass, including backporting changes to the old UI or removing one from the new, and are as follows: - Added back `|` after line number in code frame ([link](https://github.com/vercel/next.js/pull/74935#discussion_r1925725941)) - Removed unnecessary `@` in Terminal component ([link](https://github.com/vercel/next.js/pull/74935#discussion_r1925727156)) - Set open overlay default value to `true` in Pages Router ([link](https://github.com/vercel/next.js/pull/74935#discussion_r1925728104)) - Backport displaying the first first-party call stack frame to the CallStack component ([link](https://github.com/vercel/next.js/pull/74935#discussion_r1925731313)) - Move the devTools component back to the error boundary ([link](https://github.com/vercel/next.js/pull/74935#discussion_r1925732706)) - Was moved out at https://github.com/vercel/next.js/pull/74999#discussion_r1922998021 Closes NDX-674 Closes NDX-687 --- .github/workflows/build_and_test.yml | 6 +- .../webpack/plugins/define-env-plugin.ts | 6 +- .../_experimental/app/error-boundary.tsx | 28 +- .../_experimental/app/react-dev-overlay.tsx | 55 ++-- .../components/code-frame/code-frame.tsx | 4 +- .../dev-tools-indicator.tsx | 7 +- .../internal/next-logo.tsx | 3 +- .../internal/components/terminal/terminal.tsx | 4 +- .../_experimental/pages/error-boundary.tsx | 10 +- .../_experimental/pages/react-dev-overlay.tsx | 63 ++-- .../internal/container/RuntimeError/index.tsx | 2 +- .../acceptance-app/ReactRefreshLogBox.test.ts | 36 ++- .../acceptance/ReactRefreshLogBox.test.ts | 24 +- .../capture-console-error-owner-stack.test.ts | 272 +++++++++--------- .../capture-console-error.test.ts | 262 +++++++++-------- .../app-dir/dynamic-error-trace/index.test.ts | 5 +- .../error-ignored-frames.test.ts | 270 ++++++++--------- .../invalid-element-type.test.ts | 24 +- .../owner-stack-invalid-element-type.test.ts | 36 ++- ...owner-stack-react-missing-key-prop.test.ts | 24 +- .../react-missing-key-prop.test.ts | 16 +- .../app-dir/owner-stack/owner-stack.test.ts | 199 ++++++++----- .../prerender-indicator.test.ts | 54 ++-- ...r-component-next-dynamic-ssr-false.test.ts | 48 +++- .../app-dir/ssr-in-rsc/ssr-in-rsc.test.ts | 32 ++- .../error-on-next-codemod-comment.test.ts | 47 ++- .../non-root-project-monorepo.test.ts | 22 +- .../use-cache-unknown-cache-kind.test.ts | 43 ++- .../use-cache-without-dynamic-io.test.ts | 95 ++++-- test/lib/next-test-utils.ts | 91 +++++- 30 files changed, 1134 insertions(+), 654 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index f750f6c673c657..0936964a0749f3 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -578,7 +578,7 @@ jobs: uses: ./.github/workflows/build_reusable.yml with: nodeVersion: 18.18.2 - afterBuild: __NEXT_EXPERIMENTAL_PPR=true NEXT_EXTERNAL_TESTS_FILTERS="test/ppr-tests-manifest.json" node run-tests.js --timings -c ${TEST_CONCURRENCY} --type integration + afterBuild: __NEXT_EXPERIMENTAL_PPR=true __NEXT_EXPERIMENTAL_NEW_DEV_OVERLAY=true NEXT_EXTERNAL_TESTS_FILTERS="test/ppr-tests-manifest.json" node run-tests.js --timings -c ${TEST_CONCURRENCY} --type integration stepName: 'test-ppr-integration' secrets: inherit @@ -593,7 +593,7 @@ jobs: group: [1/6, 2/6, 3/6, 4/6, 5/6, 6/6] uses: ./.github/workflows/build_reusable.yml with: - afterBuild: __NEXT_EXPERIMENTAL_PPR=true NEXT_EXTERNAL_TESTS_FILTERS="test/ppr-tests-manifest.json" NEXT_TEST_MODE=dev node run-tests.js --timings -g ${{ matrix.group }} -c ${TEST_CONCURRENCY} --type development + afterBuild: __NEXT_EXPERIMENTAL_PPR=true __NEXT_EXPERIMENTAL_NEW_DEV_OVERLAY=true NEXT_EXTERNAL_TESTS_FILTERS="test/ppr-tests-manifest.json" NEXT_TEST_MODE=dev node run-tests.js --timings -g ${{ matrix.group }} -c ${TEST_CONCURRENCY} --type development stepName: 'test-ppr-dev-${{ matrix.group }}' secrets: inherit @@ -608,7 +608,7 @@ jobs: group: [1/7, 2/7, 3/7, 4/7, 5/7, 6/7, 7/7] uses: ./.github/workflows/build_reusable.yml with: - afterBuild: __NEXT_EXPERIMENTAL_PPR=true NEXT_EXTERNAL_TESTS_FILTERS="test/ppr-tests-manifest.json" NEXT_TEST_MODE=start node run-tests.js --timings -g ${{ matrix.group }} -c ${TEST_CONCURRENCY} --type production + afterBuild: __NEXT_EXPERIMENTAL_PPR=true __NEXT_EXPERIMENTAL_NEW_DEV_OVERLAY=true NEXT_EXTERNAL_TESTS_FILTERS="test/ppr-tests-manifest.json" NEXT_TEST_MODE=start node run-tests.js --timings -g ${{ matrix.group }} -c ${TEST_CONCURRENCY} --type production stepName: 'test-ppr-prod-${{ matrix.group }}' secrets: inherit diff --git a/packages/next/src/build/webpack/plugins/define-env-plugin.ts b/packages/next/src/build/webpack/plugins/define-env-plugin.ts index 417df966c57a02..f15fb1c4b0c83f 100644 --- a/packages/next/src/build/webpack/plugins/define-env-plugin.ts +++ b/packages/next/src/build/webpack/plugins/define-env-plugin.ts @@ -286,7 +286,11 @@ export function getDefineEnv({ } : undefined), 'process.env.__NEXT_EXPERIMENTAL_NEW_DEV_OVERLAY': - config.experimental.newDevOverlay ?? false, + // When `__NEXT_EXPERIMENTAL_NEW_DEV_OVERLAY` is set on CI, + // we need to pass it here so it can be enabled. + process.env.__NEXT_EXPERIMENTAL_NEW_DEV_OVERLAY === 'true' || + config.experimental.newDevOverlay || + false, } const userDefines = config.compiler?.define ?? {} diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx index 804918cda20ab6..799d73a03892a1 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/app/error-boundary.tsx @@ -3,13 +3,14 @@ import type { GlobalErrorComponent } from '../../../error-boundary' import { PureComponent } from 'react' import { RuntimeErrorHandler } from '../../../errors/runtime-error-handler' -type DevToolsErrorBoundaryProps = { +type DevOverlayErrorBoundaryProps = { children: React.ReactNode - onError: (value: boolean) => void + devOverlay: React.ReactNode globalError: [GlobalErrorComponent, React.ReactNode] + onError: (value: boolean) => void } -type DevToolsErrorBoundaryState = { +type DevOverlayErrorBoundaryState = { isReactError: boolean reactError: unknown } @@ -37,9 +38,9 @@ function ErroredHtml({ ) } -export class DevToolsErrorBoundary extends PureComponent< - DevToolsErrorBoundaryProps, - DevToolsErrorBoundaryState +export class DevOverlayErrorBoundary extends PureComponent< + DevOverlayErrorBoundaryProps, + DevOverlayErrorBoundaryState > { state = { isReactError: false, reactError: null } @@ -61,13 +62,18 @@ export class DevToolsErrorBoundary extends PureComponent< } render() { + const { children, globalError, devOverlay } = this.props + const { isReactError, reactError } = this.state + const fallback = ( - + ) - return this.state.isReactError ? fallback : this.props.children + return ( + <> + {isReactError ? fallback : children} + {devOverlay} + + ) } } diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx index 589d0a56a157b6..fbcac3085401d3 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/app/react-dev-overlay.tsx @@ -2,7 +2,7 @@ import type { OverlayState } from '../../shared' import type { GlobalErrorComponent } from '../../../error-boundary' import { useState } from 'react' -import { DevToolsErrorBoundary } from './error-boundary' +import { DevOverlayErrorBoundary } from './error-boundary' import { ShadowPortal } from '../internal/components/shadow-portal' import { Base } from '../internal/styles/base' import { ComponentStyles } from '../internal/styles/component-styles' @@ -24,34 +24,35 @@ export default function ReactDevOverlay({ const [isErrorOverlayOpen, setIsErrorOverlayOpen] = useState(false) const { readyErrors } = useErrorHook({ errors: state.errors, isAppDir: true }) - return ( - <> - - {children} - + const devOverlay = ( + + + + + - - - - - + - + + + ) - - - + return ( + + {children} + ) } diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/code-frame/code-frame.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/code-frame/code-frame.tsx index ee870c4b0bc4f5..e26a42e1095260 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/code-frame/code-frame.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/code-frame/code-frame.tsx @@ -36,9 +36,7 @@ export function CodeFrame({ stackFrame, codeFrame }: CodeFrameProps) { .map((line, a) => ~(a = line.indexOf('|')) ? line.substring(0, a) + - line - .substring(a + 1) - .replace(`^\\ {${miniLeadingSpacesLength}}`, '') + line.substring(a).replace(`^\\ {${miniLeadingSpacesLength}}`, '') : line ) .join('\n') diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/dev-tools-indicator.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/dev-tools-indicator.tsx index e238741f80d852..a281ee3f099f95 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/dev-tools-indicator.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/dev-tools-indicator.tsx @@ -126,6 +126,7 @@ const DevToolsPopover = ({ return ( @@ -205,14 +207,15 @@ const IndicatorRow = ({ label, value, onClick, + ...props }: { label: string value: React.ReactNode onClick?: () => void -}) => { +} & React.HTMLAttributes) => { const Wrapper = onClick ? 'button' : 'div' return ( - + {label} {value} diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/internal/next-logo.tsx b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/internal/next-logo.tsx index d669d16b5b2375..125168b0610617 100644 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/internal/next-logo.tsx +++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/components/errors/dev-tools-indicator/internal/next-logo.tsx @@ -301,7 +301,8 @@ export const NextLogo = ({ aria-label="Open issues overlay" onClick={onIssuesClick} > - {issueCount} {issueCount === 1 ? 'Issue' : 'Issues'} + {issueCount}{' '} + {issueCount === 1 ? 'Issue' : 'Issues'}